diff --git a/contrib/schemagen/README.md b/contrib/schemagen/README.md new file mode 100644 index 0000000..5418333 --- /dev/null +++ b/contrib/schemagen/README.md @@ -0,0 +1,75 @@ +# weaseljson schemagen + +Generates a C++ type and a streaming **builder** parser from a JSON Schema, +on top of weaseljson. + +The builder copies incoming bytes straight into their final destinations in the +result struct as they arrive (no intermediate DOM). When parsing completes you +`take()` ownership of the result. + +```sh +python3 weaseljson_schemagen.py schema.json -o parsed.h --namespace myschema +``` + +## Usage + +```cpp +#include "parsed.h" +using namespace myschema; + +RootBuilder b; +WeaselJsonStatus s = b.feed(buf, len); // call repeatedly with chunks; buf may + // be modified in place (unescaping) +if (s == WeaselJson_AGAIN) s = b.finish(); +if (s == WeaselJson_OK) { + Root value = b.take(); // ownership of the parsed result +} +``` + +`feed`/`finish` return the usual `WeaselJsonStatus`; `WeaselJson_OK` means the +document is both valid JSON and schema-valid, and `WeaselJson_REJECT` covers +both malformed JSON and schema violations. The builder holds interior pointers +into the result, so it is non-movable. + +## Schema -> C++ mapping + +| JSON Schema | C++ | +|-----------------------------------------------|----------------------------------------| +| `object` with `properties` | `struct` | +| required property | value member | +| non-required property | `std::optional` | +| `["T", "null"]` (nullable) | `std::optional` (accepts `null`) | +| `string` / `integer` / `number` / `boolean` | `std::string` / `int64_t` / `double` / `bool` | +| `enum` of strings | `enum class : int` | +| `array` with `items` | `std::vector` | +| `$ref` to `$defs`/`definitions` | the referenced named struct | +| recursive `$ref` | `std::unique_ptr` (cycle broken) | +| `additionalProperties: false` | unknown keys rejected | +| `additionalProperties` absent / `true` | unknown keys' values skipped | + +## Schema violations (rejected at parse time) + +- missing required property (top-level or nested) +- wrong JSON type for a property / array element +- `null` for a non-nullable slot +- value not in a string `enum` +- a number not representable in the target type (e.g. `1.5` for an `integer`) +- duplicate object keys +- unknown key under `additionalProperties: false` + +## Not supported (rejected at generation time, no fallback) + +`oneOf` / `anyOf` / `allOf` / `not` / `if`-`then`-`else`, +`patternProperties`, `additionalProperties` with a schema (typed map), +`prefixItems` (tuples), `const`, `dependentSchemas`/`dependentRequired`, +union `type` lists other than `["T", "null"]`, non-string enums, and remote +(`$ref` to other documents). + +## Notes + +- Strings (the bulk payload) are appended directly into their destination + `std::string`, even when split across `feed` calls. Numbers are accumulated in + a small scratch buffer and converted on completion, since an `int64_t`/`double` + cannot hold partial digits. +- `test_gen.cpp` generates from `example.schema.json` and exercises the parser + byte-by-byte (covering chunked strings/numbers), plus the rejection cases. diff --git a/contrib/schemagen/example.schema.json b/contrib/schemagen/example.schema.json new file mode 100644 index 0000000..1bd6180 --- /dev/null +++ b/contrib/schemagen/example.schema.json @@ -0,0 +1,99 @@ +{ + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "age" + ], + "properties": { + "name": { + "type": "string" + }, + "age": { + "type": "integer" + }, + "height": { + "type": "number" + }, + "active": { + "type": "boolean" + }, + "nickname": { + "type": [ + "string", + "null" + ] + }, + "role": { + "enum": [ + "admin", + "user", + "guest" + ] + }, + "hobbies": { + "type": "array", + "items": { + "type": "string" + } + }, + "address": { + "type": "object", + "required": [ + "city" + ], + "additionalProperties": false, + "properties": { + "city": { + "type": "string" + }, + "zip": { + "type": "integer" + } + } + }, + "friends": { + "type": "array", + "items": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + }, + "age": { + "type": "integer" + } + } + } + }, + "tree": { + "$ref": "#/$defs/Node" + } + }, + "$defs": { + "Node": { + "type": "object", + "additionalProperties": false, + "required": [ + "value" + ], + "properties": { + "value": { + "type": "integer" + }, + "next": { + "$ref": "#/$defs/Node" + }, + "children": { + "type": "array", + "items": { + "$ref": "#/$defs/Node" + } + } + } + } + } +} diff --git a/contrib/schemagen/test_gen.cpp b/contrib/schemagen/test_gen.cpp new file mode 100644 index 0000000..ac36de2 --- /dev/null +++ b/contrib/schemagen/test_gen.cpp @@ -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 +#include +#include + +#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; +} diff --git a/contrib/schemagen/weaseljson_schemagen.py b/contrib/schemagen/weaseljson_schemagen.py new file mode 100644 index 0000000..6f751df --- /dev/null +++ b/contrib/schemagen/weaseljson_schemagen.py @@ -0,0 +1,1026 @@ +#!/usr/bin/env python3 +"""Generate a C++ type + streaming parser from a JSON Schema, using weaseljson. + +The generated parser is a "builder": you feed it bytes incrementally and it +writes them straight into their final destinations in the result struct. When +parsing completes you take() ownership of the result. + +Only the subset of JSON Schema that maps naturally onto C++ structs is +supported. Anything else is rejected at generation time (no JsonValue fallback). + +Usage: + weaseljson_schemagen.py schema.json [-o out.h] [--namespace ns] +""" + +import argparse +import json +import keyword +import sys + + +class GenError(Exception): + pass + + +# --------------------------------------------------------------------------- +# Type model +# --------------------------------------------------------------------------- +class TScalar: + def __init__(self, kind): # 'str' | 'int' | 'dbl' | 'bool' + self.kind = kind + + +class TEnum: + def __init__(self, name): + self.name = name + + +class TObj: + def __init__(self, name): + self.name = name + + +class TArr: + def __init__(self, elem, elem_nullable): + self.elem = elem + self.elem_nullable = elem_nullable + + +class Field: + def __init__(self, key, cpp, ty, required, nullable): + self.key = key # JSON key + self.cpp = cpp # C++ member name + self.ty = ty + self.required = required + self.nullable = nullable + self.unique = False # set during recursion breaking + + +class ObjectType: + def __init__(self, name): + self.name = name + self.fields = [] # list[Field] + self.strict = False # additionalProperties: false + + +class EnumType: + def __init__(self, name, values): + self.name = name + self.values = values # list[str] + + +# --------------------------------------------------------------------------- +# Identifier helpers +# --------------------------------------------------------------------------- +def sanitize(name, fallback="x"): + out = [] + for ch in name: + out.append(ch if (ch.isalnum() or ch == "_") else "_") + s = "".join(out) + if not s: + s = fallback + if s[0].isdigit(): + s = "_" + s + if keyword.iskeyword(s) or s in _CPP_KEYWORDS: + s = s + "_" + return s + + +def camel(name): + parts = [p for p in name.replace("-", " ").replace("_", " ").split(" ") if p] + if not parts: + return "T" + return "".join(p[:1].upper() + p[1:] for p in parts) + + +_CPP_KEYWORDS = { + "alignas", + "alignof", + "and", + "asm", + "auto", + "bool", + "break", + "case", + "catch", + "char", + "class", + "const", + "constexpr", + "continue", + "decltype", + "default", + "delete", + "do", + "double", + "else", + "enum", + "explicit", + "export", + "extern", + "false", + "float", + "for", + "friend", + "goto", + "if", + "inline", + "int", + "long", + "namespace", + "new", + "not", + "nullptr", + "operator", + "or", + "private", + "protected", + "public", + "register", + "return", + "short", + "signed", + "sizeof", + "static", + "struct", + "switch", + "template", + "this", + "throw", + "true", + "try", + "typedef", + "typename", + "union", + "unsigned", + "using", + "virtual", + "void", + "volatile", + "while", +} + + +# --------------------------------------------------------------------------- +# Schema -> type model +# --------------------------------------------------------------------------- +class Builder: + def __init__(self, root_schema): + self.root_schema = root_schema + self.defs = {} + for key in ("$defs", "definitions"): + self.defs.update(root_schema.get(key, {})) + self.objects = {} # name -> ObjectType (insertion ordered) + self.enums = {} # name -> EnumType + self._building = {} # def name -> TObj/etc (for $ref cycles) + self._used_names = set() + + def unique_name(self, hint): + base = camel(hint) + name = base + i = 1 + while name in self._used_names: + i += 1 + name = f"{base}{i}" + self._used_names.add(name) + return name + + def ref_name(self, ref): + if not ref.startswith("#/"): + raise GenError(f"only local $ref supported, got: {ref}") + parts = ref[2:].split("/") + if len(parts) != 2 or parts[0] not in ("$defs", "definitions"): + raise GenError(f"unsupported $ref target: {ref}") + return parts[1] + + def build_def(self, defname): + """Build (or fetch cached) type for a named $defs entry.""" + if defname in self._building: + return self._building[defname] + if defname not in self.defs: + raise GenError(f"$ref to unknown def: {defname}") + node = self.defs[defname] + # For objects we must register the name before recursing into fields. + return self.build_type(node, defname, defname=defname) + + def build_type(self, node, hint, defname=None): + if not isinstance(node, dict): + raise GenError(f"schema node must be an object: {node!r}") + + for bad in ( + "allOf", + "oneOf", + "anyOf", + "not", + "if", + "then", + "else", + "patternProperties", + "prefixItems", + "const", + "dependentSchemas", + "dependentRequired", + ): + if bad in node: + raise GenError( + f"'{bad}' is not supported (no natural C++ struct mapping)" + ) + + if "$ref" in node: + return self.build_def(self.ref_name(node["$ref"])) + + # nullability via type lists: ["string", "null"] + nullable = False + typ = node.get("type") + if isinstance(typ, list): + non_null = [t for t in typ if t != "null"] + if "null" in typ: + nullable = True + if len(non_null) != 1: + raise GenError(f"union types are not supported: {typ!r}") + typ = non_null[0] + + # string enum + if "enum" in node: + vals = node["enum"] + if not vals or not all(isinstance(v, str) for v in vals): + raise GenError("only non-empty string enums are supported") + name = defname and self.unique_name(defname) or self.unique_name(hint) + self.enums[name] = EnumType(name, list(vals)) + return (TEnum(name), nullable) + + if typ == "object" or (typ is None and "properties" in node): + return (self._build_object(node, hint, defname), nullable) + + if typ == "array": + if "items" not in node or not isinstance(node["items"], dict): + raise GenError("arrays require a single 'items' schema") + elem, elem_nullable = self._unpack( + self.build_type(node["items"], hint + "Item") + ) + return (TArr(elem, elem_nullable), nullable) + + scalar = { + "string": "str", + "integer": "int", + "number": "dbl", + "boolean": "bool", + }.get(typ) + if scalar: + return (TScalar(scalar), nullable) + if typ == "null": + raise GenError("'null'-only types are not supported") + + raise GenError(f"unsupported schema (type={typ!r})") + + @staticmethod + def _unpack(result): + """build_type returns (ty, nullable) for some paths and a bare ty for + recursive object registration. Normalize to (ty, nullable).""" + if isinstance(result, tuple): + return result + return (result, False) + + def _build_object(self, node, hint, defname): + name = self.unique_name(defname or hint) + obj = ObjectType(name) + self.objects[name] = obj + # register for $ref cycles before building fields + if defname is not None: + self._building[defname] = TObj(name) + ap = node.get("additionalProperties", True) + if isinstance(ap, dict): + raise GenError( + "additionalProperties with a schema (typed map) is not " "supported yet" + ) + obj.strict = ap is False + required = set(node.get("required", [])) + props = node.get("properties", {}) + seen_cpp = set() + for key, sub in props.items(): + ty, nullable = self._unpack(self.build_type(sub, key)) + cpp = sanitize(key) + base = cpp + i = 1 + while cpp in seen_cpp: + i += 1 + cpp = f"{base}{i}" + seen_cpp.add(cpp) + obj.fields.append(Field(key, cpp, ty, key in required, nullable)) + return TObj(name) + + +# --------------------------------------------------------------------------- +# Recursion breaking + struct ordering +# --------------------------------------------------------------------------- +def opt_storage(field): + """True if the field is stored in a std::optional (absent or null-valued). + A field is optional storage when it is not required or accepts null. + unique_ptr fields are already nullable, so they are excluded.""" + return (not field.required or field.nullable) and not field.unique + + +def needs_complete(field): + """True if `field`'s storage requires the referenced object to be a + complete type at the point of the owning struct's definition. + optional and value members need T complete; unique_ptr does not.""" + return isinstance(field.ty, TObj) and not field.unique + + +def break_cycles(objects): + """Mark fields as unique_ptr to break definition cycles. An edge O->T + exists when O stores T by value/optional (needs T complete).""" + while True: + color = {} # name -> 0 white,1 gray,2 black + cycle_field = [None] + + def dfs(name): + color[name] = 1 + for f in objects[name].fields: + if not needs_complete(f): + continue + t = f.ty.name + c = color.get(t, 0) + if c == 1: # back edge -> cycle + cycle_field[0] = f + return True + if c == 0 and dfs(t): + # break at the deepest unbroken edge we control + if cycle_field[0] is None: + cycle_field[0] = f + return True + color[name] = 2 + return False + + found = False + for name in objects: + if color.get(name, 0) == 0 and dfs(name): + found = True + break + if not found: + return + cycle_field[0].unique = True + + +def order_objects(objects): + """Topologically order structs so value/optional members are defined + after their dependencies.""" + ordered = [] + state = {} # 0 unvisited, 1 visiting, 2 done + + def visit(name): + s = state.get(name, 0) + if s == 2: + return + if s == 1: + return # remaining cycles are via unique_ptr/vector; safe + state[name] = 1 + for f in objects[name].fields: + if needs_complete(f): + visit(f.ty.name) + state[name] = 2 + ordered.append(name) + + for name in objects: + visit(name) + return ordered + + +# --------------------------------------------------------------------------- +# C++ rendering +# --------------------------------------------------------------------------- +class Emitter: + def __init__(self, b, namespace): + self.b = b + self.namespace = namespace + self.arr_kinds = {} # vector-cpp-signature -> kind name + self.arr_types = [] # list[(kind_name, TArr)] + self.kind_order = [] # all Kind enumerators in declaration order + self.root_ty = None + self.root_nullable = False + + # -- type strings ------------------------------------------------------- + def base_cpp(self, ty): + if isinstance(ty, TScalar): + return { + "str": "std::string", + "int": "int64_t", + "dbl": "double", + "bool": "bool", + }[ty.kind] + if isinstance(ty, TEnum): + return ty.name + if isinstance(ty, TObj): + return ty.name + if isinstance(ty, TArr): + return f"std::vector<{self.storage_cpp(ty.elem, ty.elem_nullable, False)}>" + raise GenError("bad type") + + def storage_cpp(self, ty, nullable, unique): + base = self.base_cpp(ty) + if unique: + return f"std::unique_ptr<{base}>" + if nullable: + return f"std::optional<{base}>" + return base + + # -- Kind registration -------------------------------------------------- + def arr_kind(self, tarr): + sig = self.base_cpp(tarr) + if sig not in self.arr_kinds: + name = f"Arr{len(self.arr_kinds)}" + self.arr_kinds[sig] = name + self.arr_types.append((name, tarr)) + return self.arr_kinds[sig] + + def cat(self, ty): + if isinstance(ty, TScalar): + return {"str": "Str", "int": "Int", "dbl": "Dbl", "bool": "Bool"}[ty.kind] + if isinstance(ty, TEnum): + return "Enum" + if isinstance(ty, TObj): + return "Obj" + if isinstance(ty, TArr): + return "Arr" + raise GenError("bad type") + + def child_kind(self, ty): + if isinstance(ty, TObj): + return f"Kind::{ty.name}" + if isinstance(ty, TArr): + return f"Kind::{self.arr_kind(ty)}" + return "Kind{}" + + # -- top-level emit ----------------------------------------------------- + def emit(self): + self.root_ty, self.root_nullable = Builder._unpack( + self.b.build_type(self.b.root_schema, "Root") + ) + break_cycles(self.b.objects) + + # register all array kinds (walk every field + root) + self._walk_arrays(self.root_ty) + for obj in self.b.objects.values(): + for f in obj.fields: + self._walk_arrays(f.ty) + + order = order_objects(self.b.objects) + + parts = [] + parts.append(self._header()) + parts.append(self._enums()) + parts.append(self._struct_decls(order)) + parts.append(self._root_alias()) + parts.append(self._builder(order)) + parts.append(self._footer()) + return "\n".join(p for p in parts if p) + + def _walk_arrays(self, ty): + if isinstance(ty, TArr): + self.arr_kind(ty) + self._walk_arrays(ty.elem) + + # -- sections ----------------------------------------------------------- + def _header(self): + ns = self.namespace + return f"""// Generated by weaseljson_schemagen.py -- do not edit. +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "weaseljson.h" + +namespace {ns} {{""" + + def _footer(self): + return f"}} // namespace {self.namespace}\n" + + def _enums(self): + if not self.b.enums: + return "" + out = [] + for e in self.b.enums.values(): + vals = ", ".join(sanitize(v) for v in e.values) + out.append(f"enum class {e.name} : int {{ {vals} }};") + return "\n".join(out) + "\n" + + def _struct_decls(self, order): + fwd = "\n".join(f"struct {n};" for n in self.b.objects) + out = [fwd, ""] + for name in order: + obj = self.b.objects[name] + out.append(f"struct {name} {{") + for f in obj.fields: + store = self.storage_cpp(f.ty, opt_storage(f), f.unique) + init = "" + if ( + isinstance(f.ty, TScalar) + and not opt_storage(f) + and not f.unique + and f.ty.kind in ("int", "dbl", "bool") + ): + init = " = 0" if f.ty.kind != "bool" else " = false" + out.append(f' {store} {f.cpp}{init}; // "{f.key}"') + out.append("};") + out.append("") + return "\n".join(out) + + def _root_alias(self): + if isinstance(self.root_ty, TObj) and not self.root_nullable: + if self.root_ty.name == "Root": + return "" # the struct is already named Root + return f"using Root = {self.root_ty.name};\n" + store = self.storage_cpp(self.root_ty, self.root_nullable, False) + return f"using Root = {store};\n" + + # -- the builder -------------------------------------------------------- + def _kind_enum(self): + kinds = list(self.b.objects.keys()) + kinds += [n for n, _ in self.arr_types] + root_is_container = ( + isinstance(self.root_ty, (TObj, TArr)) and not self.root_nullable + ) + if not root_is_container: + kinds.append("RootScalar") + kinds.append("Skip") + self.kind_order = kinds + return " enum class Kind : uint8_t { " + ", ".join(kinds) + " };" + + def _root_info(self): + """Return (root_cat, root_container_kind_or_None).""" + if isinstance(self.root_ty, TObj) and not self.root_nullable: + return ("Obj", f"Kind::{self.root_ty.name}") + if isinstance(self.root_ty, TArr) and not self.root_nullable: + return ("Arr", f"Kind::{self.arr_kind(self.root_ty)}") + return (self.cat(self.root_ty), None) + + def _slotinfo(self): + lines = [" SlotInfo slotInfo(const Frame &f) const {", " switch (f.kind) {"] + for name, obj in self.b.objects.items(): + lines.append(f" case Kind::{name}:") + lines.append(" switch (f.field) {") + for i, fld in enumerate(obj.fields): + lines.append( + f" case {i}: return {self._slot_expr(fld.ty, fld.nullable)};" + ) + lines.append(" default: return SlotInfo{Cat::Reject};") + lines.append(" }") + for name, tarr in self.arr_types: + lines.append( + f" case Kind::{name}: return {self._slot_expr(tarr.elem, tarr.elem_nullable)};" + ) + root_cat, root_kind = self._root_info() + if root_kind is None: + lines.append( + f" case Kind::RootScalar: return {self._slot_expr(self.root_ty, self.root_nullable)};" + ) + lines.append(" default: return SlotInfo{Cat::Reject};") + lines.append(" }") + lines.append(" }") + return "\n".join(lines) + + def _slot_expr(self, ty, nullable): + cat = self.cat(ty) + nb = "true" if nullable else "false" + if isinstance(ty, TEnum): + return ( + f"SlotInfo{{Cat::Enum, Kind{{}}, {nb}, " + f"{ty.name}_names, {len(self.b.enums[ty.name].values)}}}" + ) + if isinstance(ty, (TObj, TArr)): + return f"SlotInfo{{Cat::{cat}, {self.child_kind(ty)}, {nb}}}" + return f"SlotInfo{{Cat::{cat}, Kind{{}}, {nb}}}" + + def _engage(self): + lines = [ + " void *engage(Frame &f, bool fresh) {", + " (void)fresh;", + " switch (f.kind) {", + ] + for name, obj in self.b.objects.items(): + lines.append(f" case Kind::{name}: {{") + lines.append(f" auto *o = ({name} *)f.dest;") + lines.append(" switch (f.field) {") + for i, fld in enumerate(obj.fields): + lines.append(f" case {i}: {self._engage_field(fld)}") + lines.append(" default: return nullptr;") + lines.append(" }") + lines.append(" }") + for name, tarr in self.arr_types: + vectype = self.base_cpp(tarr) + lines.append(f" case Kind::{name}: {{") + lines.append(f" auto *v = ({vectype} *)f.dest;") + lines.append(" if (fresh) v->emplace_back();") + if tarr.elem_nullable: + lines.append(" auto &e = v->back();") + lines.append(" if (!e) e.emplace();") + lines.append(" return &*e;") + else: + lines.append(" return &v->back();") + lines.append(" }") + root_cat, root_kind = self._root_info() + if root_kind is None: + lines.append(" case Kind::RootScalar: return &result_;") + lines.append(" default: return nullptr;") + lines.append(" }") + lines.append(" }") + return "\n".join(lines) + + def _engage_field(self, fld): + acc = f"o->{fld.cpp}" + if fld.unique: + base = self.base_cpp(fld.ty) + return ( + f"{{ if (!{acc}) {acc} = std::make_unique<{base}>(); " + f"return {acc}.get(); }}" + ) + if opt_storage(fld): + return f"{{ if (!{acc}) {acc}.emplace(); return &*{acc}; }}" + return f"return &{acc};" + + def _matchkey(self): + lines = [ + " int matchKey(Kind k, std::string_view key) const {", + " switch (k) {", + ] + for name, obj in self.b.objects.items(): + lines.append(f" case Kind::{name}:") + for i, fld in enumerate(obj.fields): + esc = fld.key.replace("\\", "\\\\").replace('"', '\\"') + lines.append(f' if (key == "{esc}") return {i};') + lines.append(" return -1;") + lines.append(" default: return -1;") + lines.append(" }") + lines.append(" }") + return "\n".join(lines) + + def _bitmask(self, obj, pred): + mask = 0 + for i, fld in enumerate(obj.fields): + if pred(fld): + mask |= 1 << i + return mask + + def _reqmask(self): + lines = [" uint32_t requiredMask(Kind k) const {", " switch (k) {"] + for name, obj in self.b.objects.items(): + m = self._bitmask(obj, lambda f: f.required) + lines.append(f" case Kind::{name}: return {hex(m)}u;") + lines.append(" default: return 0u;") + lines.append(" }") + lines.append(" }") + return "\n".join(lines) + + def _is_object_kind(self): + objs = list(self.b.objects.keys()) + if not objs: + return " bool isObjectKind(Kind) const { return false; }" + cases = " ".join(f"case Kind::{n}:" for n in objs) + return ( + " bool isObjectKind(Kind k) const {\n" + f" switch (k) {{ {cases} return true; default: return false; }}\n" + " }" + ) + + def _is_strict(self): + strict = [n for n, o in self.b.objects.items() if o.strict] + if not strict: + return " bool isStrict(Kind) const { return false; }" + cases = " ".join(f"case Kind::{n}:" for n in strict) + return ( + " bool isStrict(Kind k) const {\n" + f" switch (k) {{ {cases} return true; default: return false; }}\n" + " }" + ) + + def _enum_name_arrays(self): + out = [] + for e in self.b.enums.values(): + lits = ", ".join( + '"' + v.replace("\\", "\\\\").replace('"', '\\"') + '"' + for v in e.values + ) + out.append( + f" static constexpr const char *{e.name}_names[] = {{ {lits} }};" + ) + return "\n".join(out) + + def _ctor_body(self): + root_cat, root_kind = self._root_info() + lines = [] + if root_kind is None: + lines.append(" stack_.push_back(Frame{Kind::RootScalar, &result_, 0});") + return "\n".join(lines) + + def _begin_container(self, event_cat, kind_expr): + """Shared body for cbBeginObject / cbBeginArray.""" + root_cat, root_kind = self._root_info() + lines = [" if (error_) return;"] + if root_kind is not None and root_cat == event_cat: + lines.append(" if (stack_.empty()) {") + lines.append(f" stack_.push_back(Frame{{{root_kind}, &result_}});") + lines.append(" return;") + lines.append(" }") + else: + lines.append(" if (stack_.empty()) { reject(); return; }") + lines.append(" Frame &f = stack_.back();") + lines.append( + " if (f.kind == Kind::Skip) { stack_.push_back(Frame{Kind::Skip, nullptr}); return; }" + ) + lines.append(" SlotInfo si = slotInfoG(f);") + lines.append( + " if (si.cat == Cat::Skip) { stack_.push_back(Frame{Kind::Skip, nullptr}); return; }" + ) + lines.append(f" if (si.cat != Cat::{event_cat}) {{ reject(); return; }}") + lines.append(" void *p = engage(f, true);") + lines.append(" stack_.push_back(Frame{si.child, p});") + return "\n".join(lines) + + def _builder(self, order): + kind_enum = self._kind_enum() + root_cat, root_kind = self._root_info() + begin_obj = self._begin_container("Obj", root_kind) + begin_arr = self._begin_container("Arr", root_kind) + + return f""" +class RootBuilder {{ +public: + explicit RootBuilder(int stackSize = 1024) {{ + cb_ = makeCallbacks(); + parser_ = WeaselJsonParser_create(stackSize, &cb_, this, 0); +{self._ctor_body()} + }} + ~RootBuilder() {{ if (parser_) WeaselJsonParser_destroy(parser_); }} + RootBuilder(const RootBuilder &) = delete; + RootBuilder &operator=(const RootBuilder &) = delete; + RootBuilder(RootBuilder &&) = delete; // frames hold interior pointers + + // Feed more bytes. `buf` may be modified in place (unescaping). Returns: + // WeaselJson_AGAIN - need more input + // WeaselJson_OK - complete and schema-valid; call take() + // WeaselJson_REJECT - invalid JSON or schema violation + // WeaselJson_OVERFLOW - nesting exceeded stackSize + WeaselJsonStatus feed(char *buf, int len) {{ + if (error_) return WeaselJson_REJECT; + WeaselJsonStatus s = WeaselJsonParser_parse(parser_, buf, len); + if (error_) return WeaselJson_REJECT; + return s; + }} + WeaselJsonStatus finish() {{ return feed(nullptr, 0); }} + + bool ok() const {{ return done_ && !error_; }} + // Valid only after feed()/finish() returned WeaselJson_OK. + Root take() {{ return std::move(result_); }} + +private: +{kind_enum} + enum class Cat {{ Reject, Str, Int, Dbl, Bool, Enum, Obj, Arr, Skip }}; + struct SlotInfo {{ + Cat cat; + Kind child{{}}; + bool nullable = false; + const char *const *enames = nullptr; + int encount = 0; + }}; + struct Frame {{ + Kind kind; + void *dest; + int field = -1; // object: selected field (-1 want key, -2 skip) + uint32_t seen = 0; // bitmask of populated fields + }}; + static constexpr int kWantKey = -1; + static constexpr int kSkip = -2; + +{self._enum_name_arrays()} + + Root result_{{}}; + std::vector stack_; + std::string scratch_; // accumulates number text / pending key / enum text + bool started_ = false; // a chunked scalar value is in progress + bool error_ = false; + bool done_ = false; + WeaselJsonParser *parser_ = nullptr; + WeaselJsonCallbacks cb_{{}}; + + void reject() {{ error_ = true; }} + + // Wrap the generated slotInfo() with the generic key/skip states. + SlotInfo slotInfoG(const Frame &f) {{ + if (isObjectKind(f.kind)) {{ + if (f.field == kSkip) return SlotInfo{{Cat::Skip}}; + if (f.field < 0) return SlotInfo{{Cat::Reject}}; + }} + return slotInfo(f); + }} + + // A value finished; update the owning container (now on top of stack). + void valueComplete() {{ + if (stack_.empty()) {{ done_ = true; return; }} + Frame &p = stack_.back(); + started_ = false; + scratch_.clear(); +{(" if (p.kind == Kind::RootScalar) { stack_.pop_back(); done_ = true; return; }" if root_kind is None else "")} + if (isObjectKind(p.kind)) {{ + if (p.field >= 0) p.seen |= (1u << p.field); + p.field = kWantKey; + }} + }} + + void cbBeginObject() {{ +{begin_obj} + }} + void cbEndObject() {{ + if (error_) return; + Frame f = stack_.back(); + if (f.kind == Kind::Skip) {{ + stack_.pop_back(); + if (stack_.empty() || stack_.back().kind != Kind::Skip) valueComplete(); + return; + }} + if (isObjectKind(f.kind) && + (f.seen & requiredMask(f.kind)) != requiredMask(f.kind)) {{ + reject(); + return; + }} + stack_.pop_back(); + valueComplete(); + }} + void cbBeginArray() {{ +{begin_arr} + }} + void cbEndArray() {{ + if (error_) return; + Frame f = stack_.back(); + if (f.kind == Kind::Skip) {{ + stack_.pop_back(); + if (stack_.empty() || stack_.back().kind != Kind::Skip) valueComplete(); + return; + }} + stack_.pop_back(); + valueComplete(); + }} + + void cbKeyData(const char *buf, int len, int done) {{ + if (error_) return; + Frame &f = stack_.back(); + if (!isObjectKind(f.kind)) {{ reject(); return; }} + scratch_.append(buf, len); + if (!done) return; + int idx = matchKey(f.kind, scratch_); + scratch_.clear(); + if (idx < 0) {{ + if (isStrict(f.kind)) {{ reject(); return; }} + f.field = kSkip; + return; + }} + if (f.seen & (1u << idx)) {{ reject(); return; }} // duplicate key + f.field = idx; + }} + + void cbStringData(const char *buf, int len, int done) {{ + if (error_) return; + Frame &f = stack_.back(); + if (f.kind == Kind::Skip) return; + SlotInfo si = slotInfoG(f); + if (si.cat == Cat::Skip) {{ if (done) valueComplete(); return; }} + if (si.cat == Cat::Str) {{ + auto *s = (std::string *)engage(f, !started_); + s->append(buf, len); + started_ = true; + if (done) valueComplete(); + return; + }} + if (si.cat == Cat::Enum) {{ + scratch_.append(buf, len); + started_ = true; + if (!done) return; + int idx = -1; + for (int i = 0; i < si.encount; ++i) + if (scratch_ == si.enames[i]) {{ idx = i; break; }} + if (idx < 0) {{ reject(); return; }} + *(int *)engage(f, true) = idx; + valueComplete(); + return; + }} + reject(); + }} + + void cbNumberData(const char *buf, int len, int done) {{ + if (error_) return; + Frame &f = stack_.back(); + if (f.kind == Kind::Skip) return; + SlotInfo si = slotInfoG(f); + if (si.cat == Cat::Skip) {{ if (done) valueComplete(); return; }} + if (si.cat != Cat::Int && si.cat != Cat::Dbl) {{ reject(); return; }} + scratch_.append(buf, len); + started_ = true; + if (!done) return; + const char *b = scratch_.data(); + const char *e = b + scratch_.size(); + void *p = engage(f, true); + if (si.cat == Cat::Int) {{ + int64_t v = 0; + auto r = std::from_chars(b, e, v); + if (r.ec != std::errc() || r.ptr != e) {{ reject(); return; }} + *(int64_t *)p = v; + }} else {{ + double v = 0; + auto r = std::from_chars(b, e, v); + if (r.ec != std::errc() || r.ptr != e) {{ reject(); return; }} + *(double *)p = v; + }} + valueComplete(); + }} + + void cbBool(bool value) {{ + if (error_) return; + Frame &f = stack_.back(); + if (f.kind == Kind::Skip) return; + SlotInfo si = slotInfoG(f); + if (si.cat == Cat::Skip) {{ valueComplete(); return; }} + if (si.cat != Cat::Bool) {{ reject(); return; }} + *(bool *)engage(f, true) = value; + valueComplete(); + }} + + void cbNull() {{ + if (error_) return; + Frame &f = stack_.back(); + if (f.kind == Kind::Skip) return; + SlotInfo si = slotInfoG(f); + if (si.cat == Cat::Skip) {{ valueComplete(); return; }} + if (!si.nullable) {{ reject(); return; }} + valueComplete(); // leave optional empty / pointer null + }} + + static WeaselJsonCallbacks makeCallbacks() {{ + WeaselJsonCallbacks c; + c.on_begin_object = [](void *u) {{ ((RootBuilder *)u)->cbBeginObject(); }}; + c.on_end_object = [](void *u) {{ ((RootBuilder *)u)->cbEndObject(); }}; + c.on_begin_array = [](void *u) {{ ((RootBuilder *)u)->cbBeginArray(); }}; + c.on_end_array = [](void *u) {{ ((RootBuilder *)u)->cbEndArray(); }}; + c.on_key_data = [](void *u, const char *b, int n, int d) {{ + ((RootBuilder *)u)->cbKeyData(b, n, d); + }}; + c.on_string_data = [](void *u, const char *b, int n, int d) {{ + ((RootBuilder *)u)->cbStringData(b, n, d); + }}; + c.on_number_data = [](void *u, const char *b, int n, int d) {{ + ((RootBuilder *)u)->cbNumberData(b, n, d); + }}; + c.on_true_literal = [](void *u) {{ ((RootBuilder *)u)->cbBool(true); }}; + c.on_false_literal = [](void *u) {{ ((RootBuilder *)u)->cbBool(false); }}; + c.on_null_literal = [](void *u) {{ ((RootBuilder *)u)->cbNull(); }}; + return c; + }} + +{self._slotinfo()} + +{self._engage()} + +{self._matchkey()} + +{self._reqmask()} + +{self._is_object_kind()} + +{self._is_strict()} +}}; +""" + + +def main(argv): + ap = argparse.ArgumentParser(description=__doc__) + ap.add_argument("schema") + ap.add_argument("-o", "--output") + ap.add_argument("--namespace", default="weasel_schema") + args = ap.parse_args(argv) + + with open(args.schema) as fp: + schema = json.load(fp) + + b = Builder(schema) + em = Emitter(b, args.namespace) + try: + code = em.emit() + except GenError as e: + print(f"error: {e}", file=sys.stderr) + return 1 + + if args.output: + with open(args.output, "w") as fp: + fp.write(code) + else: + sys.stdout.write(code) + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:]))