Files
andrew c44b0262bc
CI / pre-commit (push) Successful in 53s
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 50s
CI / build (-DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++, clang-amd64, ubuntu-latest-amd64, true) (push) Successful in 1m36s
CI / build (-DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++, gcc-amd64, ubuntu-latest-amd64, false) (push) Successful in 1m34s
Merge pull request 'schemagen: reject absent additionalProperties' (#58) from weaselbot/weaseljson:weaselbot/issue-55 into main
Reviewed-on: #58
2026-07-20 18:06:53 +00:00

1321 lines
43 KiB
Python

#!/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<T> and value members need T complete; unique_ptr<T> 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<Root>;
# 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 <charconv>
#include <cmath>
#include <cstdint>
#include <map>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <vector>
#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<uint64_t> &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<uint64_t> m = {{ {init} }};")
lines.append(" return m;")
lines.append(" }")
lines.append(" default: {")
lines.append(" static const std::vector<uint64_t> 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<uint64_t> seen; // populated field bitset (object frames)
}};
static constexpr int kWantKey = -1;
{self._enum_name_arrays()}
Root result_{{}};
std::vector<Frame> 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<uint64_t>(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<int64_t>(mag);
}}
}} else {{
out = static_cast<int64_t>(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:]))