Jednoduchý analyzátor JSON v C

Zde je jednoduchý analyzátor JSON s rekurzivním sestupem, spousta dalších funkcí, i když používá zde rozbalitelnou vektorovou třídu, která je zde přezkoumána ( Jednoduchý rozšiřitelný vektor v C ). Neimplementoval jsem žádné optimalizace ani přístupové rozhraní vyšší úrovně, je to všechno docela základní. Neexistuje ani export JSON, jen import.

Celý zdroj je k dispozici na webu github ( https://github.com/HarryDC/JsonParser ). Včetně CMake a testovacího kódu, jen sem pošlu soubory analyzátoru.

Mým hlavním zájmem by bylo, kdyby existovalo více idiomatických způsobů psaní věcí v jazyce C. Ale samozřejmě je vždy oceněn i jakýkoli jiný vstup.

Záhlaví

#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 

Implementace

#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 

Odpovědi

Záhlaví

V jazyce C sdílejí všechna enum jména stejný prostor jmen ( a s věcmi, jako jsou názvy proměnných). Proto je dobré se pokusit snížit riziko, že se srazí.

Vaše enum json_value_type jména mají předponu TYPE_, což je docela obecné. Některá jiná knihovna se může pokusit použít stejný název. Navrhuji změnit tuto předponu například na JSON_.

Zdá se, že nepoužíváte TYPE_KEY pro cokoli. Jednoduše jej odstraňte.

Implementace

Jak poznamenává Roland Illig , argumenty iscntrl() a isspace() ve vaší skip_whitespace() funkci by měly být přeneseny do unsigned char do nepoužívejte příponu znaménka.

Alternativně a v těsnějším souladu s specifikací JSON můžete tuto funkci přepsat jednoduše takto:

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

Mnoho vašich static pomocných funkcí dělá netriviální kombinace věcí a chybí jim jakýkoli komentář vysvětlující, o co jde dělat. Jeden nebo dva řádky komentářů před každou funkcí mohou hodně pomoci čitelnosti.

Zejména vaše funkce has_char() dělá spoustu různých věcí:

  1. Přeskočí mezery.
  2. Zkontroluje přítomnost určitého znaku ve vstupu.
  3. Pokud je znak nalezen, automaticky jej přeskočí.

Pouze # 2 je zjevně implikován názvem funkce; ostatní jsou neočekávané vedlejší účinky a měly by být alespoň jasně zdokumentovány.

Ve skutečnosti se mi zdá, že by mohlo být lepší odstranit volání skip_whitespace() from has_char(), a stačí nechat volajícího výslovně přeskočit mezery, než jej v případě potřeby zavolá. V mnoha případech to váš kód již dělá, čímž duplikát přeskočí nadbytečný.

Aby byl efekt č. 3 pro čtenáře méně překvapivý, mohl by být dobrý nápad tuto funkci přejmenovat na něco víc aktivní jako například read_char().


Na konci json_parse_object() máte:

 return success; return 1; } 

To je určitě nadbytečné. Stačí se zbavit return 1;.

Také Vypadá to, že používáte obecnou funkci json_parse_value() k analýze klíčů objektů a netestujete, abyste se ujistili, že obsahují řetězce. To umožňuje, aby se nějaký neplatný JSON dostal přes váš analyzátor. Navrhuji buď přidat explicitní kontrolu typu, nebo rozdělit kód pro analýzu řetězce do samostatné funkce (jak je popsáno níže) a volat jej přímo z json_parse_object().


V horní části json_parse_array() máte:

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

Můžete jej přepsat stejným způsobem jako děláte v json_parse_object():

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

(Jen víte, stále si myslím, že jméno read_char() by bylo lepší.)

Zdá se, že z nějakého důvodu váš json_parse_array() očekává, že volající inicializuje parent struct, zatímco json_parse_object() to dělá automaticky. AFAICT není důvod pro nekonzistenci, takže byste mohli a pravděpodobně byste měli nechat obě funkce fungovat stejným způsobem.


Vaše funkce json_is_literal() není označena jako static, i když to není objeví se v záhlaví. Stejně jako is_char() já raději jej tedy přejmenujte na něco aktivnějšího, například json_read_literal() nebo jen read_literal(), aby bylo jasnější, že automaticky posune kurzor na úspěšná shoda.

(Všimněte si také, že jak bylo napsáno, tato funkce ne nekontroluje, zda literál ve vstupu skutečně končí tam, kde má. Například by úspěšně odpovídalo vstupu nullnullnull proti null.Nemyslím si, že je to skutečná chyba, protože jediné platné literály v JSONu jsou true, false a null, žádný z nich není předponou každého jiný, a protože dva literály se nemohou v platném JSONu po sobě objevit bez nějakého jiného tokenu mezi nimi. Ale určitě to stojí za zmínku v komentáři.)


Možná budete chtít také explicitně označit některé ze svých statických pomocných funkcí jako inline dát kompilátoru nápovědu, že by se měl pokusit je sloučit do volacího kódu. Navrhuji to alespoň pro skip_whitespace(), has_char() a json_is_literal().

Protože vaše přístupové funkce json_value_to_X() nesestávají z ničeho jiného než assert() a dereference ukazatele, měli byste také zvážit přesunutí jejich implementací do json.h a jejich označení jako static inline. To by umožnilo kompilátoru začlenit je do volacího kódu i v jiných .c souborech a případně optimalizovat pryč assert(), pokud volání kód již typ stejně kontroluje.


Ve své hlavní funkci json_parse() možná budete chtít explicitně zkontrolovat, zda v vstup po analýze kořenové hodnoty.

Analýza řetězců

Váš kód analýzy řetězce v json_parse_value() je nefunkční, protože to nejde zpracovat úniky zpětného lomítka. Například selže při následujícím platném vstupu JSON:

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

Možná jej budete chtít přidat jako testovací případ.

Vy by měl také otestovat, zda váš kód správně zpracovává další sekvence zpětného lomítka jako \b, \f, \n, \r, \t, \/ a zejména \\ a \unnnn. Tady je několik dalších testovacích případů pro tyto:

"\"\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" 

Protože řetězce JSON mohou obsahovat libovolné znaky Unicode, budete se muset rozhodnout, jak s nimi zacházet. Pravděpodobně nejjednodušší volbou bude deklarovat váš vstup a výstup v UTF-8 (nebo snad WTF-8 ) a převést \unnnn uniká do bajtových sekvencí UTF-8 (a volitelně naopak). Vzhledem k tomu, že používáte řetězce zakončené nulou, můžete raději dekódovat \u0000 do příliš dlouhého kódování "\xC0\x80" místo nulového bajtu.


Kvůli zachování čitelnosti hlavní json_parse_value() funkce, Důrazně doporučuji rozdělit kód pro analýzu řetězců na samostatnou pomocnou funkci. Zejména proto, že jeho správné zpracování úniků zpětného lomítka to značně zkomplikuje.

Jednou z komplikací je, že vlastně nevíte, jak dlouho řetězec bude, dokud jej nebudete analyzovat. Jedním ze způsobů, jak se s tím vypořádat, by bylo dynamicky zvětšit přidělený výstupní řetězec pomocí realloc(), například takto:

// 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; } 

Alternativním řešením by bylo spustit dva vstupy pro analýzu nad vstupem: jeden pouze pro určení délky výstupního řetězce a druhý pro jeho skutečné dekódování. To by bylo nejjednodušší něco takového:

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; } 

kde pomocná funkce:

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

analyzuje řetězec JSON nejvýše *length bajtů do new_string a zapíše skutečnou délku analyzovaného řetězce do *length, nebo, pokud new_string == NULL, určuje pouze délku řetězce bez uložení dekódovaného výstupu kdekoli.

Analýza čísel

Vaše aktuální json_parse_value() implementace považuje čísla za výchozí případ a jednoduše krmí vše, co není ", [, {, n, t nebo f do standardní funkce knihovny C strtod().

Protože strtod() přijímá nadmnožina platných literálů čísel JSON, mělo by to fungovat, ale váš kód může někdy fungovat cept neplatný JSON jako platný. Váš kód například přijme +nan, -nan, +inf a -inf jako platná čísla a bude také přijímat hexadecimální zápis jako 0xABC123. Jak také uvádí strtod() dokumentace propojená výše:

V jiném národním prostředí, než je standardní „C“ nebo „POSIX“ národní prostředí, může tato funkce rozpoznat další syntaxi závislou na národním prostředí.

Pokud chcete být přísnější, možná budete chtít explicitně ověřit vše, co vypadá jako číslo, proti gramatika JSON , než ji předáte strtod().

Upozorňujeme, že strtod() může nastavit errno např pokud je číslo vstupu mimo rozsah double. Pravděpodobně byste to měli kontrolovat.

Testování

Nezkoumal jsem vaše testy podrobně, ale je skvělé vidět, že je máte (i když, jak je uvedeno) výše, jejich pokrytí by bylo možné vylepšit).

Osobně bych ale raději přesunul testy mimo implementaci do samostatného zdrojového souboru. To má výhody i nevýhody:

  • Hlavní nevýhodou je, že již nemůžete přímo testovat statické pomocné funkce. Avšak vzhledem k tomu, že vaše veřejné API vypadá čistě a komplexně a netrpí žádnými problémy „skrytého stavu“, které by komplikovaly testování, měli byste být schopni dosáhnout dobrého pokrytí testem i jen prostřednictvím API.
  • Hlavní výhodou (kromě čistého oddělení mezi implementačním a testovacím kódem) je, že vaše testy automaticky otestují veřejné API. Zejména jakékoli problémy s json.h záhlaví se objeví v své testy. Provádění testů pomocí rozhraní API vám také pomůže zajistit, aby vaše rozhraní API bylo skutečně dostatečně úplné a flexibilní pro obecné použití.

Pokud opravdu stále chcete přímo otestovat statické funkce, vždy můžete přidat příznak preprocesoru, který je volitelně zpřístupní pro testování, buď pomocí jednoduchých obalů, nebo jednoduše odstraněním klíčového slova static z jejich definic.

Ps. I všiml jste si, že váš json_test_value_number() test u mě selhává (GCC 5.4.0, i386 arch), pravděpodobně protože číslo 23,4 není přesně reprezentovatelné v plovoucí řádové čárce. Pokud jej změníte na 23,5, bude test úspěšný.

Komentáře

  • Skvělá práce, díky, samozřejmě jsem ‚ t zkontrolujte standard stejně, jak bych měl. I když jsem úmyslně ignoroval utf-8 (pravděpodobně jsem to měl zmínit.
  • OK, dost fér. Pravděpodobně byste měli stále alespoň dekódovat jednoduché úniky ve tvaru \u00XX, protože některé kodéry se mohou rozhodnout je použít i pro znaky ASCII. (Také jsem přidal návrh označit některé z vašich funkcí jako inline výše, protože jsem to zapomněl dříve .)
  • Ta část standardu mi úplně chyběla, ano, únikové sekvence je třeba správně analyzovat.

Odpovědět

Toto není v žádném případě úplná kontrola, ale budu sdílet některé věci, které mě zaujaly při čtení vašeho kódu.

Komentáře

Zatímco komentáře jsou určitě pěkné, některé vaše vložené komentáře přidávají do kódu pouze šum.

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

Za prvé, komentář je o jeden řádek příliš brzy. Za druhé, jeden může číst, že mezery jsou spotřebovány při pohledu na funkci – název ji dokonale popisuje, th Není potřeba žádný další komentář.

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

Tento komentář opět jen opakuje to, co říká samotný kód.


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 }; 

Tyto komentáře teď nejsou opravdu zbytečné, protože dokumentují, co každá hodnota představuje. Proč ale jen pro TYPE_OBJECT a TYPE_ARRAY – proč ne pro všechny hodnoty? Osobně jsem dal odkaz na json.org těsně před tím enum. Vaše typy jsou analogické ty, které tam jsou, potřebujete pouze dokument, který má být TYPE_KEY. Což mě přivádí k dalšímu bodu …

TYPE_KEY

Když se podíváte na json.org , uvidíte objekt , který se skládá z seznam členů , které jsou zase tvořeny řetězcem a hodnotou . Což znamená, že TYPE_KEY! Stačí přidat novou strukturu pro členy skládající se z hodnoty TYPE_STRING a další hodnoty json jakéhokoli typu a můžete začít. Právě teď můžete mít např. číslo jako klíč pro hodnotu, která není povolena. Také by byla hezčí nějaká logika související s objektem, například pro smyčku:

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

Je ironií, že krok této smyčky for mohl ve skutečnosti použít komentář (proč += 2?), Ale chybí.

Různé

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

Proč ne jen return 0;?


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

a

if (success) ++(*cursor); 

a

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

a několik dalších. Nejsem obzvláště rádi dáváte podmínku a prohlášení na stejný řádek, zejména proto, že to neděláte důsledně.Jsem v pořádku, když to dělám kvůli toku řízení, jako if (!something) return;, ale stále je to „já“. Raději to udělejte správně a vložte prohlášení na nový řádek.


Také jsem zjistil, že váš kód může použít několik prázdnějších řádků k oddělení „oblastí“ nebo jakkoli jim chcete říkat . Například:

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; 

Existuje jeden prázdný řádek oddělující věci setup-and-parse od věcí check-and-return, ale můžete to udělat lepší.

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; 

Považuji to za čistší. Máte blok pro nastavení hodnot, blok pro jejich analýzu, blok pro jejich vkládání do vektoru, blok pro přeskočení mezer a blok pro dokončení aktuální akce. Poslední prázdný řádek mezi skip_whitespace(cursor); a if ... je diskutabilní , ale dávám to přednost tomuto způsobu.


Kromě toho jsem zjistil, že je váš kód snadno čitelný a srozumitelný. Správně kontrolujete případné chyby a používáte rozumné pojmenování. Pokud jde o idiomaticitu, kromě z toho, co jsem „zmínil, není nic, co bych označil jako neobvyklé nebo neidomatické.

Odpověď

Funkce z ctype.h nesmí být volány s argumenty typu char, protože to může vyvolat nedefinované chování. Dobré vysvětlení najdete v dokumentaci NetBSD .

Napsat komentář

Vaše e-mailová adresa nebude zveřejněna. Vyžadované informace jsou označeny *