Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions runtime/core/src/contract/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,22 @@ pub enum Kind {
Named(&'static str),
}

impl Kind {
/// Resolve to a position in `calls` — a protocol's `calls_names()`, in
/// declaration order. `Id(n)` is that position directly; `Named(s)` is
/// looked up. `None` if out of range or not found — a generated dispatcher
/// maps that to [`RuntimeError::UnknownCall`].
pub fn resolve(&self, calls: &[&str]) -> Option<usize> {
match self {
Kind::Id(id) => {
let idx = *id as usize;
(idx < calls.len()).then_some(idx)
}
Kind::Named(name) => calls.iter().position(|c| c == name),
}
}
}

/// The provider side of a protocol: given an inbound call and its encoded
/// params, run the user's handler and write the response [`Envelope`] into
/// `out`.
Expand All @@ -31,3 +47,23 @@ pub trait Dispatch {
out: &mut dyn BufMut,
) -> Result<(), RuntimeError>;
}

#[cfg(test)]
mod tests {
use super::Kind;

const CALLS: &[&str] = &["send", "history", "notify"];

#[test]
fn id_resolves_by_position() {
assert_eq!(Kind::Id(0).resolve(CALLS), Some(0));
assert_eq!(Kind::Id(2).resolve(CALLS), Some(2));
assert_eq!(Kind::Id(3).resolve(CALLS), None);
}

#[test]
fn named_resolves_by_lookup() {
assert_eq!(Kind::Named("history").resolve(CALLS), Some(1));
assert_eq!(Kind::Named("missing").resolve(CALLS), None);
}
}
189 changes: 189 additions & 0 deletions runtime/core/tests/dispatch_roundtrip.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
//! End to end, no network: a hand-written stand-in for what `comline-rust` will
//! generate — a client stub and a `Dispatch` impl — driven directly with
//! `MsgPack`. Proves the `contract` surface (`Kind`, `WireFormat`, `Dispatch`,
//! `Envelope`, `BufMut`, `CallError`) fits together before any codegen or
//! `setup/` rework.
#![cfg(feature = "std")]

use comline_runtime::contract::{
BufMut, CallError, Dispatch, Envelope, Kind, RuntimeError, WireFormat,
};
use comline_runtime::format::MsgPack;
use serde::{Deserialize, Serialize};

// The "schema":
// protocol Echo {
// function say(msg: str) -> str ! TooLong;
// function bump(n: u32) -> u32;
// }

#[derive(Serialize, Deserialize)]
struct SayParams<'a> {
#[serde(borrow)]
msg: &'a str,
}

#[derive(Serialize, Deserialize)]
struct BumpParams {
n: u32,
}

#[derive(Serialize, Deserialize, PartialEq, Eq, Debug)]
struct TooLong {
limit: u32,
}

const CALLS: &[&str] = &["say", "bump"];
const ERR_TOO_LONG: u16 = 0; // schema-global error ordinal

/// The generated per-function error enum for `say`.
#[derive(Debug, PartialEq, Eq)]
enum SayError {
TooLong(TooLong),
}

// ── provider: the user trait + the generated dispatcher ─────────────────────

trait Echo {
fn say(&self, msg: &str) -> Result<String, SayError>;
fn bump(&self, n: u32) -> Result<u32, core::convert::Infallible>;
}

struct EchoDispatcher<T>(T);

impl<T: Echo> Dispatch for EchoDispatcher<T> {
fn dispatch<W: WireFormat>(
&self,
call: Kind,
params: &[u8],
fmt: &W,
out: &mut dyn BufMut,
) -> Result<(), RuntimeError> {
// A real dispatcher reuses one scratch buffer; a fresh `Vec` per arm
// keeps the shape readable here.
match call.resolve(CALLS).ok_or(RuntimeError::UnknownCall)? {
0 => {
let p: SayParams = fmt.decode(params)?;
match self.0.say(p.msg) {
Ok(reply) => {
let mut body = Vec::new();
fmt.encode(&reply, &mut body)?;
Envelope::encode_ok(&body, out);
}
Err(SayError::TooLong(e)) => {
let mut body = Vec::new();
fmt.encode(&e, &mut body)?;
Envelope::encode_err(ERR_TOO_LONG, &body, out);
}
}
Ok(())
}
1 => {
let p: BumpParams = fmt.decode(params)?;
let reply = self.0.bump(p.n).unwrap();
let mut body = Vec::new();
fmt.encode(&reply, &mut body)?;
Envelope::encode_ok(&body, out);
Ok(())
}
_ => Err(RuntimeError::UnknownCall),
}
}
}

// ── consumer: the generated client stub ────────────────────────────────────

struct EchoClient<'d, D> {
dispatcher: &'d D, // stands in for a call system + transport
fmt: MsgPack,
}

impl<D: Dispatch> EchoClient<'_, D> {
fn say(&self, msg: &str) -> Result<String, CallError<SayError>> {
let mut params = Vec::new();
self.fmt.encode(&SayParams { msg }, &mut params)?;

let mut frame = Vec::new();
self.dispatcher
.dispatch(Kind::Id(0), &params, &self.fmt, &mut frame)?;

match Envelope::decode(&frame).ok_or(RuntimeError::Framing)? {
Envelope::Ok(payload) => self.fmt.decode(payload).map_err(CallError::Runtime),
Envelope::Err {
id: ERR_TOO_LONG,
body,
} => {
let e: TooLong = self.fmt.decode(body)?;
Err(CallError::App(SayError::TooLong(e)))
}
Envelope::Err { id, .. } => Err(CallError::Runtime(RuntimeError::Remote { id })),
}
}

fn bump(&self, n: u32) -> Result<u32, CallError<core::convert::Infallible>> {
let mut params = Vec::new();
self.fmt.encode(&BumpParams { n }, &mut params)?;

let mut frame = Vec::new();
self.dispatcher
.dispatch(Kind::Id(1), &params, &self.fmt, &mut frame)?;

match Envelope::decode(&frame).ok_or(RuntimeError::Framing)? {
Envelope::Ok(payload) => self.fmt.decode(payload).map_err(CallError::Runtime),
Envelope::Err { id, .. } => Err(CallError::Runtime(RuntimeError::Remote { id })),
}
}
}

// ── the service and the assertions ────────────────────────────────────────

struct Server;

impl Echo for Server {
fn say(&self, msg: &str) -> Result<String, SayError> {
if msg.len() > 8 {
return Err(SayError::TooLong(TooLong { limit: 8 }));
}
Ok(format!("echo: {msg}"))
}

fn bump(&self, n: u32) -> Result<u32, core::convert::Infallible> {
Ok(n + 1)
}
}

fn client() -> (EchoDispatcher<Server>, MsgPack) {
(EchoDispatcher(Server), MsgPack)
}

#[test]
fn ok_path_round_trips() {
let (d, fmt) = client();
let c = EchoClient {
dispatcher: &d,
fmt,
};
assert_eq!(c.say("hi").unwrap(), "echo: hi");
assert_eq!(c.bump(41).unwrap(), 42);
}

#[test]
fn a_raised_error_reaches_the_client_typed() {
let (d, fmt) = client();
let c = EchoClient {
dispatcher: &d,
fmt,
};
let err = c.say("this is far too long").unwrap_err();
assert_eq!(err, CallError::App(SayError::TooLong(TooLong { limit: 8 })));
}

#[test]
fn an_unknown_call_ordinal_is_a_runtime_error() {
let (d, _) = client();
let mut out = Vec::new();
let err = d
.dispatch(Kind::Id(9), &[], &MsgPack, &mut out)
.unwrap_err();
assert_eq!(err, RuntimeError::UnknownCall);
}
5 changes: 2 additions & 3 deletions runtime/core/tests/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
// Relative Modules
// The `setups` suite drives the `std`-only `setup/` layer.
#[cfg(feature = "std")]
pub mod setups;


Loading