#!/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 def _escape_cpp_string(s): """Return *s* escaped for use inside a C++ double-quoted string literal. JSON strings may contain control characters; emitting them verbatim into generated C++ source breaks tokenization. This helper escapes backslashes and double quotes, maps common control characters to their short escape sequences, and uses universal character names (\\u00XX) for any other character below 0x20. """ out = [] for ch in s: cp = ord(ch) if cp == 0x09: out.append("\\t") elif cp == 0x0A: out.append("\\n") elif cp == 0x0B: out.append("\\v") elif cp == 0x0C: out.append("\\f") elif cp == 0x0D: out.append("\\r") elif cp == 0x08: out.append("\\b") elif cp == 0x07: out.append("\\a") elif cp < 0x20: out.append(f"\\u{cp:04X}") elif ch == "\\": out.append("\\\\") elif ch == '"': out.append('\\"') else: out.append(ch) return "".join(out) 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] 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 unique_enum_identifiers(values): """Return a list of sanitized C++ identifiers, one per input value. Distinct JSON enum values may sanitize to the same C++ token (e.g. "foo-bar" and "foo_bar" both become "foo_bar"). This helper appends a numeric suffix to later collisions so the generated `enum class` stays valid while preserving the original order and therefore the index-to-value mapping used at parse time. """ used = set() out = [] for v in values: base = sanitize(v) if base not in used: used.add(base) out.append(base) continue n = 1 while True: candidate = f"{base}_{n}" if candidate not in used: used.add(candidate) out.append(candidate) break n += 1 return out 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", "concept", "const", "consteval", "constinit", "constexpr", "continue", "co_await", "co_return", "co_yield", "decltype", "default", "delete", "do", "double", "else", "enum", "explicit", "export", "extern", "false", "float", "for", "friend", "goto", "if", "import", "inline", "int", "long", "module", "namespace", "new", "not", "nullptr", "operator", "or", "private", "protected", "public", "register", "return", "requires", "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 self._name_taken(name): candidate = f"{base}{i}" # A numeric suffix can itself land on a reserved generated name # (e.g. hint "Arr0" -> "Arr01"). Use an underscore separator so # we never loop through the reserved block. if self._is_reserved(candidate): candidate = f"{base}_{i}" name = candidate i += 1 self._used_names.add(name) return name def _name_taken(self, name): return name in self._used_names or self._is_reserved(name) @staticmethod def _is_reserved(name): """Names generated internally that must not collide with user types.""" return name in ("Root", "RootScalar") @staticmethod def _array_reaches(target, ty): """Return True if `target` can be reached from `ty` by following TArr element types. This detects self-referential array cycles that cannot be expressed as C++ structs.""" seen = set() stack = [ty] while stack: cur = stack.pop() if cur is target: return True if id(cur) in seen: continue seen.add(id(cur)) if isinstance(cur, TArr): stack.append(cur.elem) return False 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] 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"])) # Non-object $defs entries must reuse the same type for every $ref. if defname is not None and defname in self._building: return self._building[defname] # 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)) result = (TEnum(name), nullable) if defname is not None: self._building[defname] = result return result if typ == "object" or (typ is None and "properties" in node): return (self._build_object(node, hint, defname, nullable), nullable) if typ == "array": if "items" not in node or not isinstance(node["items"], dict): raise GenError("arrays require a single 'items' schema") # Register the array before building its items so $ref cycles back # to this definition resolve to the same TArr instance. t = TArr(None, False) result = (t, nullable) if defname is not None: self._building[defname] = result elem, elem_nullable = self._unpack( self.build_type(node["items"], hint + "Item") ) t.elem = elem t.elem_nullable = elem_nullable # Self-referential array cycles (directly or through a chain of # array definitions) cannot be represented as a C++ value type. if self._array_reaches(t, elem): raise GenError( "recursive array type is not supported: " f"{defname or hint!r}" ) return result scalar = { "string": "str", "integer": "int", "number": "dbl", "boolean": "bool", }.get(typ) if scalar: result = (TScalar(scalar), nullable) if defname is not None: self._building[defname] = result return result 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, nullable=False): name = self.unique_name(defname or hint) obj = ObjectType(name) self.objects[name] = obj # register for $ref cycles before building fields tobj = TObj(name) if defname is not None: self._building[defname] = (tobj, nullable) ap = node.get("additionalProperties", None) if ap is None: raise GenError( "additionalProperties is required for object schemas; set it " "explicitly to false to reject unknown keys (absent " "additionalProperties is not supported)" ) if ap is True: raise GenError("additionalProperties: true is not supported") if isinstance(ap, dict): raise GenError( "additionalProperties with a schema (typed map) is not " "supported yet" ) 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 # --------------------------------------------------------------------------- # 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 self._arr_counter = 0 # -- 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 = self._fresh_arr_kind_name() self.arr_kinds[sig] = name self.arr_types.append((name, tarr)) self.b._used_names.add(name) return self.arr_kinds[sig] def _fresh_arr_kind_name(self): while True: name = f"Arr{self._arr_counter}" self._arr_counter += 1 if not self.b._name_taken(name): return name 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") ) # A nullable root object would otherwise produce # using Root = std::optional; # which conflicts with the struct named Root. Rename the inner struct. if isinstance(self.root_ty, TObj) and self.root_nullable: old_name = self.root_ty.name new_name = self.b.unique_name("RootInner") obj = self.b.objects.pop(old_name) obj.name = new_name self.b.objects[new_name] = obj self.root_ty = TObj(new_name) 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 #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(unique_enum_identifiers(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" esc_key = _escape_cpp_string(f.key) out.append(f' {store} {f.cpp}{init}; // "{esc_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)) if not root_is_container: kinds.append("RootScalar") 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): return ("Obj", f"Kind::{self.root_ty.name}") if isinstance(self.root_ty, TArr): 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: if self.root_nullable: lines.append( " case Kind::RootScalar: { if (!result_) result_.emplace(); return &*result_; }" ) else: 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 = _escape_cpp_string(fld.key) 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 _fieldcount(self): lines = [" int fieldCount(Kind k) const {", " switch (k) {"] for name, obj in self.b.objects.items(): lines.append(f" case Kind::{name}: return {len(obj.fields)};") lines.append(" default: return 0;") lines.append(" }") lines.append(" }") return "\n".join(lines) def _reqmask(self): lines = [ " const std::vector &requiredMask(Kind k) const {", " switch (k) {", ] for name, obj in self.b.objects.items(): words = (len(obj.fields) + 63) // 64 req = [0] * words for i, fld in enumerate(obj.fields): if fld.required: req[i >> 6] |= 1 << (i & 63) init = ", ".join(f"{hex(w)}u" for w in req) if words else "" lines.append(f" case Kind::{name}: {{") lines.append(f" static const std::vector m = {{ {init} }};") lines.append(" return m;") lines.append(" }") lines.append(" default: {") lines.append(" static const std::vector empty;") lines.append(" return empty;") lines.append(" }") 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_array_kind(self): arrs = [n for n, _ in self.arr_types] if not arrs: return " bool isArrayKind(Kind) const { return false; }" cases = " ".join(f"case Kind::{n}:" for n in arrs) return ( " bool isArrayKind(Kind k) const {\n" f" switch (k) {{ {cases} return true; default: return false; }}\n" " }" ) def _append_null(self): lines = [ " void appendNull(Frame &f) {", " switch (f.kind) {", ] 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(" v->emplace_back();") lines.append(" return;") lines.append(" }") lines.append(" default: return;") lines.append(" }") lines.append(" }") return "\n".join(lines) def _enum_name_arrays(self): out = [] for e in self.b.enums.values(): lits = ", ".join('"' + _escape_cpp_string(v) + '"' 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()) {") if self.root_nullable: lines.append(" result_.emplace();") dest = "&*result_" else: dest = "&result_" lines.append(f" stack_.push_back(Frame{{{root_kind}, {dest}}});") if event_cat == "Obj": lines.append(" {") lines.append(f" int n = fieldCount({root_kind});") lines.append(" stack_.back().seen.assign((n + 63) / 64, 0);") lines.append(" }") lines.append(" return;") lines.append(" }") else: lines.append(" if (stack_.empty()) { reject(); return; }") lines.append(" Frame &f = stack_.back();") lines.append(" SlotInfo si = slotInfoG(f);") 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});") lines.append(" if (isObjectKind(si.child)) {") lines.append(" int n = fieldCount(si.child);") lines.append(" stack_.back().seen.assign((n + 63) / 64, 0);") lines.append(" }") 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) null_at_root = ( "done_ = true; return;" if self.root_nullable else "reject(); return;" ) return f""" class RootBuilder {{ public: explicit RootBuilder(int stackSize = 1024) {{ cb_ = makeCallbacks(); parser_ = WeaselJsonParser_create(stackSize, &cb_, this, 0); if (!parser_) {{ error_ = true; return; }} {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 }}; 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 means waiting for key) std::vector seen; // populated field bitset (object frames) }}; static constexpr int kWantKey = -1; {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; }} // Parse a JSON number text as int64_t. Accepts optional decimal point and // exponent only when the mathematical value is an integer that fits in a // signed 64-bit range. static bool parseJsonInt64(const char *b, const char *e, int64_t &out) {{ const char *p = b; bool neg = false; if (p < e) {{ if (*p == '-') {{ neg = true; ++p; }} else if (*p == '+') return false; }} const char *intStart = p; while (p < e && *p >= '0' && *p <= '9') ++p; const char *intEnd = p; int fracDigits = 0; const char *fracStart = p; if (p < e && *p == '.') {{ ++p; fracStart = p; while (p < e && *p >= '0' && *p <= '9') {{ ++p; ++fracDigits; }} if (fracStart == p) return false; }} int64_t exp = 0; bool expNeg = false; if (p < e && (*p == 'e' || *p == 'E')) {{ ++p; if (p < e && (*p == '-' || *p == '+')) {{ expNeg = (*p == '-'); ++p; }} if (p == e || *p < '0' || *p > '9') return false; while (p < e && *p >= '0' && *p <= '9') {{ int digit = *p - '0'; if (exp <= (INT64_MAX - digit) / 10) exp = exp * 10 + digit; else exp = INT64_MAX; ++p; }} if (expNeg) exp = -exp; }} if (p != e) return false; if (intStart == intEnd) return false; std::string digits; digits.reserve((intEnd - intStart) + fracDigits); for (const char *q = intStart; q < intEnd; ++q) digits.push_back(*q); for (int i = 0; i < fracDigits; ++i) digits.push_back(fracStart[i]); int64_t trim = 0; while (!digits.empty() && digits.back() == '0') {{ digits.pop_back(); ++trim; }} int64_t finalExp = exp - fracDigits + trim; size_t leadingZeros = 0; while (leadingZeros < digits.size() && digits[leadingZeros] == '0') ++leadingZeros; if (leadingZeros == digits.size()) {{ out = 0; return true; }} if (finalExp < 0) return false; if (leadingZeros > 0) digits.erase(0, leadingZeros); constexpr uint64_t kMaxNeg = 9223372036854775808ULL; constexpr uint64_t kMaxPos = 9223372036854775807ULL; const uint64_t limit = neg ? kMaxNeg : kMaxPos; if (finalExp > 19) return false; int64_t maxSig = 19 - finalExp; uint64_t mag = 0; int64_t sigDigits = 0; for (char ch : digits) {{ uint64_t d = static_cast(ch - '0'); if (sigDigits >= maxSig) return false; if (mag > (limit - d) / 10) return false; mag = mag * 10 + d; ++sigDigits; }} for (int64_t i = 0; i < finalExp; ++i) {{ if (mag > limit / 10) return false; mag *= 10; }} if (mag > limit) return false; if (neg) {{ if (mag == kMaxNeg) {{ out = INT64_MIN; }} else {{ out = -static_cast(mag); }} }} else {{ out = static_cast(mag); }} return true; }} // Wrap the generated slotInfo() with the generic key state. SlotInfo slotInfoG(const Frame &f) {{ if (isObjectKind(f.kind)) {{ 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[p.field >> 6] |= (1ull << (p.field & 63)); p.field = kWantKey; }} }} void cbBeginObject() {{ {begin_obj} }} void cbEndObject() {{ if (error_) return; Frame &f = stack_.back(); if (isObjectKind(f.kind)) {{ const auto &req = requiredMask(f.kind); bool missing = false; for (size_t i = 0; i < req.size(); ++i) {{ if ((f.seen[i] & req[i]) != req[i]) {{ missing = true; break; }} }} if (missing) {{ reject(); return; }} }} stack_.pop_back(); valueComplete(); }} void cbBeginArray() {{ {begin_arr} }} void cbEndArray() {{ if (error_) return; Frame f = stack_.back(); 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_); if (idx < 0) {{ reject(); return; // unknown key }} scratch_.clear(); if (f.seen[idx >> 6] & (1ull << (idx & 63))) {{ reject(); return; // duplicate key }} f.field = idx; }} void cbStringData(const char *buf, int len, int done) {{ if (error_) return; if (stack_.empty()) {{ reject(); return; }} Frame &f = stack_.back(); SlotInfo si = slotInfoG(f); 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; if (stack_.empty()) {{ reject(); return; }} Frame &f = stack_.back(); SlotInfo si = slotInfoG(f); 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) {{ *(int64_t *)p = v; // plain integer literal: parsed exactly }} else {{ // JSON Schema "integer" accepts any number with no fractional part, // including exponent/decimal forms like 1e3 or 2.0. Parse those // exactly as int64 when the value is integral and in range. int64_t v2 = 0; if (!parseJsonInt64(b, e, v2)) {{ reject(); return; }} *(int64_t *)p = v2; }} }} 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; if (stack_.empty()) {{ reject(); return; }} Frame &f = stack_.back(); SlotInfo si = slotInfoG(f); if (si.cat != Cat::Bool) {{ reject(); return; }} *(bool *)engage(f, true) = value; valueComplete(); }} {self._is_array_kind()} {self._append_null()} void cbNull() {{ if (error_) return; if (stack_.empty()) {{ {null_at_root} }} Frame &f = stack_.back(); SlotInfo si = slotInfoG(f); if (!si.nullable) {{ reject(); return; }} if (isArrayKind(f.kind)) appendNull(f); // keep null array elements 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._fieldcount()} {self._reqmask()} {self._is_object_kind()} }}; """ 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:]))