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__
typedef enum { LEPT_NULL, LEPT_FALSE, LEPT_TRUE, LEPT_NUMBER, LEPT_STRING, LEPT_ARRAY, LEPT_OBJECT } lept_type;
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 };
int lept_parse(lept_value* v, const char* json);
lept_type lept_get_type(const lept_value* v);
#endif
|
上来先把头文件和API搭建了,主要实现功能如下(配合代码中注释理解),
1 利用宏避免重复声明,这是每个项目的必备元素。
2 利用enum
声明了7个JSON
的基本类型(true
和false
算两个)。
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
| #include "leptjson.h" #include <assert.h> #include <stdlib.h>
#define EXPECT(c, ch) do { assert(*c->json == (ch)); c->json++; } while(0)
typedef struct { const char* json; }lept_context;
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 int lept_parse_null(lept_context* c, lept_value* v) { EXPECT(c, 'n'); 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; }
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_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; }
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; } }
int lept_parse(lept_value* v, const char* json) { lept_context c; assert(v != NULL); c.json = json; v->type = LEPT_NULL; lept_parse_whitespace(&c); int ret; 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; } } return ret; }
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_parse
和lept_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
| #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_FALSE; EXPECT_EQ_INT(LEPT_PARSE_OK, lept_parse(&v, "null")); EXPECT_EQ_INT(LEPT_NULL, lept_get_type(&v)); }
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; 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, "?")); EXPECT_EQ_INT(LEPT_NULL, lept_get_type(&v)); }
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(); }
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文件夹里就能运行文件了,执行命令:
可以得到运行结果:
结果显示这些检测函数得到的全部预期值都和实际值相等,说明主体程序是没有问题的。
写在后面
争取这个项目能在9月完成吧,马上就又恢复线下课了,不知道还能不能抽出这么多时间来自学了。对了,今天是中秋节了,奈何学校依然封控,好久没出去玩了。