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
131 changes: 117 additions & 14 deletions codegen/src/generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,10 +111,23 @@ fn schema_source(units: &[FrozenUnit]) -> String {
let has_protocol = units.iter().any(|u| matches!(u, FrozenUnit::Protocol { .. }));
let errors = error_type_names(units);

// Which framings the protocols in this file reach for. A non-datagram pick
// pulls in the `Framing` trait (its helpers call `framing.name()`); the
// datagram default pulls in the `FRAMING_DATAGRAM` name constant.
let protocol_framings = || {
units.iter().filter_map(|u| match u {
FrozenUnit::Protocol { parameters, .. } => Some(framing_choice(parameters)),
_ => None,
})
};
let needs_framing_trait = protocol_framings().any(|f| f.explicit);
let needs_datagram_const = protocol_framings().any(|f| !f.explicit);

let mut output = String::new();
output.push_str("// Generated by Comline\n");
output.push_str("use serde::{Deserialize, Serialize};\n");
if has_protocol {
if has_protocol && !needs_framing_trait {
// The common case — kept byte-for-byte for the conformance goldens.
output.push_str(
"use comline_runtime::client::Client;\n\
use comline_runtime::contract::{\n \
Expand All @@ -124,6 +137,34 @@ fn schema_source(units: &[FrozenUnit]) -> String {
use comline_runtime::serve::Server;\n\
use comline_runtime::transport::Transport;\n",
);
} else if has_protocol {
let mut names = vec![
"Call",
"CallError",
"Dispatch",
"Envelope",
"Framing",
"Handshake",
"Kind",
"Reply",
"RuntimeError",
"WireFormat",
];
if needs_datagram_const {
names.push("FRAMING_DATAGRAM");
}
output.push_str("use comline_runtime::client::Client;\n");
output.push_str("use comline_runtime::contract::{\n");
for name in names {
output.push_str(" ");
output.push_str(name);
output.push_str(",\n");
}
output.push_str(
"};\n\
use comline_runtime::serve::Server;\n\
use comline_runtime::transport::Transport;\n",
);
}
output.push('\n');

Expand All @@ -147,8 +188,13 @@ fn schema_source(units: &[FrozenUnit]) -> String {
FrozenUnit::Error { name, fields, .. } => {
output.push_str(&error_struct(name, fields));
}
FrozenUnit::Protocol { name, functions, .. } => {
output.push_str(&protocol(name, functions, &errors));
FrozenUnit::Protocol {
name,
functions,
parameters,
..
} => {
output.push_str(&protocol(name, functions, parameters, &errors));
}
_ => {}
}
Expand Down Expand Up @@ -270,7 +316,38 @@ struct FnInfo {
timeout_ms: Option<u64>,
}

fn protocol(proto: &str, functions: &[FrozenUnit], errors: &HashMap<u16, String>) -> String {
/// The `Framing` a protocol's generated `connect` / `serve` helpers wire up.
struct FramingChoice {
/// Path to the framing type, e.g. `comline_runtime::framing::JsonRpcFraming`.
ty: &'static str,
/// `true` for anything but the datagram default — those helpers pass the
/// framing explicitly (`Client::connect_with_framing` / `Server::with_framing`)
/// and the `Client` alias carries it as its third type parameter.
explicit: bool,
}

/// Read `@framing = "..."` off the protocol (frozen into its `parameters`).
/// Absent / unrecognised ⇒ the datagram default.
fn framing_choice(parameters: &[FrozenUnit]) -> FramingChoice {
match annotation(parameters, "framing") {
Some("jsonrpc") | Some("json-rpc") | Some("jsonrpc-2.0") => FramingChoice {
ty: "comline_runtime::framing::JsonRpcFraming",
explicit: true,
},
_ => FramingChoice {
ty: "comline_runtime::contract::DatagramFraming",
explicit: false,
},
}
}

fn protocol(
proto: &str,
functions: &[FrozenUnit],
parameters: &[FrozenUnit],
errors: &HashMap<u16, String>,
) -> String {
let framing = framing_choice(parameters);
let fns: Vec<FnInfo> = functions
.iter()
.filter_map(|f| match f {
Expand Down Expand Up @@ -457,35 +534,61 @@ fn protocol(proto: &str, functions: &[FrozenUnit], errors: &HashMap<u16, String>
" pub fn serve<T: Transport, W: WireFormat>(self, transport: &mut T, format: W)".into(),
);
l.push(" -> Result<(), RuntimeError> {".into());
l.push(
" let hs = Handshake::new(IR_HASH, format.name(), FRAMING_DATAGRAM, 0);".into(),
);
l.push(" Server::new(self, format).serve_handshaked(transport, hs)".into());
if framing.explicit {
l.push(format!(" let framing = {};", framing.ty));
l.push(
" let hs = Handshake::new(IR_HASH, format.name(), framing.name(), 0);".into(),
);
l.push(
" Server::with_framing(self, format, framing).serve_handshaked(transport, hs)"
.into(),
);
} else {
l.push(
" let hs = Handshake::new(IR_HASH, format.name(), FRAMING_DATAGRAM, 0);".into(),
);
l.push(" Server::new(self, format).serve_handshaked(transport, hs)".into());
}
l.push(" }".into());
l.push("}".into());
l.push(String::new());
l.push(String::new());
s.push_str(&l.join("\n"));

// 5. consumer stub — wraps a `Client`
let client_ty = if framing.explicit {
format!("Client<T, W, {}>", framing.ty)
} else {
"Client<T, W>".to_string()
};
let mut l: Vec<String> = Vec::new();
l.push(format!("pub struct {proto}Client<T, W>(pub Client<T, W>);"));
l.push(format!("pub struct {proto}Client<T, W>(pub {client_ty});"));
l.push(String::new());
l.push(format!(
"impl<T: Transport, W: WireFormat> {proto}Client<T, W> {{"
));
l.push(" pub fn new(client: Client<T, W>) -> Self {".into());
l.push(format!(" pub fn new(client: {client_ty}) -> Self {{"));
l.push(" Self(client)".into());
l.push(" }".into());
l.push(String::new());
l.push(" /// Bind + run the connection handshake against the provider.".into());
l.push(
" pub fn connect(transport: T, format: W) -> Result<Self, RuntimeError> {".into(),
);
l.push(
" let hs = Handshake::new(IR_HASH, format.name(), FRAMING_DATAGRAM, 0);".into(),
);
l.push(" Ok(Self(Client::connect(transport, format, hs)?))".into());
if framing.explicit {
l.push(format!(" let framing = {};", framing.ty));
l.push(
" let hs = Handshake::new(IR_HASH, format.name(), framing.name(), 0);".into(),
);
l.push(
" Ok(Self(Client::connect_with_framing(transport, format, framing, hs)?))".into(),
);
} else {
l.push(
" let hs = Handshake::new(IR_HASH, format.name(), FRAMING_DATAGRAM, 0);".into(),
);
l.push(" Ok(Self(Client::connect(transport, format, hs)?))".into());
}
l.push(" }".into());
for (i, f) in fns.iter().enumerate() {
let args = sig_args(&f.args);
Expand Down
34 changes: 33 additions & 1 deletion codegen/tests/compiles.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,41 @@ fn chat_schema() -> Vec<FrozenUnit> {
]
}

/// A protocol annotated `@framing = "jsonrpc"` — its generated `connect` /
/// `serve` helpers must compile against `comline_runtime::framing::JsonRpcFraming`
/// and the `Client<T, W, JsonRpcFraming>` alias.
fn rpc_schema() -> Vec<FrozenUnit> {
vec![FrozenUnit::Protocol {
docstring: "Rpc".into(),
parameters: vec![FrozenUnit::Property {
name: "framing".into(),
expression: Some("jsonrpc".into()),
}],
name: "Rpc".into(),
functions: vec![
function(
"echo",
vec![arg("line", KindValue::Namespaced("string".into(), None))],
Some(KindValue::Namespaced("string".into(), None)),
vec![],
),
function(
"tick",
vec![],
Some(KindValue::Primitive(Primitive::U32(None))),
vec![],
),
],
span: (0, 0),
}]
}

#[test]
fn a_generated_protocol_crate_builds() {
let schemas = vec![("chat".to_string(), chat_schema())];
let schemas = vec![
("chat".to_string(), chat_schema()),
("rpc".to_string(), rpc_schema()),
];
let req = GenRequest {
mode: Mode::Lib,
schemas: &schemas,
Expand Down
42 changes: 42 additions & 0 deletions codegen/tests/generate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,48 @@ fn timeout_ms_annotation_emits_call_with_timeout() {
assert!(src.contains(r#"self.0.call(Call::new(1, "fast"), &())?;"#));
}

#[test]
fn framing_annotation_selects_jsonrpc_for_the_connect_and_serve_helpers() {
// @framing = "jsonrpc" protocol Rpc { function now() -> u32; }
let proto = FrozenUnit::Protocol {
docstring: "Rpc".to_string(),
parameters: vec![FrozenUnit::Property {
name: "framing".to_string(),
expression: Some("jsonrpc".to_string()),
}],
name: "Rpc".to_string(),
functions: vec![FrozenUnit::Function {
docstring: String::new(),
name: "now".to_string(),
parameters: vec![],
arguments: vec![],
_return: Some(KindValue::Primitive(Primitive::U32(None))),
throws: vec![],
span: (0, 0),
}],
span: (0, 0),
};
let schemas = vec![("rpc".to_string(), vec![proto])];
let src = generate_rust(&code_req(&schemas)).unwrap().remove(0).contents;

// the `Framing` trait is in scope (the helpers call `framing.name()`)
assert!(src.contains("\n Framing,\n"));
// the client wraps a `Client` pinned to the JSON-RPC framing
assert!(src.contains(
"pub struct RpcClient<T, W>(pub Client<T, W, comline_runtime::framing::JsonRpcFraming>);"
));
// connect / serve pass the framing explicitly and hash its name
assert!(src.contains("let framing = comline_runtime::framing::JsonRpcFraming;"));
assert!(src.contains("let hs = Handshake::new(IR_HASH, format.name(), framing.name(), 0);"));
assert!(src.contains("Ok(Self(Client::connect_with_framing(transport, format, framing, hs)?))"));
assert!(
src.contains("Server::with_framing(self, format, framing).serve_handshaked(transport, hs)")
);
// the datagram default never appears for an all-JSON-RPC schema
assert!(!src.contains("FRAMING_DATAGRAM"));
assert!(!src.contains("Client::connect(transport, format, hs)"));
}

#[test]
fn an_all_one_way_protocol_leaves_the_dispatch_reply_param_unbound() {
let proto = FrozenUnit::Protocol {
Expand Down
Loading