Add JSON Schema -> C++ streaming parser generator
CI / pre-commit (push) Successful in 52s
CI / build (-DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++, clang-arm64, ubuntu-latest-arm64, true) (push) Successful in 53s
CI / build (-DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++, gcc-arm64, ubuntu-latest-arm64, false) (push) Successful in 51s
CI / build (-DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++, clang-amd64, ubuntu-latest-amd64, true) (push) Successful in 1m33s
CI / build (-DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++, gcc-amd64, ubuntu-latest-amd64, false) (push) Successful in 1m24s
CI / pre-commit (push) Successful in 52s
CI / build (-DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++, clang-arm64, ubuntu-latest-arm64, true) (push) Successful in 53s
CI / build (-DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++, gcc-arm64, ubuntu-latest-arm64, false) (push) Successful in 51s
CI / build (-DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++, clang-amd64, ubuntu-latest-amd64, true) (push) Successful in 1m33s
CI / build (-DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++, gcc-amd64, ubuntu-latest-amd64, false) (push) Successful in 1m24s
Generates a C++ type and a streaming builder parser from a JSON Schema, built on weaseljson. The builder copies incoming bytes directly into their final destinations in the result struct (no intermediate DOM) and hands back ownership via take() once parsing completes. Supports objects/structs, required vs optional (std::optional) fields, nullable types, string enums, arrays, $ref including recursion (broken with unique_ptr), and additionalProperties (strict reject or skip). Unsupported schema constructs are rejected at generation time; schema violations are rejected at parse time.
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
// Test harness for the generated example.schema.json parser.
|
||||
// Feeds input one byte at a time to exercise chunked string/number paths.
|
||||
#include <cassert>
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
|
||||
#include "gen.h"
|
||||
|
||||
using namespace weasel_schema;
|
||||
|
||||
// Feed `in` one byte at a time. Returns final status.
|
||||
static WeaselJsonStatus parseStrided(RootBuilder &b, std::string in) {
|
||||
for (size_t i = 0; i < in.size(); ++i) {
|
||||
char c = in[i];
|
||||
WeaselJsonStatus s = b.feed(&c, 1);
|
||||
if (s != WeaselJson_AGAIN)
|
||||
return s;
|
||||
}
|
||||
return b.finish();
|
||||
}
|
||||
|
||||
static int failures = 0;
|
||||
#define CHECK(cond) \
|
||||
do { \
|
||||
if (!(cond)) { \
|
||||
printf("FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond); \
|
||||
++failures; \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
static void expectReject(const std::string &json, const char *what) {
|
||||
RootBuilder b;
|
||||
WeaselJsonStatus s = parseStrided(b, json);
|
||||
if (s == WeaselJson_REJECT) {
|
||||
printf("ok reject: %s\n", what);
|
||||
} else {
|
||||
printf("FAIL expected reject (%s) got status %d for: %s\n", what, s,
|
||||
json.c_str());
|
||||
++failures;
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
// ---- happy path, full feature coverage, byte-strided ----
|
||||
{
|
||||
std::string json = R"({
|
||||
"name": "Ada É",
|
||||
"age": 36,
|
||||
"height": 1.75,
|
||||
"active": true,
|
||||
"nickname": null,
|
||||
"role": "admin",
|
||||
"hobbies": ["math", "lace"],
|
||||
"address": { "city": "London", "zip": 12345 },
|
||||
"friends": [
|
||||
{ "name": "Bob" },
|
||||
{ "name": "Cay", "age": 40 }
|
||||
],
|
||||
"tree": {
|
||||
"value": 1,
|
||||
"children": [ { "value": 2 }, { "value": 3 } ],
|
||||
"next": { "value": 99 }
|
||||
},
|
||||
"extra_unknown": { "ignored": [1, 2, {"x": "y"}] }
|
||||
})";
|
||||
// Root is strict (additionalProperties:false), so "extra_unknown" must
|
||||
// make it reject. Verify that, then test a clean version.
|
||||
expectReject(json, "unknown key in strict root");
|
||||
}
|
||||
|
||||
{
|
||||
std::string json = R"({
|
||||
"name": "Ada É",
|
||||
"age": 36,
|
||||
"height": 1.75,
|
||||
"active": true,
|
||||
"nickname": null,
|
||||
"role": "admin",
|
||||
"hobbies": ["math", "lace"],
|
||||
"address": { "city": "London", "zip": 12345 },
|
||||
"friends": [
|
||||
{ "name": "Bob" },
|
||||
{ "name": "Cay", "age": 40 }
|
||||
],
|
||||
"tree": {
|
||||
"value": 1,
|
||||
"children": [ { "value": 2 }, { "value": 3 } ],
|
||||
"next": { "value": 99 }
|
||||
}
|
||||
})";
|
||||
RootBuilder b;
|
||||
WeaselJsonStatus s = parseStrided(b, json);
|
||||
CHECK(s == WeaselJson_OK);
|
||||
if (s == WeaselJson_OK) {
|
||||
Root r = b.take();
|
||||
CHECK(r.name == "Ada \xC3\x89"); // É -> UTF-8
|
||||
CHECK(r.age == 36);
|
||||
CHECK(r.height > 1.74 && r.height < 1.76);
|
||||
CHECK(r.active.has_value() && *r.active == true);
|
||||
CHECK(!r.nickname.has_value()); // explicit null
|
||||
CHECK(r.role.has_value() && *r.role == Role::admin);
|
||||
CHECK(r.hobbies.has_value() && r.hobbies->size() == 2);
|
||||
CHECK(r.hobbies && (*r.hobbies)[0] == "math" &&
|
||||
(*r.hobbies)[1] == "lace");
|
||||
CHECK(r.address.has_value() && r.address->city == "London");
|
||||
CHECK(r.address && r.address->zip.has_value() &&
|
||||
*r.address->zip == 12345);
|
||||
CHECK(r.friends.has_value() && r.friends->size() == 2);
|
||||
CHECK(r.friends && (*r.friends)[0].name == "Bob");
|
||||
CHECK(r.friends && !(*r.friends)[0].age.has_value());
|
||||
CHECK(r.friends && (*r.friends)[1].age.has_value() &&
|
||||
*(*r.friends)[1].age == 40);
|
||||
CHECK(r.tree.has_value() && r.tree->value == 1);
|
||||
CHECK(r.tree && r.tree->children.has_value() &&
|
||||
r.tree->children->size() == 2);
|
||||
CHECK(r.tree && r.tree->children && (*r.tree->children)[0].value == 2);
|
||||
CHECK(r.tree && r.tree->children && (*r.tree->children)[1].value == 3);
|
||||
CHECK(r.tree && r.tree->next && r.tree->next->value == 99);
|
||||
printf("ok happy path (byte-strided)\n");
|
||||
}
|
||||
}
|
||||
|
||||
// ---- whole-buffer feed (not strided) ----
|
||||
{
|
||||
std::string json = R"({"name":"x","age":7})";
|
||||
RootBuilder b;
|
||||
WeaselJsonStatus s = b.feed(json.data(), (int)json.size());
|
||||
if (s == WeaselJson_AGAIN)
|
||||
s = b.finish();
|
||||
CHECK(s == WeaselJson_OK);
|
||||
Root r = b.take();
|
||||
CHECK(r.name == "x" && r.age == 7);
|
||||
CHECK(!r.role.has_value() && !r.hobbies.has_value());
|
||||
printf("ok minimal object\n");
|
||||
}
|
||||
|
||||
// ---- schema-violation rejections ----
|
||||
expectReject(R"({"name":"x"})", "missing required field 'age'");
|
||||
expectReject(R"({"name":"x","age":"notnum"})", "wrong type (string for int)");
|
||||
expectReject(R"({"name":"x","age":1,"role":"boss"})", "bad enum value");
|
||||
expectReject(R"({"name":"x","age":1,"name":"y"})", "duplicate key");
|
||||
expectReject(R"({"name":"x","age":1,"active":null})",
|
||||
"null for non-nullable");
|
||||
expectReject(R"({"name":"x","age":1,"age":2})", "duplicate required key");
|
||||
expectReject(R"({"name":"x","age":1.5})", "fractional for integer");
|
||||
expectReject(R"({"name":"x","age":1,"address":{"zip":5}})",
|
||||
"missing required nested 'city'");
|
||||
expectReject(R"([1,2,3])", "array where object expected (root)");
|
||||
expectReject(R"({"name":"x","age":1,)", "truncated / invalid json");
|
||||
|
||||
if (failures == 0) {
|
||||
printf("\nALL TESTS PASSED\n");
|
||||
return 0;
|
||||
}
|
||||
printf("\n%d FAILURE(S)\n", failures);
|
||||
return 1;
|
||||
}
|
||||
Reference in New Issue
Block a user