1. 实现 NULL 与 Boolean的解析。
文章目录
1.1 JSON的语法规则与解释。
先说说关于 JSON NULL 与 JSON Boolean 的语法:
/*
解释:
当中 %xhh 表示以 16 进制表示的字符,/ 是多选一,* 是零或多个,( ) 用于分组。
第一行的意思是,JSON 文本由 3 部分组成,首先是空白(whitespace),接着是一个值,最后是空白。
第二行的意思是,所谓空白,是由零或多个空格符(space U+0020)、制表符(tab U+0009)、换行符(LF U+000A)、回车符(CR U+000D)所组成。
第三行是说,我们现时的值只可以是 null、false 或 true,它们分别有对应的字面值(literal)。
*/
JSON-text = ws value ws
ws = *(%x20 / %x09 / %x0A / %x0D)
value = null / false / true
null = "null"
false = "false"
true = "true"
1.2 设计头文件
声明一个枚举值 lept_type 表示JSON的类型。
typedef enum { LEPT_NULL, LEPT_FALSE, LEPT_TRUE, LEPT_NUMBER, LEPT_STRING, LEPT_ARRAY, LEPT_OBJECT } lept_type;
JSON 是一个树形结构,设计 节点使用 结构体类型 来表示。 因为现在我们只需要实现 null, true 和 false 的解析,所以目前的 结构体只需要存储一个 JSON类型。
typedef struct {
lept_type type;
}lept_value;
API设计
/*
函数目的:解析JSON ,如果输入的 JSON文本 不合法,要产生对应的错误码,方便使用者追查问题。所以这个函数需要有返回值,并且要根据返回值来判断不同的情况,为此再设计一个枚举类型来表示不同返回值的情况。
参数:1. lept_value* v :传入存储解析后JSON树形结构的根节点指针
2. const char* json :传入的 JSON 文本
返回值:int
*/
int lept_parse(lept_value* v, const char* json);
enum {
LEPT_PARSE_OK = 0, //无错误会返回
//错误码
LEPT_PARSE_EXPECT_VALUE, //JSON 只含有空白
LEPT_PARSE_INVALID_VALUE, //若值不是true、false、null
LEPT_PARSE_ROOT_NOT_SINGULAR //一个值之后,在空白之后还有其他字符
};
/*
函数目的: 获得JSON的类型
参数:1. lept_value* v :存储JSON树状结构的根节点指针
返回值:lept_type 表示 JSON的类型
*/
lept_type lept_get_type(const lept_value* v);
1.3 TDD设计理念
按照TDD的设计思想,我们应该先写测试,再实现功能。
拓展:测试驱动开发(test-driven development, TDD),它的主要循环步骤是:
1.加入一个测试。
2.运行所有测试,新的测试应该会失败。
3.编写实现代码。
4.运行所有测试,若有测试失败回到3。
5.重构代码。
6.回到 1。
首先针对我们今天要实现的函数,来写一个极简的单元测试框架。
#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)
#define EXPECT_EQ_INT(expect, actual) EXPECT_EQ_BASE((expect) == (actual), expect, actual, "%d")
/*
函数目的:1. 测试解析null
步骤:
1. 新建一个lept_value的对象,并初始化
2. 用宏EXPECT_EQ_INT做测试,判断当json文本为null时,lept_parse函数是否能正常解析。
3. 测试当结点保存的是null时,lept_get_type的返回值与LEPT_NULL是否相同。
*/
static void test_parse_null() {
lept_value v;
v.type = LEPT_FALSE;
EXPECT_EQ_INT(LEPT_PARSE_OK, lept_parse(&v, "null"));//测试lept_parse解析null
EXPECT_EQ_INT(LEPT_NULL, lept_get_type(&v));//测试 lept_get_type = LEPT_NULL
}
/* 函数目的:调用测试用例函数 */
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;
}
关于上面的代码的拓展说明:
- fprintf(stderr, "%s:%d: expect: " format " actual: " format “\n”, FILE, LINE, expect, actual); 的意思是:
1. fprintf是C/C++中的一个格式化库函数,位于头文件中,其作用是格式化输出到一个流文件中
2. stderr 标准错误
3. FILE,LINE,DATA,TIME 是C / C++编译器内置宏,这些宏定义可以帮助我们完成跨平台的源码编写,也可以输出有用的调试信息。
3.1 FILE:在源文件中插入当前源文件路径及文件名;
3.2 LINE:在源代码中插入当前源代码行号;- 为什么要用的do {… } while (0)?
如果宏里有多过一个语句(statement),就需要用 do { …} while (0) 包裹成单个语句
1.4 实现解析器
根据 API 与 单元测试 ,我们来实现解析器
#include "leptjson.h"
#include <assert.h> /* assert() */
#include <stdlib.h> /* NULL */
/*为了减少解析函数的参数,把这些数据都放进一个lept_context 结构体中*/
typedef struct {
const char* json;
}lept_context;
/*
函数目的:解析杂乱无章的 JSON 文本
参数:1. lept_value* v :存储JSON树状结构的根节点指针 2. const char* json :传入的 JSON 文本
返回值:int 数值,不同的数值表示了解析过程中的不同情况
实现思路:
1. 首先查看JSON文本的格式:JSON-text = ws value ws。
2. 解析前我们得先处理掉前面的ws,
3. 拿到value后,再根据value 的类型进行对应的处理
4. 处理完后还要处理最后的 ws ,最后的空白处理不能像第一个那么简单就处理了,为什么这么说呢?假设说我们接受到的JSON-text = null x,首先这段text满足了JSON-text,但是在最后有又多余了个x,这样就不是正确的JSON-text格式,所以我们得确定的是我们处理的JSON-text的最后一个空白后面接的是'\0'
*/
int lept_parse(lept_value* v, const char* json) {
assert(v != NULL);
lept_context c;
int ret;
c.json = json;
v->type = LEPT_NULL;//lept_parse() 若失败,会把 v 设为 null 类型,所以这里先把它设为 null,最终让 lept_parse_value() 写入解析出来的类型值。
lept_parse_whitespace(&c);//处理第一个空白
if (LEPT_PARSE_OK == (ret = lept_parse_value(&c,v))) //lept_parse_value是value对应的处理函数
{
lept_parse_whitespace(&c);//处理最后的空白
if ('\0' != *(c.json))
{
ret = LEPT_PARSE_ROOT_NOT_SINGULAR;//若一个值之后,在空白之后还有其他字符,返回错误码
}
}
return ret;
}
lept_type lept_get_type(const lept_value* v) {
assert(v != NULL);
return v->type;
}
根据上面的代码,实现两个函数 lept_parse_whitespace 与 lept_parse_value:
/* 函数目的:处理空白符 */
static void lept_parse_whitespace(lept_context* c) {
const char *p = c->json;
while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')
p++;
c->json = p;
}
/*
函数目的: 对value进行解析
思路:
1. 首先我们得先判断出这个value是做什么类型,如何判断?根据第一个字符就可以,如果是n,就代表是null;t ➔ true;f ➔ false;" ➔ string等等
2. 对不同的类型进行不同的处理
*/
static int lept_parse_value(lept_context* c, lept_value* v) {
switch (*c->json) {
case 'n': return lept_parse_null(c, v);
case '\0': return LEPT_PARSE_EXPECT_VALUE;
default: return LEPT_PARSE_INVALID_VALUE;
}
}
根据上面的代码,实现lept_parse_null
#define EXPECT(c, ch) do { assert(*c->json == (ch)); c->json++; } while(0)
/*
函数目的:null的解析函数
思路:
1. 进一步判断传入的文本是不是null
2. 是,设置接收数据结点的类型为LEPT_NULL,并返回LEPT_PARSE_OK
3. 不是,返回错误码
4. 记得 将传入的文本向后移动4位,其实应该是判断一个字符就移动一个字符,方便后续处理。
*/
static int lept_parse_null(lept_context* c, lept_value* v) {
EXPECT(c, 'n'); //及得这里有一个json++
if (c->json[0] != 'u' || c->json[1] != 'l' || c->json[2] != 'l')
return LEPT_PARSE_INVALID_VALUE;
c->json += 3;
v->type = LEPT_NULL;
return LEPT_PARSE_OK;
}
1.5 照猫画虎–上面实现了null的情况,接下来实现tree与false
首先同样是先写测试函数
/*
函数目的:测试解析true
测试思路同 test_parse_null
*/
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));
}
//测试 false
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));
}
/*
函数目的:测试JSON只含有空白字符
*/
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));
}
/* 函数目的:测试JSON文本 不是三种字面值(null,false,true)的情况 */
static void test_parse_invalid_value() {
lept_value v;
v.type = LEPT_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, "fal"));
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));
}
/* 函数目的:测试JSON文本在最后空白之后还有其他字符 */
static void test_parse_root_not_singular() {
lept_value v;
v.type = LEPT_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();
test_parse_true();
test_parse_false();
test_parse_expect_value();
test_parse_invalid_value();
test_parse_root_not_singular();
}
实现true 与 false 的解析器
/*若value = 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 = 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;
}
static int lept_parse_value(lept_context* c, lept_value* v) {
switch (*c->json) {
case 'n': return lept_parse_null(c, v);//null
case 'f': return lept_parse_false(c, v);//false
case 't': return lept_parse_true(c, v);//true
case '\0': return LEPT_PARSE_EXPECT_VALUE;//空白
default: return LEPT_PARSE_INVALID_VALUE;//若值不是那三种字面值
}
}
1.6 思考
其实很明显就可以看出,上面的代码有些问题: 重复的代码太多,对付重复的代码太多。就要用到重构。重构是一个这样的过程:在不改变代码外在行为的情况下,对代码作出修改,以改进程序的内部结构。
在 TDD 的过程中,我们的目标是编写代码去通过测试。但由于这个目标的引导性太强,我们可能会忽略正确性以外的软件品质。在通过测试之后,代码的正确性得以保证,我们就应该审视现时的代码,看看有没有地方可以改进,而同时能维持测试顺利通过。我们可以安心地做各种修改,因为我们有单元测试,可以判断代码在修改后是否影响原来的行为。
怎么处理呢?可以宏简化,比如我们可以将测试函数中的test_parse_null() 这样写:
#define TEST_ERROR(error, json, lept_type)\
do{\
lept_value v; \
v.type = LEPT_FALSE; \
EXPECT_EQ_INT(error, lept_parse(&v, json)); \
EXPECT_EQ_INT(lept_type, lept_get_type(&v)); \
} while (0);
static void test_parse_null() {
TEST_ERROR(LEPT_PARSE_OK, "null", LEPT_NULL);
}
再比如,我们在解析器中对于true、false、null的解析代码十分相似,所以我们可以把他们合到一个函数中:
//true、false、null的解析代码
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;
}
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);
/* ... */
}
}