Files
vdm/contracts/codegen/gen_cpp.py
T
samiandClaude Sonnet 5 5e3e21543a proto: give the generated C++ Dispatcher a real error channel (P1, 1.4.0)
DAEMON's daemon/docs/proto-requests-m1.md P1: velox::proto::Dispatcher's
on_* methods returned Result<T> = expected<T, ParseError>, and dispatch()
mapped every handler error to -32603 InternalError. A handler had no way to
return -32010 (download.get not-found), -32011 (download.add invalid-path)
or -32013 (probe-failed) with their data payloads -- three error fixtures a
conformant server must satisfy were unreachable, blocking DAEMON's
"conformance as a server" M1 DoD.

Two error channels now, kept separate on purpose:
  - parse: Result<T> / ParseError -- dispatch() failing to turn the wire into
    typed params. Always -32602, always structural.
  - handler: HandlerResult<T> / HandlerError -- a handler deciding the request
    can't be fulfilled. Carries any ErrorCode + message + free-form data.

    struct HandlerError {
        ErrorCode code{ErrorCode::InternalError};  // bare {} is a valid -32603
        std::string message;
        nlohmann::json data = nullptr;             // straight into the error's data
    };
    template <class T> using HandlerResult = std::expected<T, HandlerError>;

dispatch()'s handler branch is now
  make_error(id, r.error().code, r.error().message, r.error().data)
instead of a hard-coded InternalError. -32001/-32002/-32003 stay the server
layer's to raise around dispatch(), as DAEMON already does.

Verified end to end against the real dispatch() path: a handler returning
TaskNotFound/InvalidPath/ProbeFailed produces -32010/-32011/-32013 with the
data object intact, and a bare HandlerError{} still yields a clean -32603
with no data field. The `= nullptr` on the member (not `{nullptr}`) matters:
brace-init of nlohmann::json from nullptr is the array [null], not JSON null.

FixtureDispatcher regenerated to HandlerResult; conformance_main.cpp only
inspects dispatch()'s JSON and needed no change. TS side is untouched beyond
the version string -- no server Dispatcher is generated there.

P2 also handled: session.hello.version-mismatch's data.expected was a stale
"1.0.0"; now $any, with a note that the error-fixture compare is on `code`
only so a server echoing kProtocolVersion there is fine.

Version: minor, 1.3.0 -> 1.4.0. Wire is byte-identical (no schema, fixture,
or OpenRPC change) but every Dispatcher implementer must swap Result ->
HandlerResult on regen, and the bump is how lanes are told to. Not an ADR:
one lane consumes this binding, it's the one that asked, and the shape is
the one they proposed. Answered in contracts/proto-answers-daemon-m1.md.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_012fgjnqFCS5h5L7gZTZo3rV
2026-09-10 15:10:13 +04:00

632 lines
29 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.
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())