Analizator JSON simplu în C

Iată un analizor JSON descendent recursiv simplu, nu o mulțime de funcționalități suplimentare, deși folosește clasa de vector extensibil revizuită aici ( Vector simplu expandabil în C ). Nu am implementat nicio optimizare, nici o interfață de acces la nivel superior, este destul de simplu. De asemenea, nu există un export JSON, doar import.

Întreaga sursă este disponibilă la github ( https://github.com/HarryDC/JsonParser ). CMake și codul de testare sunt incluse, voi posta doar fișierele parser aici.

Principalul meu interes ar fi dacă există mai multe moduri idiomatice de a scrie lucruri în C. Dar, desigur, orice altă contribuție este întotdeauna apreciată și ea.

Antet

#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 

Implementare

#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 

Răspuns

Antet

În C, toate numele enum au același spațiu de nume unul cu celălalt ( și cu lucruri precum nume variabile). Prin urmare, este o idee bună să încercați să reduceți riscul ca acestea să se ciocnească.

Numele enum json_value_type au prefixul TYPE_, care este destul de generic. O altă bibliotecă ar putea încerca să utilizeze același nume. Aș sugera să schimbați prefixul, să zicem, JSON_.

De asemenea, nu pareți să utilizați TYPE_KEY pentru orice. Eliminați-l.

Implementare

După cum remarcă Roland Illig , argumentele pentru iscntrl() și isspace() din funcția dvs. skip_whitespace() ar trebui să fie aruncate la unsigned char evitați extensia semnului.

Alternativ și urmând mai atent

spec JSON , puteți rescrie această funcție pur și simplu ca:

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

Multe dintre funcțiile dvs. de asistență static fac combinații non-banale de lucruri și nu au niciun comentariu care să explice ce do. Una sau două linii de comentarii înainte de fiecare funcție ar putea ajuta la lizibilitate mult.

În special, funcția dvs. has_char() face o grămadă de lucruri diferite:

  1. Omite spațiul alb.
  2. Verifică prezența unui anumit caracter în intrare.
  3. Dacă caracterul este găsit, îl omite automat.

Numai # 2 este evident implicit de numele funcției; celelalte sunt efecte secundare neașteptate și ar trebui cel puțin documentate clar.

De fapt, mi se pare că ar fi mai bine să eliminați apelul către skip_whitespace() din has_char() și lăsați apelantul să sară explicit în spațiul alb înainte de a-l apela, dacă este necesar. În multe cazuri, codul dvs. face deja acest lucru, făcând saltul duplicat redundant.

De asemenea, pentru a face efectul # 3 mai puțin surprinzător pentru cititor, ar putea fi o idee bună să redenumiți funcția respectivă cu ceva puțin mai mult. activ cum ar fi, să zicem, read_char().


La sfârșitul json_parse_object(), aveți:

 return success; return 1; } 

Sigur că e redundant. Scapă de return 1;.

De asemenea, se pare că utilizați funcția generică json_parse_value() pentru a analiza tastele obiect și nu testați pentru a vă asigura că acestea sunt șiruri. Acest lucru permite unor JSON nevalide să treacă prin analizorul dvs. Aș sugera fie adăugarea unei verificări explicite de tip, fie divizarea codului de analiză a șirurilor într-o funcție separată (așa cum este descris mai jos) și apelarea acestuia direct din json_parse_object().


În partea de sus a json_parse_array(), aveți:

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

Puteți rescrie acest lucru în același mod ca faceți în json_parse_object():

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

(Numai, știți, încă mai cred că numele read_char() ar fi mai bine.)

De asemenea, dintr-un anumit motiv, json_parse_array() pare să se aștepte ca apelantul să inițializeze parent struct, în timp ce json_parse_object() o face automat. AFAICT nu există niciun motiv pentru inconsecvență, deci ați putea și probabil ar trebui să faceți ca ambele funcții să funcționeze în același mod.


Funcția dvs. json_is_literal() nu este marcată ca static, chiar dacă nu „t apar în antet. Ca is_char(), eu „d al deci, preferați să îl redenumiți cu ceva mai activ, cum ar fi json_read_literal() sau doar read_literal(), pentru a clarifica faptul că avansează automat cursorul pe un potrivire reușită.

(Rețineți, de asemenea, că, așa cum este scris, această funcție nu verifică dacă literalul din intrare se termină acolo unde ar trebui să fie. De exemplu, s-ar potrivi cu succes cu intrarea nullnullnull cu null.Nu cred că este o eroare reală, deoarece singurele litere valide din JSON sunt true, false și null, dintre care niciunul nu este prefix unul de celălalt și, din moment ce două litere nu pot apărea consecutiv în JSON valid fără un alt indicativ între ele. Dar este cu siguranță cel puțin demn de menționat într-un comentariu.)


S-ar putea să doriți, de asemenea, să marcați în mod explicit unele dintre funcțiile dvs. de asistență statică ca inline pentru a oferi compilatorului un indiciu că ar trebui să încerce să le îmbine în codul de apelare. Aș sugera să faceți acest lucru cel puțin pentru skip_whitespace(), has_char() și json_is_literal().

Întrucât funcțiile dvs. de acces json_value_to_X() nu conțin altceva decât un assert() și o dereferință a indicatorului, ar trebui să luați în considerare mutarea implementărilor acestora în json.h și marcarea acestora ca static inline. Acest lucru ar permite compilatorului să le integreze în codul de apelare chiar și în alte fișiere .c și, eventual, să optimizeze assert() dacă apelul codul verifică deja tipul oricum.


În funcția dvs. principală json_parse(), vă recomandăm să verificați în mod explicit dacă nu există altceva decât spațiu alb în intrare după ce valoarea rădăcină a fost analizată.

Analizarea șirurilor

Codul dvs. de analiză a șirurilor din json_parse_value() este rupt, deoarece nu „t manevrează scăderea barei inversă. De exemplu, nu reușește la următoarea intrare JSON validă:

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

Poate doriți să adăugați acest lucru ca caz de testare.

ar trebui, de asemenea, să testeze dacă codul dvs. gestionează corect alte secvențe de ieșire inversă, precum \b, \f, \n, \r, \t, \/ și în special \\ și \unnnn. Iată câteva alte cazuri de testare pentru acestea:

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

Deoarece șirurile JSON pot conține caractere Unicode arbitrare, va trebui să decideți cum să le gestionați. Probabil cea mai simplă alegere ar fi să declarați intrarea și ieșirea în UTF-8 (sau poate WTF-8 ) și să convertiți \unnnn scapă în secvențe de octeți UTF-8 (și, opțional, invers). Rețineți că, din moment ce utilizați șiruri terminate cu nul, este posibil să preferați să decodificați \u0000 în codificarea overlong "\xC0\x80" în locul unui octet nul.


Pentru a menține funcția principală json_parse_value() lizibilă, Aș recomanda cu tărie să împărțiți codul de analiză a șirului într-o funcție de asistență separată. Mai ales că dacă îl faceți să gestioneze corect scăpările de bară inversă, acesta se va complica considerabil.

Una dintre complicații este că nu ați știut cât timp șirul va fi până când l-ați analizat. O modalitate de a rezolva acest lucru ar fi creșterea dinamică a șirului de ieșire alocat cu realloc(), de exemplu așa:

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

O soluție alternativă ar fi să rulați două treceri de analiză peste intrare: una doar pentru a determina lungimea șirului de ieșire și alta pentru a o decoda efectiv. Acest lucru ar fi cel mai ușor de făcut. ceva de genul acesta:

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

unde funcția de ajutor:

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

analizează un șir JSON de cel mult *length octeți în new_string și scrie lungimea reală a șirului analizat în *length sau dacă new_string == NULL, doar determină lungimea șirului fără a stoca efectiv ieșirea decodificată oriunde.

Analizarea numărului

json_parse_value() implementarea tratează numerele ca fiind cazul implicit și alimentează pur și simplu orice nu are ", [, {, n, t sau f în funcția de bibliotecă standard C strtod().

Deoarece strtod() acceptă un superset de litere JSON valide, acest lucru ar trebui să funcționeze, dar codul dvs. poate face ca uneori să fie ac a acceptat JSON nevalid ca fiind valid. De exemplu, codul dvs. va accepta +nan, -nan, +inf și -inf ca numere valide și va accepta, de asemenea, notația hexazecimală ca 0xABC123. De asemenea, ca strtod() documentație legată mai sus:

Într-o altă locație decât standardul „C” sau locale „POSIX”, această funcție poate recunoaște o sintaxă suplimentară dependentă de localizare.

Dacă doriți să fiți mai stricți, vă recomandăm să validați în mod explicit orice arata ca un număr față de Gramatica JSON înainte de a o transmite către strtod().

Rețineți, de asemenea, că strtod() poate seta errno de ex dacă numărul de intrare se află în afara unui double. Probabil că ar trebui să verificați acest lucru.

Testare

Nu am analizat testele dvs. în detaliu, dar este minunat să văd că le aveți (chiar dacă, așa cum sa menționat mai sus, acoperirea lor ar putea fi îmbunătățită).

Personal, totuși, aș prefera să mut testele din implementare într-un fișier sursă separat. Acest lucru are atât avantaje, cât și dezavantaje:

  • Principalul dezavantaj este că nu mai puteți testa direct funcțiile de asistență statică. Cu toate acestea, având în vedere că API-ul dvs. public arată curat și cuprinzător și nu are probleme de „stare ascunsă” care ar complica testarea, ar trebui să puteți obține o acoperire bună a testelor chiar și prin intermediul API-ului. > Principalul avantaj (în afară de o separare curată între implementare și codul de testare) este că testele dvs. vor testa automat API-ul public. În special, orice problemă cu antetul json.h va apărea în testele dvs. De asemenea, efectuarea testelor prin API vă ajută să vă asigurați că API-ul dvs. este într-adevăr suficient de complet și flexibil pentru uz general.

Dacă doriți cu adevărat să testați în mod direct funcțiile statice, puteți adăuga întotdeauna un semnal de preprocesor care le expune opțional pentru testare, fie prin împachetări simple, fie doar prin eliminarea cuvântului cheie static din definițiile acestora.

Ps. I am observat că testul json_test_value_number() eșuează pentru mine (GCC 5.4.0, arc i386), probabil deoarece numărul 23.4 nu este exact reprezentabil în virgulă mobilă. Dacă îl schimbați la 23,5, testul va trece.

Comentarii

  • Muncă grozavă, mulțumesc, evident că nu am ‘ t verific standardul atât cât ar trebui. Deși am ignorat intenționat utf-8 (probabil ar fi trebuit să menționez asta.
  • OK, destul de corect. Probabil că ar trebui să decodezi cel puțin scăpările simple de la forma \u00XX, deoarece unele codificatoare ar putea alege să le folosească chiar și pentru caractere ASCII. (De asemenea, am adăugat o sugestie pentru a marca unele dintre funcțiile dvs. ca inline de mai sus, deoarece am uitat să fac asta mai devreme .)
  • Mi-a lipsit total acea parte a standardului, da, secvențele de evadare trebuie analizate corect.

Răspuns

Aceasta nu este în niciun caz o recenzie completă, dar voi împărtăși câteva lucruri care mi-au atras atenția în timp ce citesc codul dvs.

Comentarii

În timp ce comentariile sunt cu siguranță frumoase, unele dintre comentariile dvs. în linie adaugă doar zgomot la cod.

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

În primul rând, comentariul este cu o linie prea devreme. În al doilea rând, se poate citi că spațiul alb este consumat uitându-se la funcția – numele o descrie perfect, th Nu este nevoie de un comentariu suplimentar.

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

Din nou, acest comentariu repetă doar ceea ce spune codul în sine.


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

Acum, aceste comentarii nu sunt cu adevărat inutile, deoarece documentează ce reprezintă fiecare valoare. Dar de ce numai pentru TYPE_OBJECT și TYPE_ARRAY – de ce nu pentru toate valorile? Personal, aș fi pus doar un link către json.org chiar înainte de enum. Tipurile dvs. sunt similare cu cei de acolo, trebuie doar să documentați ceea ce se presupune că este TYPE_KEY. Ceea ce mă aduce la punctul următor …

TYPE_KEY

Aruncând o privire la json.org , puteți vedea un obiect format din o listă de membri , care la rândul lor sunt compuși dintr-un șir și o valoare . Ceea ce înseamnă că nu aveți cu adevărat nevoie de TYPE_KEY! Doar adăugați o nouă structură pentru membri constând dintr-o valoare TYPE_STRING și o altă valoare json de orice tip și sunteți bine să mergeți. În acest moment, ați putea au, de exemplu, un număr ca cheie pentru o valoare, care nu este permisă. Ar face și mai frumoasă o parte din logica legată de obiect, ca aceasta pentru buclă:

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

În mod ironic, pasul acestei bucle for ar putea folosi un comentariu (de ce += 2?), Dar îi lipsește unul.

Diverse

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

De ce nu doar return 0;?


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

și

if (success) ++(*cursor); 

și

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

și alte câteva dintre acestea. Nu sunt Îmi place în mod deosebit să puneți condiția și declarația pe aceeași linie, mai ales că nu faceți acest lucru constant.Sunt „în regulă să fac asta de dragul fluxului de control, cum ar fi if (!something) return;, dar„ încă ”este„ meh ”. Mai bine faceți-o bine și puneți declarația pe o nouă linie.


De asemenea, consider că codul dvs. ar putea folosi câteva linii mai goale pentru a separa „regiunile” sau orice ați dori să le numiți . De exemplu:

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; 

Există o singură linie goală care separă setările și analizele de chestii de verificare și returnare, dar puteți face mai bine.

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; 

Mi se pare mai curat. Aveți un bloc pentru setarea valorilor, un bloc pentru analiză, un bloc pentru punerea lor în vector, un bloc pentru omiterea spațiului alb și un bloc pentru finalizarea acțiunii curente. Ultima linie goală dintre skip_whitespace(cursor); și if ... este discutabilă , dar îl prefer în acest fel.


În afară de asta, am găsit codul dvs. ușor de citit și de înțeles. Verificați corect erorile și utilizați denumiri sensibile. În ceea ce privește idiomaticitatea, în afară din ceea ce am menționat, nimic nu aș marca ca neobișnuit sau neidomatic.

Răspuns

Funcțiile din ctype.h nu trebuie apelate cu argumente de tip char, deoarece acest lucru poate invoca un comportament nedefinit. Consultați documentația NetBSD pentru o explicație bună.

Lasă un răspuns

Adresa ta de email nu va fi publicată. Câmpurile obligatorii sunt marcate cu *