#!/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()` / ADL `from_json` are deliberately not used and not emitted. Parsing goes through `velox::proto::parse(json)` which returns `std::expected`, 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" 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 ", "#include ", "#include ", "#include ", "#include ", "#include ", "#include ", "", "#include ", "", "// 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 ", "using Result = std::expected;", "", "/// 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 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 ", "Result 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_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_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 ", "#include ", "", "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());") 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(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());") 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();") 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();") 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();") 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();") 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 {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_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_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(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());", " 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())