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
43 changes: 26 additions & 17 deletions codegen/src/generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ use comline_codegen::{GenRequest, GeneratedFile, Mode, PackageMeta};
/// `comline-runtime` — the crate the generated RPC code links against. Pinned
/// by git rev (no crates.io yet); see design/runtime-repo-structure.md.
const RUNTIME_GIT: &str = "https://github.com/ComlineProject/runtime";
const RUNTIME_REV: &str = "3689cfdc480dc6b183b8ca33ade0a3cdf1a0d97c";
const RUNTIME_REV: &str = "0dda42de5105e75e339bf66fa38085bd53ab9ab9";

pub fn generate_rust(req: &GenRequest) -> Result<Vec<GeneratedFile>> {
match req.mode {
Expand Down Expand Up @@ -118,7 +118,7 @@ fn schema_source(units: &[FrozenUnit]) -> String {
output.push_str(
"use comline_runtime::client::Client;\n\
use comline_runtime::contract::{\n \
BufMut, CallError, Dispatch, Envelope, Handshake, Kind, RuntimeError,\n \
Call, CallError, Dispatch, Envelope, Handshake, Kind, Reply, RuntimeError,\n \
WireFormat, FRAMING_DATAGRAM,\n\
};\n\
use comline_runtime::serve::Server;\n\
Expand Down Expand Up @@ -380,16 +380,24 @@ fn protocol(proto: &str, functions: &[FrozenUnit], errors: &HashMap<u16, String>
let mut l: Vec<String> = Vec::new();
l.push(format!("pub struct {proto}Dispatcher<S>(pub S);"));
l.push(String::new());
// `out` is only written by request/response arms; a protocol that is
// entirely one-way never touches it.
let out_param = if fns.iter().all(|f| f.one_way) { "_out" } else { "out" };
// `reply` is only touched by request/response arms; a protocol that is
// entirely one-way never records anything.
let reply_param = if fns.iter().all(|f| f.one_way) {
"_reply"
} else {
"reply"
};
l.push(format!("impl<S: {proto}> Dispatch for {proto}Dispatcher<S> {{"));
l.push(" fn calls(&self) -> &'static [&'static str] {".into());
l.push(format!(" {calls_const}"));
l.push(" }".into());
l.push(String::new());
l.push(" fn dispatch<W: WireFormat>(".into());
l.push(" &self,".into());
l.push(" call: Kind,".into());
l.push(" params: &[u8],".into());
l.push(" fmt: &W,".into());
l.push(format!(" {out_param}: &mut dyn BufMut,"));
l.push(format!(" {reply_param}: &mut Reply,"));
l.push(" ) -> Result<(), RuntimeError> {".into());
l.push(format!(
" match call.resolve({calls_const}).ok_or(RuntimeError::UnknownCall)? {{"
Expand All @@ -412,23 +420,21 @@ fn protocol(proto: &str, functions: &[FrozenUnit], errors: &HashMap<u16, String>
};
let call = format!("self.0.{}({call_args})", f.name);
if f.one_way {
// Run the handler; write no envelope — the `Server` sees the
// empty buffer and replies with nothing.
// Run the handler; record nothing — the `Server` sees `Outcome::None`
// and replies with nothing.
l.push(format!(" {call};"));
} else {
l.push(format!(" match {call} {{"));
l.push(" Ok(reply) => {".into());
l.push(" Ok(value) => {".into());
l.push(" let mut body = Vec::new();".into());
l.push(" fmt.encode(&reply, &mut body)?;".into());
l.push(" Envelope::encode_ok(&body, out);".into());
l.push(" fmt.encode(&value, &mut body)?;".into());
l.push(" reply.ok(&body);".into());
l.push(" }".into());
for (ord, err) in &f.throws {
l.push(format!(" Err({}::{err}(e)) => {{", f.err_ty));
l.push(" let mut body = Vec::new();".into());
l.push(" fmt.encode(&e, &mut body)?;".into());
l.push(format!(
" Envelope::encode_err({ord}u16, &body, out);"
));
l.push(format!(" reply.err({ord}u16, &body);"));
l.push(" }".into());
}
if f.throws.is_empty() {
Expand Down Expand Up @@ -495,14 +501,17 @@ fn protocol(proto: &str, functions: &[FrozenUnit], errors: &HashMap<u16, String>
}
None => "&()".to_string(),
};
// Both addresses — the framing picks (ordinal for datagram, name for
// a name-oriented framing like JSON-RPC).
let addr = format!("Call::new({i}, \"{}\")", f.name);
l.push(String::new());
if f.one_way {
// Fire-and-forget: no reply, so no `CallError<E>`.
l.push(format!(
" pub fn {}(&mut self{args}) -> Result<(), RuntimeError> {{",
f.name
));
l.push(format!(" self.0.notify({i}u16, {param_expr})"));
l.push(format!(" self.0.notify({addr}, {param_expr})"));
l.push(" }".into());
continue;
}
Expand All @@ -512,9 +521,9 @@ fn protocol(proto: &str, functions: &[FrozenUnit], errors: &HashMap<u16, String>
));
let call = match f.timeout_ms {
Some(ms) => format!(
"self.0.call_with_timeout({i}u16, {param_expr}, core::time::Duration::from_millis({ms}))"
"self.0.call_with_timeout({addr}, {param_expr}, core::time::Duration::from_millis({ms}))"
),
None => format!("self.0.call({i}u16, {param_expr})"),
None => format!("self.0.call({addr}, {param_expr})"),
};
l.push(format!(" let (reply, fmt) = {call}?;"));
l.push(" match reply {".into());
Expand Down
21 changes: 12 additions & 9 deletions codegen/tests/generate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -219,13 +219,16 @@ fn protocol_errors_map_to_ordinals_and_a_union() {
// per-protocol union + From impl
assert!(src.contains("pub enum ChatError {\n Rejected(Rejected),\n}"));
assert!(src.contains("impl From<ChatSendError> for ChatError"));
// dispatcher encodes the error at its ordinal
assert!(src.contains("Envelope::encode_err(0u16, &body, out);"));
// dispatcher records the error at its ordinal on the framing-agnostic Reply
assert!(src.contains("reply.err(0u16, &body);"));
assert!(src.contains("reply.ok(&body);"));
assert!(src.contains("fn calls(&self) -> &'static [&'static str] {"));
// client maps that ordinal back
assert!(src.contains("Envelope::Err { id: 0u16, body } =>"));
// `-> ()` (Unit) is request/response with an empty ack
assert!(src.contains("fn ping(&self) -> Result<(), ChatPingError>;"));
assert!(src.contains("self.0.call(1u16, &())"));
// both addresses on the wire; the framing picks
assert!(src.contains(r#"self.0.call(Call::new(1, "ping"), &())"#));
// a `str` arg is borrowed
assert!(src.contains("fn send(&self, body: &str) -> Result<(), ChatSendError>;"));
assert!(src.contains("pub struct ChatSendParams<'a> {\n #[serde(borrow)]\n pub body: &'a str,\n}"));
Expand All @@ -235,7 +238,7 @@ fn protocol_errors_map_to_ordinals_and_a_union() {
assert!(!src.contains("ChatPokeError"));
assert!(src.contains("fn poke(&self, note: &str);"));
assert!(src.contains("pub fn poke(&mut self, note: &str) -> Result<(), RuntimeError> {"));
assert!(src.contains("self.0.notify(2u16, &ChatPokeParams { note })"));
assert!(src.contains(r#"self.0.notify(Call::new(2, "poke"), &ChatPokeParams { note })"#));
}

#[test]
Expand Down Expand Up @@ -275,13 +278,13 @@ fn timeout_ms_annotation_emits_call_with_timeout() {
let src = generate_rust(&code_req(&schemas)).unwrap().remove(0).contents;

assert!(src.contains(
"self.0.call_with_timeout(0u16, &(), core::time::Duration::from_millis(2500))?;"
r#"self.0.call_with_timeout(Call::new(0, "slow"), &(), core::time::Duration::from_millis(2500))?;"#
));
assert!(src.contains("self.0.call(1u16, &())?;"));
assert!(src.contains(r#"self.0.call(Call::new(1, "fast"), &())?;"#));
}

#[test]
fn an_all_one_way_protocol_leaves_the_dispatch_out_param_unbound() {
fn an_all_one_way_protocol_leaves_the_dispatch_reply_param_unbound() {
let proto = FrozenUnit::Protocol {
docstring: "Bus".to_string(),
parameters: vec![],
Expand All @@ -303,8 +306,8 @@ fn an_all_one_way_protocol_leaves_the_dispatch_out_param_unbound() {
};
let schemas = vec![("bus".to_string(), vec![proto])];
let src = generate_rust(&code_req(&schemas)).unwrap().remove(0).contents;
assert!(src.contains("_out: &mut dyn BufMut,"));
assert!(!src.contains("\n out: &mut dyn BufMut,"));
assert!(src.contains("_reply: &mut Reply,"));
assert!(!src.contains("\n reply: &mut Reply,"));
}

#[test]
Expand Down
Loading