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
Reviewed-on: #58
1321 lines
43 KiB
Python
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:]))
|