proto: freeze the wire contract at 1.0.0

Schemas for the whole v1 surface: 38 methods, 9 events, 25 named types and the
JSON-RPC envelope, with x-privileged / x-transports / x-deadlineMs / x-errors
annotations that both generators emit as data rather than prose.

Four generators over one IR (contracts/codegen/schema_ir.py), so the C++ structs,
the TypeScript types and the OpenRPC document cannot disagree about what the
contract says:

  gen_cpp.py             -> core/generated/velox_proto.{hpp,cpp}
  gen_ts.py              -> extension/src/shared/protocol/
  gen_openrpc.py         -> contracts/openrpc.json
  gen_cpp_conformance.py -> tests/conformance/cpp/fixture_dispatcher.hpp

Inbound parsing never throws: parse<T>() returns std::expected<T, ParseError> and
nlohmann's throwing ADL from_json is deliberately not emitted. Schema constraints
(minimum, maxLength, pattern, ...) become real runtime checks in both languages —
the daemon does not trust the extension and the extension does not trust the
daemon.

59 golden fixtures: a success case per method, 12 error cases, 9 events. Replayed
by tests/conformance/ against both the generated C++ and a live server over both
transports. tools/mockd serves the same fixtures with unhappy-path flags so the
GUI and EXT lanes never wait for veloxd.

run.sh also proves capture.offer fails open: with a daemon answering slower than
750 ms the client gives up and lets Firefox take the download.

core/generated/ is libveloxproto, a separate target from libveloxcore, which
still never sees JSON — see docs/adr/0009.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_012fgjnqFCS5h5L7gZTZo3rV
This commit is contained in:
2026-09-09 19:55:54 +04:00
co-authored by Claude Opus 5
parent a40585f419
commit 53421d6cb8
171 changed files with 29275 additions and 51 deletions
+604
View File
@@ -0,0 +1,604 @@
#!/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.
* **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);",
"",
"/// 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.",
"class Dispatcher {",
"public:",
" virtual ~Dispatcher() = default;",
""]
for m in c.methods:
o += doc_comment(m.doc, " ")
o.append(f" virtual Result<{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, ErrorCode::InternalError, r.error().message,",
' nlohmann::json{{"path", r.error().path}});',
" 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())