JSON库(1) 初识JSON库,建立TDD

本文最后更新于:1 年前

JSON库(1) 初识JSON库,建立TDD

写在前面

这段时间一直在忙于底层知识框架的整理,很是头疼。想着劳逸结合一下,开个新坑,从零开始实现一个JSON库。之前也没有接触过JSON库,只是略有耳闻,花了一天的时间大概理解了这个项目,觉得难度非常适合新手,特开此专栏用以记录。这一节内容比较简单,实现几个最基本的功能,再把我们的测试文件TDD建立了就OK了。

实验环境

本专栏参考叶劲峰老师在2016年写的的JSON库教程从零开始的JSON库教程,感谢作者。

实验材料取自配套文件GitHub链接。本专栏将从零建立,可以不用下载配套文件。

系统为搭载ubuntu16.04版本的虚拟机。

cmake版本为3.5.1

实验内容

1 创建leptjson.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/* 利用宏避免重复声明 */
#ifndef LEPTJSON_H__
#define LEPTJSON_H__

/* JSON存在6种数据类型 */
typedef enum {
LEPT_NULL,
LEPT_FALSE,
LEPT_TRUE,
LEPT_NUMBER,
LEPT_STRING,
LEPT_ARRAY,
LEPT_OBJECT
} lept_type;

/* JSON值 */
typedef struct {
lept_type type;
}lept_value;

/* 解析函数的返回值 */
enum {
LEPT_PARSE_OK = 0,
LEPT_PARSE_EXPECT_VALUE,
LEPT_PARSE_INVALID_VALUE,
LEPT_PARSE_ROOT_NOT_SINGULAR
};

/* 解析JSON */
int lept_parse(lept_value* v, const char* json);

/* 访问结果,获取类型 */
lept_type lept_get_type(const lept_value* v);

#endif /* LEPTJSON_H__ */

上来先把头文件和API搭建了,主要实现功能如下(配合代码中注释理解),

1 利用宏避免重复声明,这是每个项目的必备元素。

2 利用enum声明了7个JSON的基本类型(truefalse算两个)。

3 建立结构体lept_value声明JSON的数据结构,我们要实现一个树作为数据结构,当前只需要类型作为其中参数。

4 创建两个API函数,一个用来解析JSON,一个用来访问结果。

2 创建leptjson.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
/*  leptjson实现文件,此文件将编译成库 */
#include "leptjson.h"
#include <assert.h> /* assert() */
#include <stdlib.h> /* NULL */

/* 检测c->json是否指向目标值 c指向下一个字符*/
#define EXPECT(c, ch) do { assert(*c->json == (ch)); c->json++; } while(0)

/* 存放参数(JSON字符串) */
typedef struct {
const char* json;
}lept_context;

/* ws = *(%x20 / %09 / %x0A / %x0D) */
/* 消除空格字符 */
static void lept_parse_whitespace(lept_context* c) {
const char *p = c->json; /* 创建指针p指向该字符串 */
/* 将指针移动到非空格字符 */
while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')
p++;
c->json = p;
}

/* null = "null" */
static int lept_parse_null(lept_context* c, lept_value* v) {
EXPECT(c, 'n'); /* 检测+移动指针 */
/* 后面三个字符不为ull */
if (c->json[0] != 'u' || c->json[1] != 'l' || c->json[2] != 'l')
return LEPT_PARSE_INVALID_VALUE;
/* 后面三个字符为ull */
c->json += 3; /* 指针后移三位 */
v->type = LEPT_NULL; /* type转变为null */
return LEPT_PARSE_OK;
}

/* 第3题答案 */
/* 与lept_parse_null基本一致 */
/* 注意在lept_parse_value中添加调用接口 */
/* true = "true" */
static int lept_parse_true(lept_context* c, lept_value* v) {
EXPECT(c, 't');
if (c->json[0] != 'r' || c->json[1] != 'u' || c->json[2] != 'e')
return LEPT_PARSE_INVALID_VALUE;
c->json += 3;
v->type = LEPT_TRUE;
return LEPT_PARSE_OK;
}

/* false = "false" */
static int lept_parse_false(lept_context* c, lept_value* v) {
EXPECT(c, 'f');
if (c->json[0] != 'a' || c->json[1] != 'l' || c->json[2] != 's' || c->json[3] != 'e')
return LEPT_PARSE_INVALID_VALUE;
c->json += 4;
v->type = LEPT_FALSE;
return LEPT_PARSE_OK;
}

/* value = null / false / true */
static int lept_parse_value(lept_context* c, lept_value* v) {
/* 检验当前字符串的首个字符,跳转到对应函数 */
switch (*c->json) {
case 'n': return lept_parse_null(c, v);
case 't': return lept_parse_true(c, v);
case 'f': return lept_parse_false(c, v);
case '\0': return LEPT_PARSE_EXPECT_VALUE;
default: return LEPT_PARSE_INVALID_VALUE;
}
}

/* 解析器 */
/* 暂时只储存JSON字符串当前位置 */
int lept_parse(lept_value* v, const char* json) {
lept_context c; /* 创建新结构体c,保存字符串 */
assert(v != NULL); /* v为空,弹出报错 */
c.json = json; /* 字符串保存到c中 */
v->type = LEPT_NULL; /* v的type赋为null */
lept_parse_whitespace(&c); /* 消除JSON字符串前面的空格字符 */
/* 第1题答案 解决字符串中间还有空格的问题 */
int ret;
/* 如果字符串正确,进入循环 */
if ((ret = lept_parse_value(&c, v)) == LEPT_PARSE_OK) {
lept_parse_whitespace(&c); /* 每次检验之前消一次空格 */
/* 指针指向JSON末尾,退出循环 */
if (*c.json != '\0') {
v->type = LEPT_NULL; /* 注意把type重新置为null */
ret = LEPT_PARSE_ROOT_NOT_SINGULAR;
}
}
return ret;
/* return lept_parse_value(&c, v); */
}

lept_type lept_get_type(const lept_value* v) {
assert(v != NULL);
return v->type;

这是我们leptjson的主要实现程序了,咱们一个个函数解释一下:

1 EXPECT宏,利用assert断言检测字符串并移动指针。

2 lept_context结构体,用以简化操作,减少解析函数之间传递多个参数。

3 函数lept_parse_whitespace,每调用一次就会将指针移动到第一个字符处,即跳过空格。

4 函数lept_parse_null lept_parse_true lept_parse_false,分别检测指针指向的字符串是否是null true false,实现也都大同小异。如果正确,返回与之对应的之前声明的数据类型,否则返回LEPT_PARSE_INVALID_VALUE

5 函数lept_parse_value,作为一个中转单位,根据字符串首个字符跳转到对应函数。

5 函数lept_parselept_get_type前面介绍过了,不再重复。

3 创建test.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
/* 单元测试(TDD) */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "leptjson.h"

static int main_ret = 0;
static int test_count = 0;
static int test_pass = 0;

/* 如果expect != actual,打印错误信息 */
#define EXPECT_EQ_BASE(equality, expect, actual, format) \
do {\
test_count++;\
if (equality)\
test_pass++;\
else {\
fprintf(stderr, "%s:%d: expect: " format " actual: " format "\n", __FILE__, __LINE__, expect, actual);\
main_ret = 1;\
}\
} while(0)

/* 调用EXPECT_EQ_BASE,检验预期值和实际值是否相等 */
#define EXPECT_EQ_INT(expect, actual) EXPECT_EQ_BASE((expect) == (actual), expect, actual, "%d")

static void test_parse_null() {
lept_value v; /* 定义JSON值结构体v 参数为type */
v.type = LEPT_FALSE; /* v的type初始化为false */
/* 过单元测试 */
/* 预期值,实际值 */
EXPECT_EQ_INT(LEPT_PARSE_OK, lept_parse(&v, "null"));
EXPECT_EQ_INT(LEPT_NULL, lept_get_type(&v));
}

/* 第2题答案 */
/* 和test_parse_null函数只有传入JSON有差别 */
/* 注意在test_parse函数中调用这两个函数 */
static void test_parse_true() {
lept_value v;
v.type = LEPT_FALSE;
EXPECT_EQ_INT(LEPT_PARSE_OK, lept_parse(&v, "true"));
EXPECT_EQ_INT(LEPT_TRUE, lept_get_type(&v));
}

static void test_parse_false() {
lept_value v;
v.type = LEPT_TRUE;
EXPECT_EQ_INT(LEPT_PARSE_OK, lept_parse(&v, "false"));
EXPECT_EQ_INT(LEPT_FALSE, lept_get_type(&v));
}

static void test_parse_expect_value() {
lept_value v;

v.type = LEPT_FALSE;
EXPECT_EQ_INT(LEPT_PARSE_EXPECT_VALUE, lept_parse(&v, ""));
EXPECT_EQ_INT(LEPT_NULL, lept_get_type(&v));

v.type = LEPT_FALSE;
EXPECT_EQ_INT(LEPT_PARSE_EXPECT_VALUE, lept_parse(&v, " "));
EXPECT_EQ_INT(LEPT_NULL, lept_get_type(&v));
}

static void test_parse_invalid_value() {
lept_value v; /* 定义JSON值结构体v 参数为type */
v.type = LEPT_FALSE; /* v的type初始化为false */
/* 过单元测试 */
/* 预期值,实际值 */
EXPECT_EQ_INT(LEPT_PARSE_INVALID_VALUE, lept_parse(&v, "nul"));
EXPECT_EQ_INT(LEPT_NULL, lept_get_type(&v));

v.type = LEPT_FALSE;
EXPECT_EQ_INT(LEPT_PARSE_INVALID_VALUE, lept_parse(&v, "?"));
EXPECT_EQ_INT(LEPT_NULL, lept_get_type(&v));
}

/* 测试字符串中间包含空格 */
static void test_parse_root_not_singular() {
lept_value v; /* 定义JSON值结构体v 参数为type */
v.type = LEPT_FALSE; /* v的type初始化为false */
/* 过单元测试 */
/* 预期值,实际值 */
EXPECT_EQ_INT(LEPT_PARSE_ROOT_NOT_SINGULAR, lept_parse(&v, "null x"));
EXPECT_EQ_INT(LEPT_NULL, lept_get_type(&v));
}

static void test_parse() {
test_parse_null();
/* 调用第2题新增两个函数 */
test_parse_true();
test_parse_false();

test_parse_expect_value();
test_parse_invalid_value();
test_parse_root_not_singular();
}

int main() {
test_parse();
printf("%d/%d (%3.2f%%) passed\n", test_pass, test_count, test_pass * 100.0 / test_count);
return main_ret;
}

这是我们的测试驱动开发(test-driven development, TDD)程序,它的主要循环步骤是:

1 加入一个测试。
2 运行所有测试,新的测试应该会失败。
3 编写实现代码。
4 运行所有测试,若有测试失败回到3。
5 重构代码。
6 回到 1。

其实注释已经非常详细了,不过我还是简单写一下各部分的功能吧。

1 宏定义EXPECT_EQ_BASE,检测实际值和预期值是否对应。

2 宏定义EXPECT_EQ_INT,调用EXPECT_EQ_BASE

3 函数test_parse_null test_parse_true test_parse_false分别检验字符串为null true false的情况。

4 函数test_parse_expect_value检验字符串为空和空格的情况。

5 函数test_parse_invalid_value检验字符串为nul ?的情况,即错误字符串。

6 函数test_parse_root_not_singular检验字符串为null x的情况,即中间有空格。

7 函数test_parse 负责调用上面所说的几个测试函数。

到这里我们程序的三个文件就全部编写完成了,下面我们就要对它们使用cmake编译链接了,在这之前需要再创建一个cmake的编译文件。

4 创建CMakeLists.txt

1
2
3
4
5
6
7
8
9
10
cmake_minimum_required (VERSION 2.6)
project (leptjson_test C)

if (CMAKE_C_COMPILER_ID MATCHES "GNU|Clang")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -ansi -pedantic -Wall")
endif()

add_library(leptjson leptjson.c)
add_executable(leptjson_test test.c)
target_link_libraries(leptjson_test leptjson)

编译运行

在该文件夹下执行如下命令:

1
2
3
4
$mkdir build
$cd build
$cmake ..
$make

会看到出现了一个build文件夹,然后我们在build文件夹里就能运行文件了,执行命令:

1
$./leptjson_test

可以得到运行结果:

图为命令行界面

结果显示这些检测函数得到的全部预期值都和实际值相等,说明主体程序是没有问题的。

写在后面

争取这个项目能在9月完成吧,马上就又恢复线下课了,不知道还能不能抽出这么多时间来自学了。对了,今天是中秋节了,奈何学校依然封控,好久没出去玩了。