28dac8e9c8
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>
88 lines
2.8 KiB
C++
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
|