Files
vdm/contracts/codegen/gen_cpp_conformance.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

87 lines
3.6 KiB
Python

#!/usr/bin/env python3
"""Emit tests/conformance/cpp/fixture_dispatcher.hpp.
velox::proto::Dispatcher has one pure virtual per method, on purpose: a daemon that
forgets to implement a method does not compile. That is exactly what a conformance runner
needs, and also what makes one tedious to hand-write — so it is generated.
The dispatcher answers each method from that method's golden fixture, which lets the C++
side exercise the real dispatch path: envelope handling, the transport check, parameter
parsing, and result serialisation.
Run: python3 contracts/codegen/gen_cpp_conformance.py
"""
from __future__ import annotations
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
from gen_cpp import BANNER, cpp_type, handler_name # noqa: E402
from schema_ir import load # noqa: E402
OUT = Path(__file__).resolve().parent.parent.parent / "tests" / "conformance" / "cpp" / "fixture_dispatcher.hpp"
def main() -> int:
c = load()
o = [BANNER.format(version=c.version), "#pragma once", "",
'#include "velox_proto.hpp"', "",
"#include <functional>", "#include <string>", "", "namespace velox::conformance {", "",
"/// Answers every method from its golden fixture, so the generated dispatch path",
"/// itself is under test: envelope, transport check, param parse, result serialise.",
"class FixtureDispatcher final : public proto::Dispatcher {",
"public:",
" /// `results` maps a method name to that method's golden result JSON.",
" explicit FixtureDispatcher(std::function<const nlohmann::json*(const std::string&)> results)",
" : results_(std::move(results)) {}",
""]
for m in c.methods:
# Every method's params and result is a named struct, and this class lives in
# velox::conformance, so the names need qualifying.
pt, rt = "proto::" + cpp_type(m.params), "proto::" + cpp_type(m.result)
o += [
f" proto::HandlerResult<{rt}> {handler_name(m.name)}(const {pt}& params) override {{",
" (void)params;",
f' return golden<{rt}>("{m.name}");',
" }",
"",
]
o += [
"private:",
" // Every fixture-backed handler only ever succeeds. A missing or unparseable",
" // fixture is a bug in the suite, not a contract outcome, so it surfaces as",
" // InternalError rather than being dressed up as a real error code.",
" template <class T>",
" proto::HandlerResult<T> golden(const std::string& method) {",
" const nlohmann::json* value = results_(method);",
" if (value == nullptr)",
" return std::unexpected(proto::HandlerError{",
' proto::ErrorCode::InternalError, "no fixture for method " + method});',
" auto parsed = proto::parse<T>(*value, method);",
" if (!parsed)",
" return std::unexpected(proto::HandlerError{",
" proto::ErrorCode::InternalError,",
' "fixture for " + method + " failed to parse: " + parsed.error().message});',
" return std::move(*parsed);",
" }",
"",
" std::function<const nlohmann::json*(const std::string&)> results_;",
"};",
"",
"} // namespace velox::conformance",
"",
]
OUT.parent.mkdir(parents=True, exist_ok=True)
OUT.write_text("\n".join(o))
print(f"gen_cpp_conformance: {len(c.methods)} handlers -> {OUT}")
return 0
if __name__ == "__main__":
raise SystemExit(main())