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

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:
2026-06-14 23:46:15 -04:00
parent ca6f69424f
commit dfcac64330
4 changed files with 1357 additions and 0 deletions
+157
View File
@@ -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;
}