lept_json库的学习2
上一篇讲了JSON库主要有Parse和Stringify两个功能,我先来讲讲Parse,因为叶老师说Parse搞定了,Stringify很简单。
Parse()函数架构
跳过空白函数()
Parse_Value函数()
跳过空白函数()
举个例子
"
{object}
"
我实际上是怎么敲的呢?把我的操作换成转义字符:
那么整个parse函数就只需要三步:
- 先parse_whitespase,
- 再parse_value
- 最后再parse_whitespace.
很简单很清晰的架构。
那么怎么parse_whitespace呢?
只要跳过’\n’ ‘\t’ ‘\r’ 和空格’ '这些字符就可以了
以下是parse_whitespace的代码:
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;
}
这里之所以用static,是因为这个是json库的内部函数,它并不是对外的接口,也就是说,我并不打算把这个函数给使用json库的人用,也不希望当客户在使用json库的时候,发现重名之类的影响,所以把这个函数定义为static,使这个函数只会在这个文件内(json库的文件)有用。
相对应的,我们需要给用户用到的函数,也就是对外接口函数,就不需要这个static了。
这也是很简单的一些机理,我就简单的提一下。
context
可以看到parse_whitespace函数的参数是lept_context
这里lept_context其实就是一种存储字符串的数据类型,只不过在之后的程序编写中,单纯的字符串是无法满足解析和生成要求的,特别是当触及到内存分配管理之类的,会需要我们引入堆栈。
但是现在,我们定义的context就是一个简单的const char * 用来记录输入给解析函数(简称解析器)的文本信息。
typedef struct {
const char* json;//目前只需要一个const char *字符串
/*
* char* stack;//添加一个动态堆栈
* size_t size, top;//size 堆栈容量,top 栈顶位置
*/
}lept_context;
Parse_Value()函数架构
/*....*/
switch (首字符) {
case 'n': return Null/Bool类型Parse函数;//null,n开头
case 't': return Null/Bool类型Parse函数;//true,t开头
case 'f': return Null/Bool类型Parse函数;//false,f开头
default: return Number类型Parse函数; //因为number类型没有标志性首字符
//所以放在default
case '"': return String类型Parse函数; //字符串,"开头
case '[': return Array 类型Parse函数; //数组,[开头
case '{': return Object类型Parse函数; k//对象,{开头
case '\0': return 解析状态为 什么也没读取到
}
/*....*/
Null_or_Bool_Parse Function
我们先来讲Null/Bool的Parse Function
这里可以看到,我们把Null和Bool的解析函数归为了一个。
我们先来思考一下Null的解析应该使什么样的。
那么就得出null的解析函数如下:
static int lept_parse_null(lept_context& c, lept_value& v) {
assert(*c.json == (ch));//确认第一个字符为n
c.json++; //读取下一个字符
if (c.json[0] != 'u' || c.json[1] != 'l' || c.json[2] != 'l')
//判断之后的三个字符是否分别是u l l
return LEPT_PARSE_INVALID_VALUE;
//如果不是返回错误类型INVALID_VALUE,无效字符
//如果是null没错
c.json += 3; //跳过这三个字符
v.type = LEPT_NULL; //将value_type置为Null类型
return LEPT_PARSE_OK; //返回解析成功parse_ok
}
为了方便起见,我们把头两行的断言assert和读取下一字符操作写到一个宏里面(因为之后其它类型都会用到)
#define EXPECT(c,ch) do{\
assert(*c.json == (ch));\
c.json++;\
}while(0)
这里面提到了value_type和parse状态,我直接把定义贴出来,不多赘述。
/*value_type*/
typedef enum {
LEPT_NULL=0,
LEPT_FALSE,//BOOL
LEPT_TRUE, //BOOL
LEPT_NUMBER,
LEPT_STRING,
LEPT_ARRAY,
LEPT_OBJECT
}lept_type;
/*parse状态*/
enum {
LEPT_PARSE_OK=0,
LEPT_PARSE_EXPECT_VALUE,
LEPT_PARSE_INVALID_VALUE,
LEPT_PARSE_ROOT_NOT_SINGULAR,
//暂时只需要上面四个
/*
LEPT_PARSE_NUMBER_TOO_BIG,
LEPT_PARSE_MISS_QUOTATION_MARK,
LEPT_PARSE_INVALID_STRING_ESCAPE,
LEPT_PARSE_INVALID_STRING_CHAR,
LEPT_PARSE_INVALID_UNICODE_HEX,
LEPT_PARSE_INVALID_UNICODE_SURROGATE,
LEPT_PARSE_MISS_COMMA_OR_SQUARE_BRACKET,
LEPT_PARSE_MISS_KEY,
LEPT_PARSE_MISS_COLON,
LEPT_PARSE_MISS_COMMA_OR_CURLY_BRACKET
*/
};
我解释一下为什么Bool不定义成一个类型,因为Bool一共就两个类型,而且两个类型是完全不一样的,true和false一个天一个地,一个1一个0,一个to be,一个not to be。
两者没有任何兼容,所以直接拆成两个类型存储方便我们编写程序以及外部使用和阅读。
那么照着null的解析,true和false的解析也可以很容易的写出来,
但是这样我们会写到重复的代码,为了从小贯彻提高代码复用性,减少代码重复感,我们可以写一个适用于null true false 三个类型的通用函数
static int lept_parse_literal(
lept_context& c, //输入解析器的字符串
lept_value& v, //解析后存入的数据结构
const char* literal,//将会读取的字符(expect)
lept_type type) //将要存储的value_type
{
size_t i;
EXPECT(c, literal[0]); //判断首字母是否对应,进行了json++
for (i = 0; literal[i + 1]; i++) { // i+1<strlen(literal)-1
if (c.json[i] != literal[i + 1]) //如果输入字符与目标字符不对应
return LEPT_PARSE_INVALID_VALUE;//解析状态为 无效字符
}
c.json += i;
v.type = type; //将value类型置为需要的type
return LEPT_PARSE_OK; //一切正常返回parse_ok
}
这段代码中for循环里的literal[i+1]这个限制条件非常有意思,非常建议读者自己玩玩,反正我是玩了半天才玩明白。
然后就可以去Parse_value()函数里完善Null/Bool类型解析函数啦!
/*....*/
switch(首字符){
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);
/*....*/
}
/*....*/
这里用两个图来表现一下parse(),parse_value(),parse_whitespace(),parse_literal()之间的关系
大致如此
单元测试
写完了null/bool的解析,我们就需要单元测试
- 首先是正确测试
在测试文件中,用
test_parse_null()
test_parse_true()
test_parse_false()
三个函数来测试。
static void test_parse_null() {
lept_value v; //声明一个value
lept_init(v); //初始化type为null
v.type = LEPT_FALSE; //因为这里要判断parse是否将null的类型设置为null了
//所以一开始先将v的type置为false
EXPECT_EQ_INT(LEPT_PARSE_OK, lept_parse(v, "null"));//解析是否成功
EXPECT_EQ_INT(LEPT_NULL, lept_get_type(v)); //解析后是否修改了type
lept_free(v);
}
static void test_parse_true() {
lept_value v;
lept_init(v);
v.type = LEPT_FALSE;
EXPECT_EQ_INT(LEPT_PARSE_OK, lept_parse(v, "true"));
EXPECT_EQ_INT(LEPT_TRUE, lept_get_type(v));
lept_free(v);
}
static void test_parse_false() {
lept_value v;
lept_init(v);
v.type = LEPT_TRUE;
EXPECT_EQ_INT(LEPT_PARSE_OK, lept_parse(v, "false"));
EXPECT_EQ_INT(LEPT_FALSE, lept_get_type(v));
lept_free(v);
}
这里面lept_init函数是将value_type初始化为Null类型
(虽然是用宏写的,不过效果和函数一样)
也可以称之为Init_type函数,代码如下:
#define lept_init(v) do { v.type = LEPT_NULL; } while(0)
lept_free函数是释放v的内存的,但现在来讲,NULL/BOOL并没有实际存储任何数据,而只是存储了一个value_type,所以lept_free函数并没有太大作用,目前它只是又将
value_type又置回了null而已。不过代码还是要贴出来的:
void lept_free (lept_value & v) {
assert(&v != NULL);
/*size_t i;
switch (v.type) {
case LEPT_STRING:
free(v.u.m_str.s);
break;
case LEPT_ARRAY:
for (i = 0; i < v.u.m_arr.size; i++)
lept_free(v.u.m_arr.e[i]);
free(v.u.m_arr.e);
break;
case LEPT_OBJECT:
for (i = 0; i < v.u.m_obj.size; i++) {
free(v.u.m_obj.m[i].k);
lept_free(v.u.m_obj.m[i].v);
}
free(v.u.m_obj.m);
break;
default: break;
}*/
v.type = LEPT_NULL;//目前如此
}
这样说,看上去像是在做重复的无意义的工作。
但其实在我们从lept_parse_null、lept_parse_true、lept_parse_false三个函数
改进到lept_parse_literal一个函数的时候,这个单元测试就负责测试我们新编写的函数是否能够达到原来的效果。
单元测试的作用在越大型的项目中,越能体现其重要性。
它表现的是一种规章制度,一种更合理的开发模式。(这些都是个人理解)
- 其次是错误测试
我们先写一个错误比对的通用宏
#define TEST_ERROR(error, json)\
do {\
lept_value v;\
lept_init(v);\
v.type = LEPT_FALSE;\
EXPECT_EQ_INT(error, lept_parse(v, json));\
EXPECT_EQ_INT(LEPT_NULL, lept_get_type(v));\
lept_free(v);\
} while(0)
而后是两种类型的错误示例(其实这里的测试主要是用来教学 单元测试而写的,实际上
static void test_parse_expect_value() {
TEST_ERROR(LEPT_PARSE_EXPECT_VALUE, "");
TEST_ERROR(LEPT_PARSE_EXPECT_VALUE, " ");
}//啥也没读取到就结束了,所以我想要一个值,expect value,这样理解
static void test_parse_invalid_value() {
TEST_ERROR(LEPT_PARSE_INVALID_VALUE, "nul");
TEST_ERROR(LEPT_PARSE_INVALID_VALUE, "?")//非法输入(无效值)
到这里null/bool类型的解析和单元测试就结束了,后面是个人的一些话。
感谢
看到这里的你,毕竟我觉得没人会看到这。
一些我个人的思考。
首先是,null、Null、NULL它的拼写可能有多种,true、false也是,那么我在设计解析器时,是否需要改进呢?
我个人认为,这个是可以改进的,只要在value_parse那改成:
case 'N':return lept_parse_literal(c,v,"Null",LEPT_NULL);
case 'n':return lept_parse_literal(c,v,"null",LEPT_NULL);
改进是肯定可以改进的,但是那是不是还要考虑输入NuLl、nUlL这种呢?你肯定觉得这种离谱的我就不会输入了。
那么我作为解析器,我觉得只有null靠谱,只有null是null类型,
你不要给我输入其它的字符来骗我这是null,其它的我一概不认。
我们写一个json库应该追求的是简洁、轻量级、高效,所以在这种细枝末节上,我们只要规定说,我们这个Json库,它只认null为null类型,
在生成的时候,也只会生成null为null类型,就可以了。使用这个json库的人,就按照这个规定使用。就可以了。
这是我个人的看法。
lept_json Github:https://github.com/miloyip/json-tutorial