C의 간단한 JSON 파서

여기에 검토 된 확장 가능한 벡터 클래스를 사용하지만 많은 추가 기능이 아닌 간단한 재귀 하강 JSON 파서가 있습니다 ( C의 단순 확장형 벡터 ). 나는 어떤 최적화도 구현하지 않았고 더 높은 수준의 액세스 인터페이스도 구현하지 않았습니다. 이것은 모두 매우 기본적인 것입니다. 또한 JSON 내보내기가 아니라 가져 오기만하면됩니다.

전체 소스는 github ( https://github.com/HarryDC/JsonParser )에서 사용할 수 있습니다. CMake 및 테스트 코드가 포함되어 있으므로 여기에 파서 파일을 게시하겠습니다.

저의 주된 관심은 C로 작성하는 관용적 인 방법이 더 있으면 좋을 것입니다. 물론 다른 입력도 항상 감사합니다.

헤더

#ifndef HS_JSON_H #define HS_JSON_H #include "vector.h" enum json_value_type { TYPE_NULL, TYPE_BOOL, TYPE_NUMBER, TYPE_OBJECT, // Is a vector with pairwise entries, key, value TYPE_ARRAY, // Is a vector, all entries are plain TYPE_STRING, TYPE_KEY }; typedef struct { int type; union { int boolean; double number; char* string; char* key; vector array; vector object; } value; } json_value; // Parse string into structure of json elements and values // return 1 if successful. int json_parse(const char* input, json_value* root); // Free the structure and all the allocated values void json_free_value(json_value* val); // Convert value to string if possible, asserts if not char* json_value_to_string(json_value* value); // Convert value to double if possible asserts if not double json_value_to_double(json_value* value); // Convert value to bool if possible asserts if not int json_value_to_bool(json_value* value); // Convert value to vector if it"s an array asserts if not vector* json_value_to_array(json_value* value); // Convert value to vector if it"s an object, asserts if not vector* json_value_to_object(json_value* value); // Fetch the value with given index from root, asserts if root is not array json_value* json_value_at(const json_value* root, size_t index); // Fetche the value with the given key from root, asserts if root is not object json_value * json_value_with_key(const json_value * root, const char * key); #ifdef BUILD_TEST void json_test_all(void); #endif #endif 

구현

#include "json.h" #include <assert.h> #include <ctype.h> #include <stddef.h> #include <stdlib.h> #include <string.h> static int json_parse_value(const char ** cursor, json_value * parent); static void skip_whitespace(const char** cursor) { if (**cursor == "\0") return; while (iscntrl(**cursor) || isspace(**cursor)) ++(*cursor); } static int has_char(const char** cursor, char character) { skip_whitespace(cursor); int success = **cursor == character; if (success) ++(*cursor); return success; } static int json_parse_object(const char** cursor, json_value* parent) { json_value result = { .type = TYPE_OBJECT }; vector_init(&result.value.object, sizeof(json_value)); int success = 1; while (success && !has_char(cursor, "}")) { json_value key = { .type = TYPE_NULL }; json_value value = { .type = TYPE_NULL }; success = json_parse_value(cursor, &key); success = success && has_char(cursor, ":"); success = success && json_parse_value(cursor, &value); if (success) { vector_push_back(&result.value.object, &key); vector_push_back(&result.value.object, &value); } else { json_free_value(&key); break; } skip_whitespace(cursor); if (has_char(cursor, "}")) break; else if (has_char(cursor, ",")) continue; else success = 0; } if (success) { *parent = result; } else { json_free_value(&result); } return success; return 1; } static int json_parse_array(const char** cursor, json_value* parent) { int success = 1; if (**cursor == "]") { ++(*cursor); return success; } while (success) { json_value new_value = { .type = TYPE_NULL }; success = json_parse_value(cursor, &new_value); if (!success) break; skip_whitespace(cursor); vector_push_back(&parent->value.array, &new_value); skip_whitespace(cursor); if (has_char(cursor, "]")) break; else if (has_char(cursor, ",")) continue; else success = 0; } return success; } void json_free_value(json_value* val) { if (!val) return; switch (val->type) { case TYPE_STRING: free(val->value.string); val->value.string = NULL; break; case TYPE_ARRAY: case TYPE_OBJECT: vector_foreach(&(val->value.array), (void(*)(void*))json_free_value); vector_free(&(val->value.array)); break; } val->type = TYPE_NULL; } int json_is_literal(const char** cursor, const char* literal) { size_t cnt = strlen(literal); if (strncmp(*cursor, literal, cnt) == 0) { *cursor += cnt; return 1; } return 0; } static int json_parse_value(const char** cursor, json_value* parent) { // Eat whitespace int success = 0; skip_whitespace(cursor); switch (**cursor) { case "\0": // If parse_value is called with the cursor at the end of the string // that"s a failure success = 0; break; case """: ++*cursor; const char* start = *cursor; char* end = strchr(*cursor, """); if (end) { size_t len = end - start; char* new_string = malloc((len + 1) * sizeof(char)); memcpy(new_string, start, len); new_string[len] = "\0"; assert(len == strlen(new_string)); parent->type = TYPE_STRING; parent->value.string = new_string; *cursor = end + 1; success = 1; } break; case "{": ++(*cursor); skip_whitespace(cursor); success = json_parse_object(cursor, parent); break; case "[": parent->type = TYPE_ARRAY; vector_init(&parent->value.array, sizeof(json_value)); ++(*cursor); skip_whitespace(cursor); success = json_parse_array(cursor, parent); if (!success) { vector_free(&parent->value.array); } break; case "t": { success = json_is_literal(cursor, "true"); if (success) { parent->type = TYPE_BOOL; parent->value.boolean = 1; } break; } case "f": { success = json_is_literal(cursor, "false"); if (success) { parent->type = TYPE_BOOL; parent->value.boolean = 0; } break; } case "n": success = json_is_literal(cursor, "null"); break; default: { char* end; double number = strtod(*cursor, &end); if (*cursor != end) { parent->type = TYPE_NUMBER; parent->value.number = number; *cursor = end; success = 1; } } } return success; } int json_parse(const char* input, json_value* result) { return json_parse_value(&input, result); } char* json_value_to_string(json_value* value) { assert(value->type == TYPE_STRING); return (char *)value->value.string; } double json_value_to_double(json_value* value) { assert(value->type == TYPE_NUMBER); return value->value.number; } int json_value_to_bool(json_value* value) { assert(value->type == TYPE_BOOL); return value->value.boolean; } vector* json_value_to_array(json_value* value) { assert(value->type == TYPE_ARRAY); return &value->value.array; } vector* json_value_to_object(json_value* value) { assert(value->type == TYPE_OBJECT); return &value->value.object; } json_value* json_value_at(const json_value* root, size_t index) { assert(root->type == TYPE_ARRAY); if (root->value.array.size < index) { return vector_get_checked(&root->value.array,index); } else { return NULL; } } json_value* json_value_with_key(const json_value* root, const char* key) { assert(root->type == TYPE_OBJECT); json_value* data = (json_value*)root->value.object.data; size_t size = root->value.object.size; for (size_t i = 0; i < size; i += 2) { if (strcmp(data[i].value.string, key) == 0) { return &data[i + 1]; } } return NULL; } #ifdef BUILD_TEST #include <stdio.h> void json_test_value_string(void) { printf("json_parse_value_string: "); // Normal parse, skip whitespace const char* string = " \n\t\"Hello World!\""; json_value result = { .type = TYPE_NULL }; assert(json_parse_value(&string, &result)); assert(result.type == TYPE_STRING); assert(result.value.string != NULL); assert(strlen(result.value.string) == 12); assert(strcmp("Hello World!", result.value.string) == 0); json_free_value(&result); // Empty string string = "\"\""; json_parse_value(&string, &result); assert(result.type == TYPE_STRING); assert(result.value.string != NULL); assert(strlen(result.value.string) == 0); json_free_value(&result); printf(" OK\n"); } void json_test_value_number(void) { printf("json_test_value_number: "); const char* string = " 23.4"; json_value result = { .type = TYPE_NULL }; assert(json_parse_value(&string, &result)); assert(result.type == TYPE_NUMBER); assert(result.value.number == 23.4); json_free_value(&result); printf(" OK\n"); } void json_test_value_invalid(void) { printf("json_test_value_invalid: "); { // not a valid value const char* string = "xxx"; json_value result = { .type = TYPE_NULL }; assert(!json_parse_value(&string, &result)); assert(result.type == TYPE_NULL); json_free_value(&result); } { // parse_value at end should fail const char* string = ""; json_value result = { .type = TYPE_NULL }; assert(!json_parse_value(&string, &result)); assert(result.type == TYPE_NULL); json_free_value(&result); } printf(" OK\n"); } void json_test_value_array(void) { printf("json_test_value_array: "); { // Empty Array const char* string = "[]"; json_value result = { .type = TYPE_NULL }; assert(result.value.array.data == NULL); assert(json_parse_value(&string, &result)); assert(result.type = TYPE_ARRAY); assert(result.value.array.data != NULL); assert(result.value.array.size == 0); json_free_value(&result); } { // One Element const char* string = "[\"Hello World\"]"; json_value result = { .type = TYPE_NULL }; assert(result.value.array.data == NULL); assert(json_parse_value(&string, &result)); assert(result.type = TYPE_ARRAY); assert(result.value.array.data != NULL); assert(result.value.array.size == 1); json_value* string_value = (json_value *)result.value.array.data; assert(string_value->type == TYPE_STRING); assert(strcmp("Hello World", string_value->value.string) == 0);; json_free_value(&result); } { // Mutliple Elements const char* string = "[0, 1, 2, 3]"; json_value result = { .type = TYPE_NULL }; assert(result.value.array.data == NULL); assert(json_parse_value(&string, &result)); assert(result.type = TYPE_ARRAY); assert(result.value.array.data != NULL); assert(result.value.array.size == 4); json_free_value(&result); } { // Failure const char* string = "[0, 2,,]"; json_value result = { .type = TYPE_NULL }; assert(result.value.array.data == NULL); assert(result.type == TYPE_NULL); assert(result.value.array.data == NULL); json_free_value(&result); } { // Failure // Shouldn"t need to free, valgrind shouldn"t show leak const char* string = "[0, 2, 0"; json_value result = { .type = TYPE_NULL }; assert(result.value.array.data == NULL); assert(result.type == TYPE_NULL); assert(result.value.array.data == NULL); } printf(" OK\n"); } void json_test_value_object(void) { printf("json_test_value_object: "); { // Empty Object const char* string = "{}"; json_value result = { .type = TYPE_NULL }; assert(result.value.object.data == NULL); assert(json_parse_value(&string, &result)); assert(result.type = TYPE_OBJECT); assert(result.value.array.data != NULL); assert(result.value.array.size == 0); json_free_value(&result); } { // One Pair const char* string = "{ \"a\" : 1 }"; json_value result = { .type = TYPE_NULL }; assert(result.value.object.data == NULL); assert(json_parse_value(&string, &result)); assert(result.type = TYPE_OBJECT); assert(result.value.array.data != NULL); assert(result.value.array.size == 2); json_value* members = (json_value *)result.value.object.data; assert(strcmp(json_value_to_string(members), "a") == 0); ++members; assert(json_value_to_double(members) == 1.0); json_free_value(&result); } { // Multiple Pairs const char* string = "{ \"a\": 1, \"b\" : 2, \"c\" : 3 }"; json_value result = { .type = TYPE_NULL }; assert(result.value.object.data == NULL); assert(json_parse_value(&string, &result)); assert(result.type = TYPE_OBJECT); assert(result.value.array.data != NULL); assert(result.value.array.size == 6); json_value* members = (json_value *)result.value.object.data; assert(strcmp(json_value_to_string(&members[4]), "c") == 0); assert(json_value_to_double(&members[5]) == 3.0); json_free_value(&result); } printf(" OK\n"); } void json_test_value_literal(void) { printf("json_test_values_literal: "); { const char* string = "true"; json_value result = { .type = TYPE_NULL }; assert(json_parse_value(&string, &result)); assert(result.type == TYPE_BOOL); assert(result.value.boolean); json_free_value(&result); } { const char* string = "false"; json_value result = { .type = TYPE_NULL }; assert(json_parse_value(&string, &result)); assert(result.type == TYPE_BOOL); assert(!result.value.boolean); json_free_value(&result); } { const char* string = "null"; json_value result = { .type = TYPE_NULL }; assert(json_parse_value(&string, &result)); assert(result.type == TYPE_NULL); json_free_value(&result); } printf(" OK\n"); } const char* test_string_valid = " \ { \"item1\" : [1, 2, 3, 4], \ \"item2\" : { \"a\" : 1, \"b\" : 2, \"c\" : 3 }, \ \"item3\" : \"An Item\" \ }"; const char* test_string_invalid = " \ { \"item1\" : [1, 2, 3, 4], \ \"item2\" : { \"a\" : 1, \"b\" : 2, \"c\" : 3 }, \ \"item3\" , \"An Item\" \ }"; void json_test_coarse(void) { printf("json_test_coarse: "); json_value root; assert(json_parse(test_string_valid, &root)); json_value* val = json_value_with_key(&root, "item1"); assert(root.type == TYPE_OBJECT); assert(root.value.object.size == 6); assert(val != NULL); assert(json_value_to_array(val) != NULL); assert(json_value_to_array(val)->size == 4); val = json_value_with_key(&root, "item3"); assert(val != NULL); assert(json_value_to_string(val) != NULL); assert(strcmp(json_value_to_string(val), "An Item") == 0); json_free_value(&root); // valgrind check for releasing intermediary data assert(!json_parse(test_string_invalid, &root)); printf(" OK\n"); } void json_test_all(void) { json_test_value_invalid(); json_test_value_string(); json_test_value_number(); json_test_value_array(); json_test_value_object(); json_test_value_literal(); json_test_coarse(); } #endif 

Answer

Header

C에서 모든 enum 이름은 서로 동일한 네임 스페이스를 공유합니다 ( 변수 이름과 같은 것). 따라서 “충돌 할 위험을 줄이려고 노력하는 것이 좋습니다.

enum json_value_type 이름에는 접두사 는 매우 일반적입니다. 다른 라이브러리는 같은 이름을 사용하려고 할 수 있습니다. 해당 접두어를 JSON_로 변경하는 것이 좋습니다.

또한 TYPE_KEY. 그냥 제거하세요.

구현

Roland Illig가 언급했듯이 , iv id = “8ebd65d75a”에 대한 주장 skip_whitespace() 함수의>

isspace()unsigned char로 캐스트되어야합니다. 부호 확장을 피하십시오.

또는 JSON 사양 을 더 자세히 따르면이 함수를 다음과 같이 간단히 다시 작성할 수 있습니다.

static void skip_whitespace(const char** cursor) { while (**cursor == "\t" || **cursor == "\r" || **cursor == "\n" || **cursor == " ") ++(*cursor); } 

대부분의 static 도우미 함수는 사소한 조합을 수행하며 그 내용을 설명하는 주석이 없습니다. 하다. 각 함수 앞에 한두 줄의 주석 줄은 가독성을 크게 높일 수 있습니다.

특히 has_char() 함수는 다음과 같은 여러 가지 작업을 수행합니다.

  1. 공백을 건너 뜁니다.
  2. 입력에 특정 문자가 있는지 확인합니다.
  3. 문자가 발견되면 자동으로 건너 뜁니다.

함수 이름은 # 2 만 분명히 의미합니다. 나머지는 예상치 못한 부작용이며 최소한 명확하게 문서화해야합니다.

사실 skip_whitespace()에 대한 호출을 제거하는 것이 더 나을 것 같습니다. has_char()에서 호출하고 필요한 경우 호출하기 전에 호출자가 공백을 명시 적으로 건너 뛰도록합니다. 많은 경우에 여러분의 코드는 이미 그렇게하여 중복 건너 뛰기를 중복되게 만듭니다.

또한 효과 # 3을 독자에게 덜 놀라게하려면 해당 함수의 이름을 조금 더 변경하는 것이 좋습니다. 예를 들어 read_char()와 같이 활성 상태입니다.


json_parse_object() 끝에 다음이 있습니다.

 return success; return 1; } 

물론 중복됩니다. return 1;를 제거하세요.

또한, “일반 json_parse_value() 함수를 사용하여 개체 키를 구문 분석하고 문자열인지 확인하기 위해 테스트하지 않는 것 같습니다. 이렇게하면 잘못된 JSON이 파서를 통과 할 수 있습니다. 명시적인 유형 검사를 추가하거나 문자열 구문 분석 코드를 별도의 함수 (아래 설명 참조)로 분할하고 json_parse_object()에서 직접 호출하는 것이 좋습니다.


json_parse_array() 상단에 다음이 있습니다.

if (**cursor == "]") { ++(*cursor); return success; } while (success) { 

다음과 같은 방식으로 다시 작성할 수 있습니다. json_parse_object()에서 수행 :

while (success && !has_char("]")) { 

(오직 알다시피, 저는 여전히 이름이 read_char()가 더 낫습니다.)

또한 어떤 이유로 든 json_parse_array()는 호출자가

struct,json_parse_object()는 자동으로 수행합니다. AFAICT에는 불일치에 대한 이유가 없으므로 두 함수가 모두 작동하도록 할 수 있으며 같은 방식입니다.


json_is_literal() 함수는 static로 표시되지 않습니다. 헤더에 표시됩니다. is_char()처럼 I “d al 따라서 json_read_literal() 또는 read_literal()와 같이 좀 더 활동적인 이름으로 이름을 바꾸는 것이 좋습니다. 성공적으로 일치합니다.

(또한이 함수는 작성된대로 입력의 리터럴이 실제로 예상되는 위치에서 끝나는 지 확인하지 않습니다 . 예를 들어 입력 nullnullnullnull와 성공적으로 일치시킵니다.JSON의 유효한 리터럴은 true, falsenull, 어느 것도 서로의 접두사가 아니며 두 개의 리터럴은 사이에 다른 토큰이 없으면 유효한 JSON에 연속적으로 나타날 수 없습니다. 그러나 적어도 주석에서 주목할 가치가 있습니다.)


정적 도우미 함수 중 일부를 inline로 명시 적으로 표시 할 수도 있습니다. 컴파일러에게 호출 코드에 병합을 시도해야한다는 힌트를 제공합니다. 적어도 skip_whitespace(), has_char()json_is_literal().

json_value_to_X() 접근 자 함수는 모두 및 포인터 역 참조가있는 경우 해당 구현을 json.h로 이동하고 static inline로 표시하는 것도 고려해야합니다. 이렇게하면 컴파일러가 다른 .c 파일에서도 호출 코드로 인라인 할 수 있으며 호출이 다음과 같은 경우 assert()를 최적화 할 수 있습니다. 어쨌든 코드는 이미 유형을 확인합니다.


json_parse() 함수에서 공백 만 남았는지 명시 적으로 확인할 수 있습니다. 루트 값이 파싱 된 후 입력합니다.

문자열 파싱

json_parse_value()의 문자열 파싱 코드가 손상되었습니다. 백 슬래시 이스케이프를 처리합니다. 예를 들어 다음과 같은 유효한 JSON 입력에서 실패합니다.

"I say: \"Hello, World!\"" 

테스트 케이스로 추가 할 수 있습니다.

당신 또한 코드가 \b, \f, \n, \r, \t, \/, 특히 \\\unnnn. 이에 대한 몇 가지 추가 테스트 사례는 다음과 같습니다.

"\"\b\f\n\r\t\/\\" "void main(void) {\r\n\tprintf(\"I say: \\\"Hello, World!\\\"\\n\");\r\n}" "\u0048\u0065\u006C\u006C\u006F\u002C\u0020\u0057\u006F\u0072\u006C\u0064\u0021" "\u3053\u3093\u306B\u3061\u306F\u4E16\u754C" 

JSON 문자열에는 임의의 유니 코드 문자가 포함될 수 있으므로 처리 방법을 결정해야합니다. 아마도 가장 간단한 선택은 입력 및 출력을 UTF-8 (또는 WTF-8 )로 선언하고 는 UTF-8 바이트 시퀀스로 이스케이프합니다 (선택적으로 그 반대도 가능). null로 끝나는 문자열을 사용하고 있으므로 \u0000 오버 롱 인코딩 "\xC0\x80"


json_parse_value() 함수를 읽을 수 있도록 유지하기 위해 문자열 구문 분석 코드를 별도의 도우미 함수로 분리하는 것이 좋습니다. 특히 백 슬래시 이스케이프를 올바르게 처리하면 상당히 복잡해집니다.

복잡한 점 중 하나는 실제로 얼마나 오래 걸리는지 알 수 없다는 것입니다. 문자열은 구문 분석 할 때까지입니다.이를 처리하는 한 가지 방법은 할당 된 출력 문자열을 realloc()로 동적으로 늘리는 것입니다. 예를 들면 다음과 같습니다.

// resize output buffer *buffer to new_size bytes // return 1 on success, 0 on failure static int resize_buffer(char** buffer, size_t new_size) { char *new_buffer = realloc(*buffer, new_size); if (new_buffer) { *buffer = new_buffer; return 1; } else return 0; } // parse a JSON string value // expects the cursor to point after the initial double quote // return 1 on success, 0 on failure static int json_parse_string(const char** cursor, json_value* parent) { int success = 1; size_t length = 0, allocated = 8; // start with an 8-byte buffer char *new_string = malloc(allocated); if (!new_string) return 0; while (success && **cursor != """) { if (**cursor == "\0") { success = 0; // unterminated string } // we"re going to need at least one more byte of space while (success && length + 1 > allocated) { success = resize_buffer(&new_string, allocated *= 2); } if (!success) break; if (**cursor != "\\") { new_string[length++] = **cursor; // just copy normal bytes to output ++(*cursor); } else switch ((*cursor)[1]) { case "\\":new_string[length++] = "\\"; *cursor += 2; break; case "/": new_string[length++] = "/"; *cursor += 2; break; case """: new_string[length++] = """; *cursor += 2; break; case "b": new_string[length++] = "\b"; *cursor += 2; break; case "f": new_string[length++] = "\f"; *cursor += 2; break; case "n": new_string[length++] = "\n"; *cursor += 2; break; case "r": new_string[length++] = "\r"; *cursor += 2; break; case "t": new_string[length++] = "\t"; *cursor += 2; break; case "u": // TODO: handle Unicode escapes! (decode to UTF-8?) // note that this may require extending the buffer further default: success = 0; break; // invalid escape sequence } } success = success && resize_buffer(&new_string, length+1); if (!success) { free(new_string); return 0; } new_string[length] = "\0"; parent->type = TYPE_STRING; parent->value.string = new_string; ++(*cursor); // move cursor after final double quote return 1; } 

대체 솔루션은 입력에 대해 두 개의 구문 분석 패스를 실행하는 것입니다. 하나는 출력 문자열의 길이를 확인하기위한 것이고 다른 하나는 실제로 디코딩하는 것입니다. 이것은 가장 쉽게 수행 할 수 있습니다. 다음과 같이 :

static int json_parse_string(const char** cursor, json_value* parent) { char *tmp_cursor = *cursor; size_t length = (size_t)-1; if (!json_string_helper(&tmp_cursor, &length, NULL)) return 0; char *new_string = malloc(length); if (!new_string) return 0; if (!json_string_helper(&tmp_cursor, &length, new_string)) { free(new_string); return 0; } parent->type = TYPE_STRING; parent->value.string = new_string; *cursor = tmp_cursor; return 1; } 

여기서 도우미 함수 :

static int json_parse_helper(const char** cursor, size_t* length, char* new_string) { // ... } 

div id = “d735825c20”> 바이트를 new_string에 입력하고 구문 분석 된 문자열의 실제 길이를 *length에 씁니다. new_string == NULL 인 경우 실제로 디코딩 된 출력을 어디에도 저장하지 않고 문자열의 길이를 결정합니다.

숫자 파싱

현재 json_parse_value() 구현은 숫자를 기본 케이스로 취급하고 단순히 ", [, {, n, t 또는 f를 C 표준 라이브러리 함수 strtod()에 추가합니다.

strtod()는 유효한 JSON 숫자 리터럴의 상위 집합 이 작동합니다. 유효하지 않은 JSON을 유효한 것으로 간주하십시오. 예를 들어 코드는 +nan, -nan, +inf-inf는 유효한 숫자로 사용되며 0xABC123와 같은 16 진수 표기법도 허용합니다. 또한 위에 링크 된 strtod() 문서 참고 :

표준 “C”이외의 로케일 또는 “POSIX”로케일,이 함수는 추가 로케일 종속 구문을 인식 할 수 있습니다.

더 엄격하게하려면 JSON 문법 strtod()에 전달합니다.

또한 strtod()errno 예 입력 번호가 double 범위를 벗어난 경우. 아마도 이것을 확인해야 할 것입니다.

테스트

당신의 테스트를 자세히 살펴 보지는 않았지만, 당신이 테스트를 가지고 있다는 것을 알게되어 기쁩니다. 위의 경우 범위가 개선 될 수 있습니다.

개인적으로는 테스트를 구현에서 분리 된 소스 파일로 옮기고 싶습니다. 여기에는 장점과 단점이 모두 있습니다.

  • 주된 단점은 더 이상 정적 도우미 함수를 직접 테스트 할 수 없다는 것입니다. 그러나 공개 API가 깨끗하고 포괄적 인 것처럼 보이고 테스트를 복잡하게 만드는 “숨겨진 상태”문제가 발생하지 않는다는 점을 감안할 때 API를 통해서도 좋은 테스트 범위를 달성 할 수 있어야합니다.
  • 주된 이점 (구현 코드와 테스트 코드를 명확하게 구분하는 것 외에도)은 테스트가 공개 API를 자동으로 테스트한다는 것입니다. 특히 json.h 헤더의 모든 문제는 또한 API를 통해 테스트를 수행하면 API가 일반 용도로 충분히 완전하고 유연하다는 것을 확인하는 데 도움이됩니다.

정적 함수를 직접 테스트하려는 경우, 간단한 래퍼를 통해 또는 정의에서 static 키워드를 제거하여 테스트를 위해 선택적으로 노출하는 전 처리기 플래그를 항상 추가 할 수 있습니다.

Ps. I 귀하의 json_test_value_number() 테스트가 저에게 실패한다는 것을 알았습니까 (GCC 5.4.0, i386 arch), 아마도 숫자 23.4는 부동 소수점으로 정확하게 표현할 수 없기 때문입니다. 23.5로 변경하면 테스트를 통과하게됩니다.

댓글

  • 수고하셨습니다. 감사합니다. 분명히 제가하지 않았습니다. ‘ 내가 가지고 있어야 할만 큼 표준을 확인하지 마십시오. 의도적으로 utf-8을 무시했지만 (아마도 언급 했어야했습니다.
  • 좋습니다. 공평합니다. 최소한 \u00XX, 일부 인코더는 ASCII 문자에도 사용하도록 선택할 수 있습니다. (또한 이전에 수행하는 것을 잊었으므로 위의 일부 함수를 inline로 표시하라는 제안을 추가했습니다. .)
  • 표준의 해당 부분을 완전히 놓쳤습니다. 예, 이스케이프 시퀀스를 올바르게 구문 분석해야합니다.

Answer

전체 리뷰는 아니지만 코드를 읽는 동안 눈에 띄는 몇 가지 사항을 공유하겠습니다.

댓글

While 주석은 확실히 훌륭합니다. 일부 인라인 주석은 코드에 노이즈 만 추가합니다.

// Eat whitespace int success = 0; skip_whitespace(cursor); 

우선 주석이 한 줄 너무 빠릅니다. 둘째, 함수를 보면 공백이 소모된다는 것을 읽을 수 있습니다. 이름이 완벽하게 설명합니다. 추가 주석이 필요하지 않습니다.

case "\0": // If parse_value is called with the cursor at the end of the string // that"s a failure success = 0; break; 

다시 말하지만이 주석은 코드 자체가 말하는 내용을 반복합니다.


enum json_value_type { TYPE_NULL, TYPE_BOOL, TYPE_NUMBER, TYPE_OBJECT, // Is a vector with pairwise entries, key, value TYPE_ARRAY, // Is a vector, all entries are plain TYPE_STRING, TYPE_KEY }; 

이제 이러한 주석은 각 값이 나타내는 내용을 문서화하므로 실제로 쓸모가 없습니다. 그러나 TYPE_OBJECTTYPE_ARRAY에만 해당되는 이유는 무엇입니까? 모든 값에 대해서는 그렇지 않은 이유는 무엇입니까? 개인적으로 json.org 에 대한 링크를 enum 바로 앞에 넣었습니다. 귀하의 유형은 다음과 유사합니다. TYPE_KEY가 무엇인지 문서화하기 만하면됩니다. 다음 요점으로 넘어갑니다 …

TYPE_KEY

json.org 를 살펴보면 객체 문자열 으로 구성된 구성원 의 목록입니다. 즉, TYPE_KEY! TYPE_STRING 값과 모든 유형의 다른 json 값으로 구성된 members 에 대한 새 구조체를 추가하기 만하면됩니다. 지금 바로 사용할 수 있습니다. 예를 들어 허용되지 않는 값에 대한 키로 숫자가 있습니다. for 루프와 같이 객체 관련 로직 중 일부를 더 좋게 만들 수도 있습니다.

for (size_t i = 0; i < size; i += 2) 

이상하게도이 for 루프의 단계는 실제로 주석을 사용할 수 있지만 (왜 += 2?) 주석이 없습니다.

기타

case "\0": // If parse_value is called with the cursor at the end of the string // that"s a failure success = 0; break; 

return 0;가 아닌 이유는 무엇입니까?


while (iscntrl(**cursor) || isspace(**cursor)) ++(*cursor); 

if (success) ++(*cursor); 

if (has_char(cursor, "}")) break; else if (has_char(cursor, ",")) continue; 

및 기타 몇 가지. 특히 같은 줄에 조건과 문장을 넣는 것을 좋아합니다.if (!something) return;와 같이 제어 흐름을 위해이 작업을 수행해도 괜찮지 만 여전히 “meh”입니다. 더 잘하고 새로운 줄에 명령문을 넣으십시오.


또한 코드가 “영역”또는 당신이 부르고 싶은 것을 구분하기 위해 더 많은 빈 줄을 사용할 수 있음을 발견했습니다. 예 :

json_value key = { .type = TYPE_NULL }; json_value value = { .type = TYPE_NULL }; success = json_parse_value(cursor, &key); success = success && has_char(cursor, ":"); success = success && json_parse_value(cursor, &value); if (success) { vector_push_back(&result.value.object, &key); vector_push_back(&result.value.object, &value); } else { json_free_value(&key); break; } skip_whitespace(cursor); if (has_char(cursor, "}")) break; else if (has_char(cursor, ",")) continue; else success = 0; 

확인 및 반환 항목에서 설정 및 구문 분석 항목을 구분하는 하나의 빈 줄이 있지만 할 수 있습니다. 더 낫습니다.

json_value key = { .type = TYPE_NULL }; json_value value = { .type = TYPE_NULL }; success = json_parse_value(cursor, &key); success = success && has_char(cursor, ":"); success = success && json_parse_value(cursor, &value); if (success) { vector_push_back(&result.value.object, &key); vector_push_back(&result.value.object, &value); } else { json_free_value(&key); break; } skip_whitespace(cursor); if (has_char(cursor, "}")) break; else if (has_char(cursor, ",")) continue; else success = 0; 

이것은 값을 설정하기위한 블록, 값을 분석하기위한 블록, 값을 넣을 수있는 블록이 있습니다. 공백을 건너 뛰기위한 블록과 현재 작업을 마무리하기위한 블록을 벡터에 삽입합니다. skip_whitespace(cursor);if ... 사이의 마지막 빈 줄은 논쟁의 여지가 있습니다. ,하지만 저는이 방법을 선호합니다.


그 외에는 코드를 쉽게 읽고 이해할 수 있습니다. 오류가 있는지 제대로 확인하고 적절한 이름을 사용합니다. 관용성에 대해서는 별도로 내가 언급 한 바에 따르면, 특이하거나 비정상적인 것으로 표시 할 것은 없습니다.

답변

ctype.h의 함수는 , 정의되지 않은 동작을 호출 할 수 있기 때문입니다. 좋은 설명은 NetBSD 문서 를 참조하세요.

답글 남기기

이메일 주소를 발행하지 않을 것입니다. 필수 항목은 *(으)로 표시합니다