開源項目cJSON具體實現1(NULL 與 Boolean的解析)

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. FILELINEDATA,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);
       
 /* ... */
    }
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章