Files
secs-gem/include/secsgem/gem/router.hpp
T
raphael 28dac8e9c8 I2: Router::dispatch_with_s9 helper + end-to-end S9F3/F5 tests
The S9F3/F5 fallback was previously inlined in apps/secs_server.cpp;
this commit lifts it onto Router as a template helper and adds two
focused tests asserting the wire behaviour against a real back-to-
back HSMS Connection pair.

  template <typename EmitFn, typename HeaderProvider>
  std::optional<Message> dispatch_with_s9(emit, header, msg);

The helper does the has_handler / has_handler_for_stream check and
calls the supplied emit function with S9F3 (unknown stream) or S9F5
(unknown function in known stream).  The header_provider returns the
optional MHEAD bytes — keeping the helper free of any direct
Connection coupling.

Tests:
  - SUT registered only for S1F1; peer sends S1F5 -> SUT replies
    S9F5 to the peer.
  - SUT registered only for S1F1; peer sends S7F19 -> SUT replies
    S9F3 to the peer.

Closes Tranche I — SML parser and the auto-S9F* fallback closeout
both verified end-to-end.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-08 03:58:03 +02:00

88 lines
2.8 KiB
C++

#pragma once
#include <cstdint>
#include <functional>
#include <map>
#include <optional>
#include <utility>
#include "secsgem/secs2/message.hpp"
namespace secsgem::gem {
namespace s2 = secsgem::secs2;
// A small (stream, function) dispatch table. The Server registers one
// handler per primary SxFy and calls `dispatch` from the Connection's
// message handler. Replaces the imperative if-ladder; behaviour stays in
// the handlers (since each SxFy reply shape is unique), but routing is
// data.
//
// Default behaviour for unregistered primaries:
// - If a `fallback` is installed, it runs.
// - Otherwise, if the inbound message has W set, reply with SxF0
// (Abort) per E5 convention.
// - Otherwise, do nothing.
class Router {
public:
using Handler = std::function<std::optional<s2::Message>(const s2::Message&)>;
Router& on(uint8_t stream, uint8_t function, Handler h) {
handlers_[{stream, function}] = std::move(h);
return *this;
}
Router& fallback(Handler h) {
fallback_ = std::move(h);
return *this;
}
std::optional<s2::Message> dispatch(const s2::Message& msg) const {
auto it = handlers_.find({msg.stream, msg.function});
if (it != handlers_.end()) return it->second(msg);
if (fallback_) return fallback_(msg);
if (msg.reply_expected) return s2::Message(msg.stream, 0, false);
return std::nullopt;
}
// Introspection: lets the message_handler wrapper decide whether an
// unhandled message is "unknown stream" (S9F3) or "unknown function in
// a known stream" (S9F5).
bool has_handler(uint8_t stream, uint8_t function) const {
return handlers_.find({stream, function}) != handlers_.end();
}
bool has_handler_for_stream(uint8_t stream) const {
for (const auto& [key, _] : handlers_)
if (key.first == stream) return true;
return false;
}
std::size_t size() const { return handlers_.size(); }
// Wrap dispatch so unhandled primaries trigger an S9 error report
// before the message is consumed. `emit_s9` takes (function_byte,
// mhead) — the connection's emit_s9 has exactly that signature, so a
// caller can write `router.dispatch_with_s9([&](auto f, auto h) {
// conn->emit_s9(f, h); }, current_header, msg)`. Per E5/E30:
// S9F3 = stream not recognized
// S9F5 = function within known stream not recognized
template <typename EmitFn, typename HeaderProvider>
std::optional<s2::Message> dispatch_with_s9(
EmitFn emit_s9, HeaderProvider header_provider,
const s2::Message& msg) const {
if (!has_handler(msg.stream, msg.function)) {
if (auto mhead = header_provider()) {
const uint8_t f = has_handler_for_stream(msg.stream) ? 5 : 3;
emit_s9(f, *mhead);
}
}
return dispatch(msg);
}
private:
std::map<std::pair<uint8_t, uint8_t>, Handler> handlers_;
Handler fallback_;
};
} // namespace secsgem::gem