Files
vdm/contracts/codegen/gen_cpp.py
T
samiandClaude Sonnet 5 4e177ec809 proto: stop conformance from downloading real files, ADR the null-clearing gap
1. download.add.json had startMode "now" against a real, large (~6 GB)
   Ubuntu ISO with saveDir hardcoded to /home/sami/Downloads/Programs.
   Against a real veloxd (tests/conformance/run.sh) that's a real
   download into the real user's real home, every single run — it had
   already happened twice. startMode -> "later" (exercises the add path,
   hands nothing to the engine) and saveDir is dropped entirely (resolves
   to saveTo.defaultDir instead, checked against allowedRoots the same
   way). Documented the rule this fixture was breaking in
   contracts/fixtures/README.md so it doesn't happen a third time.

   Auditing the rest for the same shape (now real: capture.offer, D7)
   found a second, subtler instance: capture.offer.take.json's "take"
   admits a real, immediately-started task the same way download.add
   does, and the "Programs" category's saveDir is a migration-seeded
   builtin (~/Downloads/Programs) that no isolated test setup can
   redirect -- so even after pointing the URL at example.org (RFC 2606),
   a real ~6 GB sparse .veloxpart still landed in the real home on the
   declared Content-Length alone. Shrunk to a plausible-but-small 5 MiB.
   Also scoped to "transport": "uds" -- a real "take" persists an active
   task, so replaying the same fixture again on the second live transport
   against the same shared daemon was hitting capture.offer's own
   dedupe-by-URL and failing on a missing taskId, not a bug.
   download.add's other real-URL siblings (errors/*.invalid-path,
   *.invalid-params, *.disk-full) all fail before admission or are
   requires-gated; left alone.

2. ADR 0018: DAEMON can set a nullable field through download.update /
   settings.set but never clear it back to null, because the generated
   C++ parser collapses "absent" and "explicit null" to the same
   std::nullopt for every optional field (contracts/codegen/gen_cpp.py,
   on purpose, and correct for create-style params -- just wrong for
   patch-style ones, which is the only place the schema documents
   "explicit null clears"). Decision: an opt-in x-clearable schema
   annotation makes just those fields std::optional<std::optional<T>> in
   C++ (TS already round-trips this natively); not a blanket rule
   (would retype response fields like TaskSummary.effectiveUrl that have
   no clear-vs-absent distinction to make), not an explicit clear-list
   field (would redesign a wire contract DAEMON already built against
   just to route around a generator gap). Recorded, not implemented here
   -- that's its own PROTO PR (schema annotations + gen_cpp.py + gen_ts.py
   + regeneration + a minor VERSION bump per ADR 0015), not bundled into
   a fixture-safety pass. Left a pointer to the ADR at the generator
   comment it concerns.

3. Re-verified every xfail entry against current deferrals.md rather
   than trust the reasons already on file: D7/D8 (capture.offer/
   getRules), D3d/e/f/g/h/i (rules, queue.reorder, schedule, limiter,
   download.update/refreshUrl) and D9 (settings) have all closed since
   the list was last pruned, so most of it was stale. Removed everything
   that now cleanly passes; kept and re-reasoned everything that doesn't:
   - errors/download.provideAuth.not-found.json stays, as asked: real
     bug, on_download_provideAuth never checks the task exists.
   - category.list.json (mimeTypes -- documented D3a gap), schedule.set.json
     (nextRunAt -- documented D3f gap): unchanged in substance, reason
     text was already accurate.
   - download.probe/get/list/update.json, session.hello.json,
     queue.start/reorder.json, category.remove.json: not bugs -- each
     golden depicts a richer lifecycle/config state (a probed download,
     real queue or category membership, media/grabber capabilities) than
     this harness's fresh, never-started bound tasks and empty isolated
     DB can produce.
   - limiter.get.json: real fixture bug, not a daemon one -- applyToRunning
     is a write-only instruction on limiter.set, on_limiter_get never
     returns it; the golden shouldn't have had it either. Fixed the
     fixture and tools/mockd's own limiter.get, which had the same field
     hardcoded into its in-memory state independent of the fixture file.
   - grabber.*/media.*: still genuinely stub (M4 territory).
   Only remaining unexpected-pass surfaced while re-verifying
   (errors/capture.offer.ignore.json, always "take" instead of "ignore")
   traced to capture.minSizeBytes defaulting to 0 on a fresh daemon,
   making its below-minimum-size scenario unreachable -- not a bug, so
   raised the setting in run.sh's isolated seeding instead of xfailing it.

ctest -L conformance: green, 100% (2/2), ~87s.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SFeUKLbdHizrJjLBeK7ffz
2026-09-12 16:56:35 +04:00

637 lines
30 KiB
Python

#!/usr/bin/env python3
"""Emit core/generated/velox_proto.{hpp,cpp} from contracts/schema/.
Design notes that matter to the CORE and DAEMON lanes:
* **No exceptions.** nlohmann's own throwing `get<T>()` / ADL `from_json` are deliberately
not used and not emitted. Parsing goes through `velox::proto::parse<T>(json)` which
returns `std::expected<T, ParseError>`, so a malformed frame from the wire is an ordinary
value the RPC loop handles, not a throw unwinding through the transfer path.
* **Two error channels, kept separate.** `parse<T>() -> Result<T>` (i.e. `expected<T,
ParseError>`) is the wire failing to become typed params — always `-32602`, always
structural. A `Dispatcher::on_*` handler returns `HandlerResult<T>` (i.e. `expected<T,
HandlerError>`), which carries any contract error code plus a free-form `data` object,
so a handler can answer `-32010` `{taskId}`, `-32011` `{path}`, `-32013` `{httpStatus}`
and so on. `dispatch()` forwards the handler's code/message/data straight into the
JSON-RPC error object.
* **Serialisation is ADL `to_json`,** so `nlohmann::json j = task;` works as expected.
Only the outbound direction is allowed to be implicit; the wire is never trusted.
* **`libveloxproto`, not `libveloxcore`.** This code includes nlohmann/json, which
CLAUDE.md forbids inside core. It is a separate target that core and daemon both link;
the layering rule constrains `libveloxcore`, and `core/generated/` is not part of it.
See docs/adr/0009-generated-protocol-library.md.
Run: python3 contracts/codegen/gen_cpp.py
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
from schema_ir import Contract, Field, TypeDef, TypeRef, load, pascal, topo_sorted # noqa: E402
OUT_DIR = Path(__file__).resolve().parent.parent.parent / "core" / "generated"
BANNER = """// ---------------------------------------------------------------------------
// GENERATED FILE — DO NOT EDIT.
//
// Source: contracts/schema/**
// Generator: contracts/codegen/gen_cpp.py
// Contract: v{version}
//
// Hand-editing this file is a merge blocker. Fix the schema and regenerate:
// python3 contracts/codegen/gen_cpp.py
// Only lane PROTO commits to contracts/.
// ---------------------------------------------------------------------------
"""
def cpp_type(ref: TypeRef) -> str:
if ref.kind == "named":
return ref.name or "void"
if ref.kind == "string":
return "std::string"
if ref.kind == "integer":
return "std::int64_t"
if ref.kind == "number":
return "double"
if ref.kind == "boolean":
return "bool"
if ref.kind == "json":
return "nlohmann::json"
if ref.kind == "array":
return f"std::vector<{cpp_type(ref.inner)}>"
if ref.kind == "map":
return f"std::map<std::string, {cpp_type(ref.inner)}>"
raise AssertionError(ref.kind)
def cpp_constraints(ref: TypeRef, var: str, path: str, indent: str) -> list[str]:
"""Range, length and pattern checks. The wire is untrusted: a `maximum` in the schema
has to be a check here, or -32602 would never fire for an out-of-range value."""
lim = ref.limits
if not lim:
return []
i = indent
o: list[str] = []
def bail(msg: str) -> str:
return f'return std::unexpected(ParseError{{std::string({path}), "{msg}"}});'
if ref.kind in ("integer", "number"):
if "minimum" in lim:
o.append(f'{i}if ({var} < {lim["minimum"]}) {bail("value is below the minimum of " + str(lim["minimum"]))}')
if "maximum" in lim:
o.append(f'{i}if ({var} > {lim["maximum"]}) {bail("value is above the maximum of " + str(lim["maximum"]))}')
elif ref.kind == "string":
if "minLength" in lim:
o.append(f'{i}if ({var}.size() < {lim["minLength"]}u) {bail("value is shorter than " + str(lim["minLength"]) + " characters")}')
if "maxLength" in lim:
o.append(f'{i}if ({var}.size() > {lim["maxLength"]}u) {bail("value is longer than " + str(lim["maxLength"]) + " characters")}')
if "pattern" in lim:
lit = json.dumps(lim["pattern"])
o.append(f"{i}{{")
o.append(f"{i} static const std::regex re({lit}, std::regex::ECMAScript);")
o.append(f'{i} if (!std::regex_match({var}, re)) {bail("value does not match the required pattern")}')
o.append(f"{i}}}")
elif ref.kind == "array":
if "minItems" in lim:
o.append(f'{i}if ({var}.size() < {lim["minItems"]}u) {bail("fewer than " + str(lim["minItems"]) + " items")}')
if "maxItems" in lim:
o.append(f'{i}if ({var}.size() > {lim["maxItems"]}u) {bail("more than " + str(lim["maxItems"]) + " items")}')
return o
def field_type(f: Field) -> str:
inner = cpp_type(f.type)
return f"std::optional<{inner}>" if f.optional else inner
def ident(name: str) -> str:
"""A JSON property name as a C++ member name."""
out = name.replace(".", "_").replace("-", "_")
reserved = {"class", "delete", "namespace", "operator", "template", "new", "auto"}
return out + "_" if out in reserved else out
def doc_comment(text: str, indent: str = "") -> list[str]:
if not text:
return []
lines: list[str] = []
words = text.split()
cur = ""
for w in words:
if len(cur) + len(w) + 1 > 92:
lines.append(cur)
cur = w
else:
cur = f"{cur} {w}".strip()
if cur:
lines.append(cur)
return [f"{indent}/// {ln}" for ln in lines]
def method_ident(name: str) -> str:
return pascal(name)
def handler_name(name: str) -> str:
return "on_" + name.replace(".", "_")
# ------------------------------------------------------------------ header
def emit_header(c: Contract) -> str:
o: list[str] = [BANNER.format(version=c.version), "#pragma once", ""]
o += [
"#include <cstdint>",
"#include <expected>",
"#include <map>",
"#include <optional>",
"#include <string>",
"#include <string_view>",
"#include <vector>",
"",
"#include <nlohmann/json.hpp>",
"",
"// This is libveloxproto, NOT libveloxcore. The layering rule in CLAUDE.md forbids",
"// JSON inside the engine; the engine does not link this target. See",
"// docs/adr/0009-generated-protocol-library.md.",
"namespace velox::proto {",
"",
f'inline constexpr std::string_view kProtocolVersion = "{c.version}";',
"",
"/// Why a payload could not be turned into a typed value. `path` is a JSON Pointer",
"/// into the offending document, so a conformance failure names the exact field.",
"struct ParseError {",
" std::string path;",
" std::string message;",
"};",
"",
"/// Every parse in this file returns one of these. Nothing here throws.",
"template <class T>",
"using Result = std::expected<T, ParseError>;",
"",
"/// Which listener a request arrived on. Decides whether a privileged method is",
"/// allowed: see `is_allowed_on`.",
"enum class Transport { Uds, Ws };",
"",
]
# ---- enums
for t in topo_sorted(c.types):
if t.kind == "string_enum":
o += doc_comment(t.doc)
o.append(f"enum class {t.name} {{")
for v in t.values:
o.append(f" {v.name}, // \"{v.wire}\"")
o.append("};")
o.append(f"std::string_view to_string({t.name} v) noexcept;")
o.append(f"Result<{t.name}> parse_{t.name}(std::string_view s);")
o.append("")
elif t.kind == "int_enum":
o += doc_comment(t.doc)
o.append(f"enum class {t.name} : std::int32_t {{")
for v in t.values:
o += doc_comment(v.doc, " ")
o.append(f" {v.name} = {v.wire},")
o.append("};")
o.append(f"std::string_view to_string({t.name} v) noexcept;")
o.append(f"std::optional<{t.name}> {t.name.lower()}_from_int(std::int32_t v) noexcept;")
o.append("")
elif t.kind == "map_alias":
o += doc_comment(t.doc)
o.append(f"using {t.name} = {cpp_type(t.alias)};")
o.append("")
# ---- structs
for t in topo_sorted(c.types):
if t.kind != "struct":
continue
o += doc_comment(t.doc)
o.append(f"struct {t.name} {{")
if not t.fields:
o.append(" // No fields: this method takes no parameters.")
for f in t.fields:
o += doc_comment(f.doc, " ")
o.append(f" {field_type(f)} {ident(f.name)}{{}};")
o.append("};")
o.append("")
o += [
"// --- serialisation ---------------------------------------------------------",
"// ADL hooks, so `nlohmann::json j = value;` works. Outbound only: there is no",
"// generated from_json, because nlohmann's inbound path throws and the wire is",
"// never trusted. Use parse<T> below.",
"",
]
for t in topo_sorted(c.types):
if t.kind in ("struct", "string_enum", "int_enum"):
o.append(f"void to_json(nlohmann::json& j, const {t.name}& v);")
o.append("")
o += [
"// --- parsing ---------------------------------------------------------------",
"",
"/// Turn an untrusted JSON value into a typed one. Specialised below for every",
"/// contract type; the primary template is intentionally not defined, so asking",
"/// for a type the contract does not have is a compile error, not a runtime one.",
"template <class T>",
"Result<T> parse(const nlohmann::json& j, std::string_view path = \"\");",
"",
]
for t in topo_sorted(c.types):
if t.kind in ("struct", "string_enum", "int_enum", "map_alias"):
o.append(f"template <> Result<{t.name}> parse<{t.name}>(const nlohmann::json& j, std::string_view path);")
o.append("")
# ---- method / event enums
o += [
"// --- method surface --------------------------------------------------------",
"",
"/// Every method in the contract. Generated, so a daemon cannot answer a method",
"/// the contract does not define, and cannot silently fail to answer one it does.",
"enum class Method {",
]
for m in c.methods:
o.append(f" {method_ident(m.name)}, // {m.name}")
o += ["};", "",
"inline constexpr std::size_t kMethodCount = " + str(len(c.methods)) + ";",
"",
"std::string_view to_string(Method m) noexcept;",
"std::optional<Method> method_from_string(std::string_view s) noexcept;",
"",
"/// True for methods refused over the WebSocket transport with -32003. The",
"/// extension is not allowed to reconfigure the daemon or destroy user data.",
"bool is_privileged(Method m) noexcept;",
"bool is_allowed_on(Method m, Transport t) noexcept;",
"",
"/// The contract's answer deadline. capture.offer's 750 ms is the one that",
"/// matters: past it the extension has already let Firefox take the download.",
"std::int32_t deadline_ms(Method m) noexcept;",
""]
o += ["/// Server-to-client notifications.", "enum class Event {"]
for e in c.events:
o.append(f" {pascal(e.name[len('event.'):])}, // {e.name}")
o += ["};", "",
"std::string_view to_string(Event e) noexcept;",
"std::optional<Event> event_from_string(std::string_view s) noexcept;",
"",
"// --- dispatch --------------------------------------------------------------",
"",
"/// Build a JSON-RPC error response. `id` may be null for a request that could",
"/// not be parsed far enough to have one.",
"nlohmann::json make_error(const nlohmann::json& id, ErrorCode code, std::string_view message,",
" nlohmann::json data = nullptr);",
"nlohmann::json make_result(const nlohmann::json& id, nlohmann::json result);",
"nlohmann::json make_notification(Event e, nlohmann::json params);",
"",
"/// A handler's own failure — as opposed to ParseError, which is the wire failing",
"/// to become typed params. Carries any contract error code, a message, and a",
"/// free-form `data` object that goes straight into the JSON-RPC error's `data`",
"/// field: `{\"taskId\": ...}` for TaskNotFound, `{\"path\": ...}` for InvalidPath,",
"/// `{\"httpStatus\": ...}` for ProbeFailed. `code` defaults to InternalError so a",
"/// handler that sets only a message still produces a valid error response.",
"///",
"/// -32001/-32002/-32003 are the server layer's to raise around dispatch(), not a",
"/// handler's: they are decided before or without reference to method params.",
"struct HandlerError {",
" ErrorCode code{ErrorCode::InternalError};",
" std::string message;",
" // `= nullptr`, not `{nullptr}`: brace-init of nlohmann::json from nullptr",
" // yields the array [null], not JSON null. make_error() drops a null data.",
" nlohmann::json data = nullptr;",
"};",
"",
"template <class T>",
"using HandlerResult = std::expected<T, HandlerError>;",
"",
"/// One virtual per method. The daemon implements this; `dispatch` below does the",
"/// envelope handling, the transport check and the parameter parsing, so a handler",
"/// only ever sees a validated, typed params struct. Return `std::unexpected(",
"/// HandlerError{...})` to answer with a specific error code and data.",
"class Dispatcher {",
"public:",
" virtual ~Dispatcher() = default;",
""]
for m in c.methods:
o += doc_comment(m.doc, " ")
o.append(f" virtual HandlerResult<{cpp_type(m.result)}> {handler_name(m.name)}(const {cpp_type(m.params)}& params) = 0;")
o.append("")
o += ["};", "",
"/// Parse one JSON-RPC request, route it, and return the response to write back.",
"/// Never throws. Returns a null json for a notification that needs no reply.",
"nlohmann::json dispatch(Dispatcher& handler, Transport transport, const nlohmann::json& request);",
"",
"} // namespace velox::proto",
""]
return "\n".join(o)
# ------------------------------------------------------------------ source
def emit_source(c: Contract) -> str:
o: list[str] = [BANNER.format(version=c.version),
'#include "velox_proto.hpp"', "", "#include <algorithm>", "#include <regex>", "",
"namespace velox::proto {", "", "namespace {", "",
'std::string join(std::string_view path, std::string_view key) {',
' std::string out(path);',
' out += "/";',
' out += key;',
' return out;',
'}', "", "} // namespace", ""]
# enum conversions
for t in topo_sorted(c.types):
if t.kind == "string_enum":
o.append(f"std::string_view to_string({t.name} v) noexcept {{")
o.append(" switch (v) {")
for v in t.values:
o.append(f' case {t.name}::{v.name}: return "{v.wire}";')
o.append(" }")
o.append(' return "";')
o.append("}")
o.append("")
o.append(f"Result<{t.name}> parse_{t.name}(std::string_view s) {{")
for v in t.values:
o.append(f' if (s == "{v.wire}") return {t.name}::{v.name};')
o.append(f' return std::unexpected(ParseError{{"", "not a valid {t.name}: \'" + std::string(s) + "\'"}});')
o.append("}")
o.append("")
o.append(f"void to_json(nlohmann::json& j, const {t.name}& v) {{ j = to_string(v); }}")
o.append("")
o.append(f"template <> Result<{t.name}> parse<{t.name}>(const nlohmann::json& j, std::string_view path) {{")
o.append(' if (!j.is_string()) return std::unexpected(ParseError{std::string(path), "expected a string"});')
o.append(f" auto r = parse_{t.name}(j.get_ref<const std::string&>());")
o.append(" if (!r) return std::unexpected(ParseError{std::string(path), r.error().message});")
o.append(" return *r;")
o.append("}")
o.append("")
elif t.kind == "int_enum":
o.append(f"std::string_view to_string({t.name} v) noexcept {{")
o.append(" switch (v) {")
for v in t.values:
o.append(f' case {t.name}::{v.name}: return "{v.name}";')
o.append(" }")
o.append(' return "";')
o.append("}")
o.append("")
o.append(f"std::optional<{t.name}> {t.name.lower()}_from_int(std::int32_t v) noexcept {{")
o.append(" switch (v) {")
for v in t.values:
o.append(f" case {v.wire}: return {t.name}::{v.name};")
o.append(" default: return std::nullopt;")
o.append(" }")
o.append("}")
o.append("")
o.append(f"void to_json(nlohmann::json& j, const {t.name}& v) {{ j = static_cast<std::int32_t>(v); }}")
o.append("")
o.append(f"template <> Result<{t.name}> parse<{t.name}>(const nlohmann::json& j, std::string_view path) {{")
o.append(' if (!j.is_number_integer()) return std::unexpected(ParseError{std::string(path), "expected an integer"});')
o.append(f" auto v = {t.name.lower()}_from_int(j.get<std::int32_t>());")
o.append(' if (!v) return std::unexpected(ParseError{std::string(path), "not a contract error code"});')
o.append(" return *v;")
o.append("}")
o.append("")
# map aliases: a parse specialisation only. The alias is a std::map, which nlohmann
# already serialises, so an emitted to_json here would be an ambiguous overload.
for t in topo_sorted(c.types):
if t.kind != "map_alias":
continue
o.append(f"template <> Result<{t.name}> parse<{t.name}>(const nlohmann::json& j, std::string_view path) {{")
o += emit_value_parse(t.alias, "j", "out", "path", " ")
o.append(" return out;")
o.append("}")
o.append("")
# struct to_json / parse
for t in topo_sorted(c.types):
if t.kind != "struct":
continue
# A no-parameter method's struct has nothing to read, so the parameter goes
# unnamed: the project builds with -Wall -Wextra -Werror.
vname = "v" if t.fields else "/*v*/"
o.append(f"void to_json(nlohmann::json& j, const {t.name}& {vname}) {{")
o.append(" j = nlohmann::json::object();")
for f in t.fields:
m = ident(f.name)
if f.optional:
o.append(f' if (v.{m}.has_value()) j["{f.name}"] = *v.{m};')
if f.required:
o.append(f' else j["{f.name}"] = nullptr;')
else:
o.append(f' j["{f.name}"] = v.{m};')
o.append("}")
o.append("")
pname = "path" if t.fields else "/*path*/"
o.append(f"template <> Result<{t.name}> parse<{t.name}>(const nlohmann::json& j, std::string_view {pname}) {{")
if t.fields:
o.append(' if (!j.is_object()) return std::unexpected(ParseError{std::string(path), "expected an object"});')
else:
o.append(' if (!j.is_object()) return std::unexpected(ParseError{"", "expected an object"});')
o.append(f" {t.name} out;")
for f in t.fields:
o += emit_field_parse(f, indent=" ")
o.append(" return out;")
o.append("}")
o.append("")
o += emit_tables(c)
o += emit_dispatch(c)
o += ["} // namespace velox::proto", ""]
return "\n".join(o)
def emit_value_parse(ref: TypeRef, src: str, dst: str, path: str, indent: str) -> list[str]:
"""Statements that parse json expression `src` into a fresh variable named `dst`."""
i = indent
o: list[str] = []
if ref.kind == "named":
o.append(f"{i}auto {dst}_r = parse<{ref.name}>({src}, {path});")
o.append(f"{i}if (!{dst}_r) return std::unexpected({dst}_r.error());")
o.append(f"{i}auto {dst} = std::move(*{dst}_r);")
elif ref.kind == "string":
o.append(f"{i}if (!{src}.is_string()) return std::unexpected(ParseError{{std::string({path}), \"expected a string\"}});")
o.append(f"{i}auto {dst} = {src}.get<std::string>();")
elif ref.kind == "integer":
o.append(f"{i}if (!{src}.is_number_integer()) return std::unexpected(ParseError{{std::string({path}), \"expected an integer\"}});")
o.append(f"{i}auto {dst} = {src}.get<std::int64_t>();")
elif ref.kind == "number":
o.append(f"{i}if (!{src}.is_number()) return std::unexpected(ParseError{{std::string({path}), \"expected a number\"}});")
o.append(f"{i}auto {dst} = {src}.get<double>();")
elif ref.kind == "boolean":
o.append(f"{i}if (!{src}.is_boolean()) return std::unexpected(ParseError{{std::string({path}), \"expected a boolean\"}});")
o.append(f"{i}auto {dst} = {src}.get<bool>();")
elif ref.kind == "json":
o.append(f"{i}auto {dst} = {src};")
elif ref.kind == "array":
et = cpp_type(ref.inner)
o.append(f"{i}if (!{src}.is_array()) return std::unexpected(ParseError{{std::string({path}), \"expected an array\"}});")
o.append(f"{i}std::vector<{et}> {dst};")
o.append(f"{i}{dst}.reserve({src}.size());")
o.append(f"{i}for (std::size_t idx = 0; idx < {src}.size(); ++idx) {{")
o.append(f'{i} const std::string ip = join({path}, std::to_string(idx));')
o += emit_value_parse(ref.inner, f"{src}[idx]", f"{dst}_e", "ip", i + " ")
o.append(f"{i} {dst}.push_back(std::move({dst}_e));")
o.append(f"{i}}}")
elif ref.kind == "map":
et = cpp_type(ref.inner)
o.append(f"{i}if (!{src}.is_object()) return std::unexpected(ParseError{{std::string({path}), \"expected an object\"}});")
o.append(f"{i}std::map<std::string, {et}> {dst};")
o.append(f"{i}for (const auto& [mk, mv] : {src}.items()) {{")
o.append(f'{i} const std::string mp = join({path}, mk);')
o += emit_value_parse(ref.inner, "mv", f"{dst}_e", "mp", i + " ")
o.append(f"{i} {dst}.emplace(mk, std::move({dst}_e));")
o.append(f"{i}}}")
else:
raise AssertionError(ref.kind)
o += cpp_constraints(ref, dst, path, indent)
return o
def emit_field_parse(f: Field, indent: str) -> list[str]:
i = indent
m = ident(f.name)
o = [f'{i}{{',
f'{i} const std::string fp = join(path, "{f.name}");',
f'{i} const auto it = j.find("{f.name}");']
if f.optional:
# Absent and null mean the same thing: the field is not set. A client that omits
# a nullable field and one that sends null are treated identically on purpose --
# correct for create-style params, where there is no existing value to distinguish
# "never set" from "explicitly cleared". Patch-style fields need the distinction
# (download.update's patch: "an explicit null clears a nullable field") and get an
# opt-in exception via x-clearable per ADR 0018 (not implemented yet: this is the
# decision record, not the generator change).
o.append(f"{i} if (it != j.end() && !it->is_null()) {{")
o += emit_value_parse(f.type, "(*it)", "val", "fp", i + " ")
o.append(f"{i} out.{m} = std::move(val);")
o.append(f"{i} }}")
else:
o.append(f"{i} if (it == j.end() || it->is_null())")
o.append(f'{i} return std::unexpected(ParseError{{fp, "required field is missing"}});')
o += emit_value_parse(f.type, "(*it)", "val", "fp", i + " ")
o.append(f"{i} out.{m} = std::move(val);")
o.append(f"{i}}}")
return o
def emit_tables(c: Contract) -> list[str]:
o = ["std::string_view to_string(Method m) noexcept {", " switch (m) {"]
for m in c.methods:
o.append(f' case Method::{method_ident(m.name)}: return "{m.name}";')
o += [" }", ' return "";', "}", ""]
o += ["std::optional<Method> method_from_string(std::string_view s) noexcept {"]
for m in c.methods:
o.append(f' if (s == "{m.name}") return Method::{method_ident(m.name)};')
o += [" return std::nullopt;", "}", ""]
o += ["bool is_privileged(Method m) noexcept {", " switch (m) {"]
for m in c.methods:
o.append(f" case Method::{method_ident(m.name)}: return {str(m.privileged).lower()};")
o += [" }", " return true; // unknown means refuse", "}", ""]
o += ["bool is_allowed_on(Method m, Transport t) noexcept {", " switch (m) {"]
for m in c.methods:
uds = "true" if "uds" in m.transports else "false"
ws = "true" if "ws" in m.transports else "false"
o.append(f" case Method::{method_ident(m.name)}: return t == Transport::Uds ? {uds} : {ws};")
o += [" }", " return false;", "}", ""]
o += ["std::int32_t deadline_ms(Method m) noexcept {", " switch (m) {"]
for m in c.methods:
o.append(f" case Method::{method_ident(m.name)}: return {m.deadline_ms};")
o += [" }", " return 5000;", "}", ""]
o += ["std::string_view to_string(Event e) noexcept {", " switch (e) {"]
for e in c.events:
o.append(f' case Event::{pascal(e.name[len("event."):])}: return "{e.name}";')
o += [" }", ' return "";', "}", ""]
o += ["std::optional<Event> event_from_string(std::string_view s) noexcept {"]
for e in c.events:
o.append(f' if (s == "{e.name}") return Event::{pascal(e.name[len("event."):])};')
o += [" return std::nullopt;", "}", ""]
return o
def emit_dispatch(c: Contract) -> list[str]:
o = [
"nlohmann::json make_error(const nlohmann::json& id, ErrorCode code, std::string_view message,",
" nlohmann::json data) {",
" nlohmann::json err = {{\"code\", static_cast<std::int32_t>(code)}, {\"message\", std::string(message)}};",
" if (!data.is_null()) err[\"data\"] = std::move(data);",
" return {{\"jsonrpc\", \"2.0\"}, {\"id\", id}, {\"error\", std::move(err)}};",
"}",
"",
"nlohmann::json make_result(const nlohmann::json& id, nlohmann::json result) {",
" return {{\"jsonrpc\", \"2.0\"}, {\"id\", id}, {\"result\", std::move(result)}};",
"}",
"",
"nlohmann::json make_notification(Event e, nlohmann::json params) {",
" return {{\"jsonrpc\", \"2.0\"}, {\"method\", std::string(to_string(e))}, {\"params\", std::move(params)}};",
"}",
"",
"nlohmann::json dispatch(Dispatcher& handler, Transport transport, const nlohmann::json& request) {",
" const nlohmann::json id = request.contains(\"id\") ? request.at(\"id\") : nlohmann::json(nullptr);",
"",
" if (!request.is_object() || request.value(\"jsonrpc\", \"\") != \"2.0\" || !request.contains(\"method\"))",
" return make_error(id, ErrorCode::InvalidRequest, \"not a JSON-RPC 2.0 request\");",
" if (!request.at(\"method\").is_string())",
" return make_error(id, ErrorCode::InvalidRequest, \"method must be a string\");",
"",
" const auto method = method_from_string(request.at(\"method\").get_ref<const std::string&>());",
" if (!method)",
" return make_error(id, ErrorCode::MethodNotFound, \"no such method\");",
" if (!is_allowed_on(*method, transport))",
" return make_error(id, ErrorCode::TransportForbidden,",
" \"method is not permitted on this transport\");",
"",
" const nlohmann::json params =",
" request.contains(\"params\") ? request.at(\"params\") : nlohmann::json::object();",
"",
" switch (*method) {",
]
for m in c.methods:
pt, rt = cpp_type(m.params), cpp_type(m.result)
o += [
f" case Method::{method_ident(m.name)}: {{",
f' auto p = parse<{pt}>(params, "params");',
" if (!p)",
" return make_error(id, ErrorCode::InvalidParams, p.error().message,",
' nlohmann::json{{"path", p.error().path}});',
f" auto r = handler.{handler_name(m.name)}(*p);",
" if (!r)",
" return make_error(id, r.error().code, r.error().message, r.error().data);",
" nlohmann::json out = *r;",
" return make_result(id, std::move(out));",
" }",
]
o += [" }", "",
" return make_error(id, ErrorCode::MethodNotFound, \"no such method\");",
"}", ""]
return o
def main() -> int:
c = load()
OUT_DIR.mkdir(parents=True, exist_ok=True)
(OUT_DIR / "velox_proto.hpp").write_text(emit_header(c))
(OUT_DIR / "velox_proto.cpp").write_text(emit_source(c))
print(f"gen_cpp: {len(c.types)} types, {len(c.methods)} methods, {len(c.events)} events -> {OUT_DIR}")
return 0
if __name__ == "__main__":
raise SystemExit(main())