初级项目_JSON库

项目参考https://github.com/miloyip/json-tutorial

https://sourceforge.net/projects/cjson/

一、实现

首先我们要明白什么是JSON库,在了解JSON库之前就需要JSON是什么,这里我们引用网络爬虫那一篇里关于JSON的介绍

1.JSON数据格式

JSON,全称为 JavaScript Object Notation, 也就是 JavaScript 对象标记,它通过对象和数组的组合来表示数据,构造简洁但是结构化程度非常高,是一种轻量级的数据交换格式

1.1 JSON数据

JSON数据可能的数据类型为如下6种:

  • null: 表示为 null
  • boolean: 表示为 true 或 false
  • number: 一般的浮点数表示方式,如 3.14
  • string: 表示为 “…”

千万注意 JSON 字符串类型的数据表示需要使用双引号而非单引号(这点与其他编程语言不同)!

  • array: 表示为 [ … ]
  • object: 表示为 { … }

以上六种数据类型中最常用的是数组和对象(因为JSON数据本质上就是一个序列化的对象或数组,至于字符串等类型只是其组成部分):

  • 对象:它在 JavaScript 中是使用花括号 {} 包裹起来的内容,数据结构为 {key1:value1, key2:value2, …} 的键值对结构,键名只能使用字符串,键值可以是以上任何数据类型

  • 数组:数组在 JavaScript 中是方括号 [] 包裹起来的内容,数据结构为 [“java”, “javascript”, “vb”, …] 的索引结构,数组值的类型可以是以上任意类型

一个 JSON 数据可以写为如下形式(由于最外层是中括号,所以最终该JSON数据的类型是列表类型):

1
2
3
4
5
6
7
8
9
[{
"name": "Bob",
"gender": "male",
"birthday": "1992-10-18"
}, {
"name": "Selina",
"gender": "female",
"birthday": "1995-10-18"
}]

尽管JSON源自JS语言,但它只是一种数据格式,可用于任何编程语言(JSON、XML、YAML这方面具有相似性);

1.2 JSON库

我们可以调用 JSON 库的 loads 方法将文本字符串转为 JSON 数据,可以通过 dumps() 方法将 JSON 数据转为文本字符串;

1.2.1 JSON文本转换为JSON数据

如果从 JSON 文本中读取内容,例如这里有一个 data.json 文本文件,其内容是之前定义过的 JSON 字符串(一定注意JSON文本文件中保存的就是普通的字符串!!!因为这种字符串可以转换为JSON数据所以我们称为JSON字符串或JSON文本),我们可以先将文本文件内容读出,然后再利用 loads 方法转化

1
2
3
4
5
6
7
8
9
10
11
#data.json

[{
"name": "Bob",
"gender": "male",
"birthday": "1992-10-18"
}, {
"name": "Selina",
"gender": "female",
"birthday": "1995-10-18"
}]
1
2
3
4
5
6
import json

with open('data.json', 'r') as file:
str = file.read()
data = json.loads(str)#这里使用 loads 方法将字符串转为 JSON 对象
print(data)

运行结果如下

1
[{'name': 'Bob', 'gender': 'male', 'birthday': '1992-10-18'}, {'name': 'Selina', 'gender': 'female', 'birthday': '1995-10-18'}]

1.2.2 JSON数据转换为JSON文本

一般我们将JSON数据转换为JSON文本是为了保存相关信息,对于保存JSON文本有两种方法:

  • 一种是先转换为JSON文本再调用文件的write()方法写入文本,保存为普通文本字符串格式
1
2
with open('data.json', 'w') as file:
file.write(json.dumps(data))

  • 另一种是先将JSON数据转换为文本字符串,再将文本字符串转调整为JSON树状格式保存(增加参数indent表示缩进字符个数使调整为JSON格式),本质上还是普通文本字符串只是缩进更美观;
1
2
with open('data.json', 'w') as file:
file.write(json.dumps(data, indent=2))

当写入文本中有中文时,需要指定参数 ensure_ascii 为 False,另外还要规定文件输出的编码

1
2
with open('data.json', 'w', encoding='utf-8') as file:
file.write(json.dumps(data, indent=2, ensure_ascii=False))

我们从上面也可以看到,Python中处理JSON文本就借助了Python中的JSON库,当然我们这里要实现的是以C/C++来实现一个JSON库,我们的JSON库主要完成三个需求:

  1. JSON 文本解析为一个JSON数据(parse);
  2. 提供接口访问该数据结构(access);
  3. 把JSON数据转换成 JSON 文本(stringify);

2.Parse部分

本部分主要介绍如何将JSON文本转换为JSON数据

1
2
3
4
5
6
7
8
输入 -> 输出
字符串类型:"\"Hello\" -> "Hello"
数值类型:"123.31" -> 123.31
空值:"null" -> null
布尔值:"false" -> false
布尔值:"true" ->true
数组:"[123]" -> [123]
对象:"{"hello":123}" -> {"hello":123}

需要注意的有几点:

  • 我们在测试的时候输入的都是JSON文本(这是符合实际生产要求的,因为我们从.json文件中取出来的也是json文本),也就是带双引号的json数据,而转换过后的JSON数据只有字符串类型是带双引号的;
  • 至于为什么测试JSON字符串的时候需要加上转义的双引号,因为我们在测试的时候,最外层的双引号模拟的是.json文件,双引号内部的数据才是.json文件中的内容,内部的转义双引号模拟的是.json文件中的json字符串;
  • 可能很多人分不清json文本和json数据的区别,我们可以简单理解为,json文本是加了双引号的json数据;

2.1 数据结构

2.1.1 节点类型

JSON 中有 6 种数据类型,如果把 true 和 false 当作两个类型就是 7 种,为此声明一个lept_type枚举类型

1
typedef enum { LEPT_NULL, LEPT_FALSE, LEPT_TRUE, LEPT_NUMBER, LEPT_STRING, LEPT_ARRAY, LEPT_OBJECT } lept_type;

2.1.2 节点结构

JSON数据是树结构,每个节点使用lept_value结构体表示,我们称它为一个JSON数据值

1
2
3
4
5
6
7
8
9
struct lept_value {
union {
struct { lept_member* m; size_t size; }o; /* object: members, member count */
struct { lept_value* e; size_t size; }a; /* array: elements, element count */
struct { char* s; size_t len; }s; /* string: null-terminated string, string length */
double n; /* number */
}u;
lept_type type; /*JSON节点类型,七选一*/
};

2.1.3 对象成员

JSON对象由对象成员构成,对象成员就是键值对,其中键只能是字符串类型,值可以是七种类型中的任何一种;

这里我们使用lept_value结构体来保存对象成员的,但是对象成员的我们舍弃使用lept_value因为不需要type这个字段;

1
2
3
4
struct lept_member {
char* k; size_t klen; /* member key string, key string length */
lept_value v; /* member value */
};

2.1.4 Debug标识

借助枚举常量的方式定义一些必要的Debug标识,lept_parse()函数的返回值是这些枚举值其中之一

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
enum {
LEPT_PARSE_OK = 0, //lept_parse()解析正常
LEPT_PARSE_EXPECT_VALUE, //json字符串只含空白如""
LEPT_PARSE_INVALID_VALUE, //非法json字符串(无法解析)比如"nul" "?"
LEPT_PARSE_ROOT_NOT_SINGULAR, //json字符串中一个值与另一个值之间使用空格"null k",或者整数以0开头"0123"
LEPT_PARSE_NUMBER_TOO_BIG, //json值为超出double表示范围的Number
LEPT_PARSE_MISS_QUOTATION_MARK, //json值为字符串时,只存在开始双引号没有结束双引号如"string
LEPT_PARSE_INVALID_STRING_ESCAPE, //不合法的转义序列如 \'
LEPT_PARSE_INVALID_STRING_CHAR, //不合法的字符 %x00 至 %x1F
LEPT_PARSE_INVALID_UNICODE_HEX, // \u 后不是 4 位十六进位数字
LEPT_PARSE_INVALID_UNICODE_SURROGATE, //只有高代理项而欠缺低代理项,或是低代理项不在合法码点范围
LEPT_PARSE_MISS_COMMA_OR_SQUARE_BRACKET,//json值为数组时,只存在开始中括号没有结束中括号如[123,21
LEPT_PARSE_MISS_KEY, //json值为对象时缺少键
LEPT_PARSE_MISS_COLON, //json值为对象时缺少值
LEPT_PARSE_MISS_COMMA_OR_CURLY_BRACKET //json值为对象时,只存在开始大括号不存在结束大括号如{"123:123

2.1.5 缓冲结构

在JSON解析的时候,我们需要一个合适的缓冲区结构体来存储临时的解析结果(特别是像字符串、数组、对象这种很长的数据类型)

1
2
3
4
5
typedef struct {
const char* json;
char* stack;
size_t size, top;
}lept_context;

2.2 解析器

我们分别按照从简单数据类型到复合数据类型的方式介绍(复合数据类型的解析器采用的是简单类型解析器的递归调用)

2.2.1 解析器外部接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int lept_parse(lept_value* v, const char* json) {
lept_context c;
int ret;
assert(v != NULL);
c.json = json;
c.stack = NULL;
c.size = c.top = 0;
lept_init(v);
lept_parse_whitespace(&c);
if ((ret = lept_parse_value(&c, v)) == LEPT_PARSE_OK) {
lept_parse_whitespace(&c);
if (*c.json != '\0') {
v->type = LEPT_NULL;
ret = LEPT_PARSE_ROOT_NOT_SINGULAR;
}
}
assert(c.top == 0);
free(c.stack);
return ret;
}

2.2.2 解析器核心

这部分负责选择将JSON文本交给哪个解析器进行解析

1
2
3
4
5
6
7
8
9
10
11
12
static int lept_parse_value(lept_context* c, lept_value* v) {
switch (*c->json) {
case 't': return lept_parse_literal(c, v, "true", LEPT_TRUE);
case 'f': return lept_parse_literal(c, v, "false", LEPT_FALSE);
case 'n': return lept_parse_literal(c, v, "null", LEPT_NULL);
default: return lept_parse_number(c, v);
case '"': return lept_parse_string(c, v);
case '[': return lept_parse_array(c, v);
case '{': return lept_parse_object(c, v);
case '\0': return LEPT_PARSE_EXPECT_VALUE;
}
}
(1)literal解析器

该解析器处理 “null” “true” “false” 的JSON文本;

注意之前提出的一个问题:为什么不能直接使用memcmp()这种函数比较是否是null true false而使用指针挨着比较每个字符呢?

实际上我们使用memcmp()也能解决这个问题,作者应该是为了让我们更加熟练的使用指针所以使用了指针的方法;

1
2
3
4
5
6
7
8
9
10
static int lept_parse_literal(lept_context* c, lept_value* v, const char* literal, lept_type type) {
size_t i;
EXPECT(c, literal[0]);
for (i = 0; literal[i + 1]; i++)
if (c->json[i] != literal[i + 1])
return LEPT_PARSE_INVALID_VALUE;
c->json += i;
v->type = type;
return LEPT_PARSE_OK;
}
1
2
3
4
5
6
7
8
9
10
//使用memcmp()
static int lept_parse_true(lept_context* c, lept_value* v) {
if (memcmp(c->json,"true",4)!=0)
{
return LEPT_PARSE_INVALID_VALUE;
}
c->json += 4;
v->type = LEPT_TRUE;
return LEPT_PARSE_OK;
}
(2)number解析器

该解析器处理json值为数字的JSON文本;

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
//完整解析数字的方案应该是先检查数字是否合法,接着再使用strtod()函数进行转换,然后判断数值是否过大
//将数字字符串转换为double便于存储在节点的double成员中,借助标准库strtod(),如果不借助这个库纯手写也不是不行
/*
double strtod(const char *nptr,char **endptr);

strtod()会扫描参数nptr字符串,跳过前面的空格字符,直到遇上数字或正负符号才开始做转换,
到出现非数字或字符串结束时('\0')才结束转换,并将结果返回。若endptr不为NULL,则会将遇到不合条件而终止的nptr中的字符指针由endptr传回。
参数nptr字符串可包含正负号、小数点或E(e)来表示指数部分。
*/
static int lept_parse_number(lept_context* c, lept_value* v) {
//1.首先需要校验数字是否合法,使用指针p表示当前的解析字符位置
const char* p = c->json;
//2.假如当前有负号或者0(会判断是否是独0)则直接跳过,除了这两种情况则第一个字符必选是1~9
if (*p == '-') p++;
if (*p == '0') p++;
else {
if (!ISDIGIT1TO9(*p)) return LEPT_PARSE_INVALID_VALUE;
for (p++; ISDIGIT(*p); p++);
}
//3.如果出现小数点,跳过小数点检查其至少有一个digit(数字0~9)
if (*p == '.') {
p++;
if (!ISDIGIT(*p)) return LEPT_PARSE_INVALID_VALUE;
for (p++; ISDIGIT(*p); p++);
}
//4.如果出现指数(大小写的e),跳过出现的正负号,之后至少有一个digit(数字0~9),与小数相同逻辑
if (*p == 'e' || *p == 'E') {
p++;
if (*p == '+' || *p == '-') p++;
if (!ISDIGIT(*p)) return LEPT_PARSE_INVALID_VALUE;
for (p++; ISDIGIT(*p); p++);
}
//5.检查完代码之后,就可以正常进行转换了,因为此时代码是合法的一定能转化为正确的浮点数故不需要指针接收终止指针
errno = 0;
v->n = strtod(c->json, NULL);
//6.假如最后的数值过大
if (errno == ERANGE && (v->n == HUGE_VAL || v->n == -HUGE_VAL))
return LEPT_PARSE_NUMBER_TOO_BIG;
//7.假如一切正常则返回OK并赋值和类型
v->type = LEPT_NUMBER;
c->json = p;//虽然不知道为什么每次处理完毕都需要将其指向最后,但这应该是指针使用习惯
return LEPT_PARSE_OK;
}
(3)string解析器

处理json值为字符串的JSON文本

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
//解析字符串:只需要先备份栈顶,然后把解析到的字符压栈,最后计算出长度并一次性把所有字符弹出,再设置至值里便可以
static int lept_parse_string(lept_context* c, lept_value* v) {
size_t head = c->top, len;
const char* p;
EXPECT(c, '\"');
p = c->json;
for (;;) {
char ch = *p++;
//1.处理转义序列
switch (ch) {
//1.1 结束字符
case '\"':
len = c->top - head;
lept_set_string(v, (const char*)lept_context_pop(c, len), len);
c->json = p;
return LEPT_PARSE_OK;
//1.2 转义字符
case '\\':
switch (*p++) {
case '\"': PUTC(c, '\"'); break;
case '\\': PUTC(c, '\\'); break;
case '/': PUTC(c, '/' ); break;
case 'b': PUTC(c, '\b'); break;
case 'f': PUTC(c, '\f'); break;
case 'n': PUTC(c, '\n'); break;
case 'r': PUTC(c, '\r'); break;
case 't': PUTC(c, '\t'); break;
default:
c->top = head;
return LEPT_PARSE_INVALID_STRING_ESCAPE;
}
break;
//1.3 字符串末尾,也就是说碰到这个就算整个字符串结束,正常的json字符串是"\"HELLO\"\0",这种情况是"\"HELLO\0"缺少了结束双引号
case '\0':
c->top = head;
return LEPT_PARSE_MISS_QUOTATION_MARK;
//2.处理正常字符
default:
//处理不合法的字符
if ((unsigned char)ch < 0x20) {
c->top = head;
return LEPT_PARSE_INVALID_STRING_CHAR;
}
//将合法字符压栈
PUTC(c, ch);
}
}
}

(4)array解析器
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
static int lept_parse_array(lept_context* c, lept_value* v) {
size_t i, size = 0;
int ret;
EXPECT(c, '[');
lept_parse_whitespace(c);
//1.允许解析空数组[]格式
if (*c->json == ']') {
c->json++;
v->type = LEPT_ARRAY;
v->u.a.size = 0;
v->u.a.e = NULL;
return LEPT_PARSE_OK;
}
//2.假如不是空数组,借助for循环依次解析,c是缓冲区lept_context,e是最终lept_value,我们所说的临时lept_value出现在递归时定义的lept_parse_value
for (;;) {
lept_value e;
lept_init(&e);
//2.1 利用lept_parse_value递归的思想,先解析一小部分的数组值存入临时lept_value中
if ((ret = lept_parse_value(c, &e)) != LEPT_PARSE_OK)
break;//只有当解析数组其中一部分的时候报错才会break退出整个数组的解析
//将递归解析得到的临时lept_value压入永久的lept_value中
memcpy(lept_context_push(c, sizeof(lept_value)), &e, sizeof(lept_value));
/*
void *memcpy(void *destin, void *source, unsigned n);

@destin-- 指向用于存储复制内容的目标数组,类型强制转换为 void* 指针;
@source-- 指向要复制的数据源,类型强制转换为 void* 指针;
@n-- 要被复制的字节数;

该函数返回一个指向目标存储区destin的指针
*/
size++;
lept_parse_whitespace(c);
//2.2 如果遇见,说明接下来还有需要处理的数组值,于是借助for循环继续处理
//注意,以下三种情况必然会出现一种
if (*c->json == ',') {
c->json++;
lept_parse_whitespace(c);
}
//2.3 如果遇到结束]则终止并处理成功
else if (*c->json == ']') {
c->json++;
v->type = LEPT_ARRAY;
v->u.a.size = size;
size *= sizeof(lept_value);
memcpy(v->u.a.e = (lept_value*)malloc(size), lept_context_pop(c, size), size);
return LEPT_PARSE_OK;
}
//2.4 不存在结束]提醒报错(如果是缺少[就不会进入处理array的函数,根据case的规则极大可能进入default处理number接着报错)
else {
ret = LEPT_PARSE_MISS_COMMA_OR_SQUARE_BRACKET;
break;
}
}
//3.数组解析完毕后,进行内存清理工作
/* Pop and free values on the stack */
for (i = 0; i < size; i++)
lept_free((lept_value*)lept_context_pop(c, sizeof(lept_value)));
return ret;
}

(5)object解析器
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
//知道如何lept_parse_array则解析对象几乎是相同的,只是每个迭代需要多处理一个字符串类型的键和冒号
static int lept_parse_object(lept_context* c, lept_value* v) {
size_t i, size;
lept_member m;
int ret;
EXPECT(c, '{');
lept_parse_whitespace(c);
//1.允许空对象也就是{}存在
if (*c->json == '}') {
c->json++;
v->type = LEPT_OBJECT;
v->u.o.m = 0;
v->u.o.size = 0;
return LEPT_PARSE_OK;
}
m.k = NULL;
size = 0;
//2.for循环依次遍历处理{...}对象
for (;;) {
char* str;
lept_init(&m.v);
//2.1 假如空键也就是 ""这是不允许的,抛出异常
/* parse key */
if (*c->json != '"') {
ret = LEPT_PARSE_MISS_KEY;
break;
}
//2.2 第一部分利用重构的lept_parse_string_raw解析键的字符串
if ((ret = lept_parse_string_raw(c, &str, &m.klen)) != LEPT_PARSE_OK)
break;
//若字符串解析成功则将结果存储在临时栈中(因为现在这个对象成员只是对象组成的一部分,很可能还会接着处理其他对象成员)
memcpy(m.k = (char*)malloc(m.klen + 1), str, m.klen);
m.k[m.klen] = '\0';
/* parse ws colon ws */
lept_parse_whitespace(c);
//2.3 第二步是解析冒号,当然冒号的前后可以存在空白字符
if (*c->json != ':') {
ret = LEPT_PARSE_MISS_COLON;//假如根本不存在冒号则抛出异常
break;
}
c->json++;
lept_parse_whitespace(c);
//2.4 第三部分是解析对象成员的值,这部分和数组一样递归调用lept_parse_value
/* parse value */
if ((ret = lept_parse_value(c, &m.v)) != LEPT_PARSE_OK)
break;
//解析成功则将结果写入临时lept_number的value字段,接着将整个临时lept_number压入栈
memcpy(lept_context_push(c, sizeof(lept_member)), &m, sizeof(lept_member));
size++;
//成功解析并且写入键值之后将m.k设置为空指针,将拥有权转移给栈
m.k = NULL; /* ownership is transferred to member on stack */
/* parse ws [comma | right-curly-brace] ws */
lept_parse_whitespace(c);

//2.5 下面三种情况必然出现其一,逗号则接着for循环解析,}表示整个对象解析完毕,否则就是没有}抛出异常
if (*c->json == ',') {
c->json++;
lept_parse_whitespace(c);
}
else if (*c->json == '}') {
size_t s = sizeof(lept_member) * size;
c->json++;
v->type = LEPT_OBJECT;
v->u.o.size = size;
//解析完毕(这里指的是整个对象都被解析完毕),我们将栈上的成员复制到结果中
memcpy(v->u.o.m = (lept_member*)malloc(s), lept_context_pop(c, s), s);
return LEPT_PARSE_OK;
}
else {
ret = LEPT_PARSE_MISS_COMMA_OR_CURLY_BRACKET;
break;
}
}
//for循环中出现任何问题导致break则会进行下面这一步,释放临时的key字符串和栈上的成员
/* Pop and free members on the stack */
free(m.k);
for (i = 0; i < size; i++) {
lept_member* m = (lept_member*)lept_context_pop(c, sizeof(lept_member));
free(m->k);
lept_free(&m->v);
}
v->type = LEPT_NULL;
return ret;
}

3.Stringify部分

3.1 数据结构

3.1.1 缓冲结构

这里使用的缓冲结构与解析器使用的缓冲结构是一样的

1
2
3
4
5
typedef struct {
const char* json;
char* stack;
size_t size, top;
}lept_context;

3.2 生成器

3.2.1 生成器外部接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
char* lept_stringify(const lept_value* v, size_t* length) {
lept_context c;
assert(v != NULL);
//这里我们再次使用了解析器中用过的lept_context缓冲结构体存储临时的生成结果
c.stack = (char*)malloc(c.size = LEPT_PARSE_STRINGIFY_INIT_SIZE);
c.top = 0;
lept_stringify_value(&c, v);
//当传入length参数不为0时,就能获得生成的JSON文本的长度,当然使用strlen()也是可以的,但这将耗费一定性能消耗
if (length)
*length = c.top;
//生成根节点的值之后,还需要手动加入一个空字符作结尾,因为生成的是字符串!
PUTC(&c, '\0');
return c.stack;
}

3.2.2 生成器核心

我们根据节点类型来辨别需要生成的JSON字符串;

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
static void lept_stringify_value(lept_context* c, const lept_value* v) {
size_t i;
switch (v->type) {
//生成"null""false""true"可以直接借助PUTS宏输出对应的字符串即可,无需额外写函数
case LEPT_NULL: PUTS(c, "null", 4); break;
case LEPT_FALSE: PUTS(c, "false", 5); break;
case LEPT_TRUE: PUTS(c, "true", 4); break;
//生成数字文本,首先使用 sprintf("%.17g", ...) 来把浮点数转换成文本,"%.17g" 足够把双精度浮点转换成可还原的文本
//在生成的时候直接写进 c 里的堆栈,然后再按实际长度调用 c->top
case LEPT_NUMBER: c->top -= 32 - sprintf(lept_context_push(c, 32), "%.17g", v->u.n); break;
case LEPT_STRING: lept_stringify_string(c, v->u.s.s, v->u.s.len); break;
case LEPT_ARRAY:
PUTC(c, '[');
for (i = 0; i < v->u.a.size; i++) {
if (i > 0)
PUTC(c, ',');
lept_stringify_value(c, &v->u.a.e[i]);
}
PUTC(c, ']');
break;
case LEPT_OBJECT:
PUTC(c, '{');
for (i = 0; i < v->u.o.size; i++) {
if (i > 0)
PUTC(c, ',');
lept_stringify_string(c, v->u.o.m[i].k, v->u.o.m[i].klen);
PUTC(c, ':');
lept_stringify_value(c, &v->u.o.m[i].v);
}
PUTC(c, '}');
break;
default: assert(0 && "invalid type");
}
}
(1)string生成器
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
static void lept_stringify_string(lept_context* c, const char* s, size_t len) {
static const char hex_digits[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };
size_t i, size;
char* head, *p;
assert(s != NULL);
//预先分配足够的内存
p = head = lept_context_push(c, size = len * 6 + 2); /* "\u00xx..." */
*p++ = '"';
for (i = 0; i < len; i++) {
unsigned char ch = (unsigned char)s[i];
//生成字符串的时候我们首先需要对某些转义进行处理,也就是将转义字符分别拆开逐个存入栈中
switch (ch) {
case '\"': *p++ = '\\'; *p++ = '\"'; break;
case '\\': *p++ = '\\'; *p++ = '\\'; break;
case '\b': *p++ = '\\'; *p++ = 'b'; break;
case '\f': *p++ = '\\'; *p++ = 'f'; break;
case '\n': *p++ = '\\'; *p++ = 'n'; break;
case '\r': *p++ = '\\'; *p++ = 'r'; break;
case '\t': *p++ = '\\'; *p++ = 't'; break;
default:
if (ch < 0x20) {
//自行编写十六进位输出
*p++ = '\\'; *p++ = 'u'; *p++ = '0'; *p++ = '0';
*p++ = hex_digits[ch >> 4];
*p++ = hex_digits[ch & 15];
}
else
*p++ = s[i];
}
}
*p++ = '"';
c->top -= size - (p - head);
}
(2)数组生成器

生成数组只需要输出[],中间部分使用递归调用lept_stringify_value()

1
2
3
4
5
6
7
8
9
10
11
case LEPT_ARRAY:
PUTC(c, '[');
for (i = 0; i < v->u.a.size; i++) {
//注意只在第一个数组元素之后添加,分隔符
if (i > 0)
PUTC(c, ',');
lept_stringify_value(c, &v->u.a.e[i]);
}
PUTC(c, ']');
break;

(3)对象生成器

生成对象主要分为两步,生成键(使用lept_stringify_string()递归调用)以及生成值(使用lept_stringify_value()),在键值之间需要添加:

1
2
3
4
5
6
7
8
9
10
11
case LEPT_OBJECT:
PUTC(c, '{');
for (i = 0; i < v->u.o.size; i++) {
if (i > 0)
PUTC(c, ',');
lept_stringify_string(c, v->u.o.m[i].k, v->u.o.m[i].klen);
PUTC(c, ':');
lept_stringify_value(c, &v->u.o.m[i].v);
}
PUTC(c, '}');
break;

二、附录

1.单元测试

我们日常在写练习题的时候一般都是以printf/cout输出结果观察是否正确,但是随着软件项目越来越复杂这样做会非常低效,我们经常采用的一种方式是单元测试;

常用的单元测试框架有 xUnit 系列,如 C++ 的 Google Test、C# 的 NUnit。简单起见,本项目编写了一个极简单的单元测试方式。

一般来说,软件开发是以周期进行的。例如,加入一个功能,再写关于该功能的单元测试。但也有另一种软件开发方法论,称为测试驱动开发(test-driven development, TDD),它的主要循环步骤是:

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

TDD 是先写测试,再实现功能(单元测试是先加入功能再写测试)。好处是实现只会刚好满足测试,而不会写了一些不需要的代码,或是没有被测试的代码。

但无论我们是采用 TDD,或是先实现后测试,都应尽量加入足够覆盖率的单元测试。

回到 leptjson 项目,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
#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;

#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)

#define EXPECT_EQ_INT(expect, actual) EXPECT_EQ_BASE((expect) == (actual), expect, actual, "%d")

static void test_parse_null() {
lept_value v;
v.type = LEPT_TRUE;
EXPECT_EQ_INT(LEPT_PARSE_OK, lept_parse(&v, "null"));
EXPECT_EQ_INT(LEPT_NULL, lept_get_type(&v));
}

/* ... */

static void test_parse() {
test_parse_null();
/* ... */
}

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

现时只提供了一个 EXPECT_EQ_INT(expect, actual) 的宏,每次使用这个宏时,如果 expect != actual(预期值不等于实际值),便会输出错误信息。
若按照 TDD 的步骤,我们先写一个测试,如上面的 test_parse_null(),而 lept_parse() 只返回 LEPT_PARSE_OK

1
2
/Users/miloyip/github/json-tutorial/tutorial01/test.c:27: expect: 0 actual: 1
1/2 (50.00%) passed

第一个返回 LEPT_PARSE_OK,所以是通过的。第二个测试因为 lept_parse() 没有把 v.type 改成 LEPT_NULL,造成失败。我们再实现 lept_parse() 令到它能通过测试。

然而,完全按照 TDD 的步骤来开发,是会减慢开发进程。所以我个人会在这两种极端的工作方式取平衡。通常会在设计 API 后,先写部分测试代码,再写满足那些测试的实现。


初级项目_JSON库
https://gintoki-jpg.github.io/2022/07/27/项目_JSON库/
作者
杨再俨
发布于
2022年7月27日
许可协议