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
2 changes: 1 addition & 1 deletion codegen/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ license = "GPL-3.0-only"
# repository structure — no crates.io publishing at this stage). Pinned to the
# same generation rev `comline-typescript` uses, so a CLI build has one
# `comline-codegen`. Local iteration: add a `[patch]` onto a sibling checkout.
comline-codegen = { git = "https://github.com/ComlineProject/generation", rev = "1f8c5e290eba72a578860ebb6dc33453a2f0726b" }
comline-codegen = { git = "https://github.com/ComlineProject/generation", rev = "c70336dcc9485cea2c99b0f1db3789e420deaeda" }
comline-core = { git = "https://github.com/ComlineProject/core", rev = "47ac5f10a36b587d14d1b9672fba65d8f3712962" }

eyre = "0.6.8"
Expand Down
57 changes: 41 additions & 16 deletions codegen/src/generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,14 @@ const RUNTIME_GIT: &str = "https://github.com/ComlineProject/runtime";
const RUNTIME_REV: &str = "0dda42de5105e75e339bf66fa38085bd53ab9ab9";

pub fn generate_rust(req: &GenRequest) -> Result<Vec<GeneratedFile>> {
let default_framing = req.default_framing.as_deref();
match req.mode {
Mode::Code => Ok(req
.schemas
.iter()
.map(|(namespace, units)| GeneratedFile {
path: PathBuf::from(format!("{namespace}.rs")),
contents: schema_source(units),
contents: schema_source(units, default_framing),
})
.collect()),

Expand All @@ -54,7 +55,7 @@ pub fn generate_rust(req: &GenRequest) -> Result<Vec<GeneratedFile>> {
for (namespace, units) in req.schemas {
files.push(GeneratedFile {
path: PathBuf::from(format!("src/{namespace}.rs")),
contents: schema_source(units),
contents: schema_source(units, default_framing),
});
}
Ok(files)
Expand Down Expand Up @@ -107,7 +108,7 @@ fn lib_rs(schemas: &[(String, Vec<FrozenUnit>)]) -> String {
s
}

fn schema_source(units: &[FrozenUnit]) -> String {
fn schema_source(units: &[FrozenUnit], default_framing: Option<&str>) -> String {
let has_protocol = units.iter().any(|u| matches!(u, FrozenUnit::Protocol { .. }));
let errors = error_type_names(units);

Expand All @@ -116,7 +117,9 @@ fn schema_source(units: &[FrozenUnit]) -> String {
// 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)),
FrozenUnit::Protocol { parameters, .. } => {
Some(framing_choice(parameters, default_framing))
}
_ => None,
})
};
Expand Down Expand Up @@ -194,7 +197,13 @@ fn schema_source(units: &[FrozenUnit]) -> String {
parameters,
..
} => {
output.push_str(&protocol(name, functions, parameters, &errors));
output.push_str(&protocol(
name,
functions,
parameters,
default_framing,
&errors,
));
}
_ => {}
}
Expand Down Expand Up @@ -326,28 +335,44 @@ struct FramingChoice {
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 {
/// The datagram default — the generated helpers use `Client::connect` /
/// `Server::new` and the `Client` alias keeps its two type parameters.
const DATAGRAM_FRAMING: FramingChoice = FramingChoice {
ty: "comline_runtime::contract::DatagramFraming",
explicit: false,
};

/// Map one framing name to a choice. `None` for a name this generator does not
/// know (the caller falls through to the next level).
fn framing_for(name: &str) -> Option<FramingChoice> {
match name {
"jsonrpc" | "json-rpc" | "jsonrpc-2.0" => Some(FramingChoice {
ty: "comline_runtime::framing::JsonRpcFraming",
explicit: true,
},
_ => FramingChoice {
ty: "comline_runtime::contract::DatagramFraming",
explicit: false,
},
}),
"datagram" | "comline.datagram" => Some(DATAGRAM_FRAMING),
_ => None,
}
}

/// Resolve a protocol's framing, most specific first: its own `@framing`
/// annotation, then the package `default_framing` (`comline.toml`), then the
/// datagram default. An unrecognised name at either level falls through.
fn framing_choice(parameters: &[FrozenUnit], default_framing: Option<&str>) -> FramingChoice {
annotation(parameters, "framing")
.and_then(framing_for)
.or_else(|| default_framing.and_then(framing_for))
.unwrap_or(DATAGRAM_FRAMING)
}

fn protocol(
proto: &str,
functions: &[FrozenUnit],
parameters: &[FrozenUnit],
default_framing: Option<&str>,
errors: &HashMap<u16, String>,
) -> String {
let framing = framing_choice(parameters);
let framing = framing_choice(parameters, default_framing);
let fns: Vec<FnInfo> = functions
.iter()
.filter_map(|f| match f {
Expand Down
30 changes: 28 additions & 2 deletions codegen/tests/compiles.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@ fn function(

/// A schema with a struct, an `error`, and a protocol exercising: a throwing
/// call, a non-throwing call returning a list, a zero-arg call, and a
/// `KindValue::Unit` return.
/// `KindValue::Unit` return. `@framing = "datagram"` keeps it on the datagram
/// stack even though the compile test sets a `jsonrpc` package default — so the
/// datagram path (with all this machinery) still gets built.
fn chat_schema() -> Vec<FrozenUnit> {
vec![
FrozenUnit::Struct {
Expand All @@ -73,7 +75,10 @@ fn chat_schema() -> Vec<FrozenUnit> {
},
FrozenUnit::Protocol {
docstring: "Chat".into(),
parameters: vec![],
parameters: vec![FrozenUnit::Property {
name: "framing".into(),
expression: Some("datagram".into()),
}],
name: "Chat".into(),
functions: vec![
function(
Expand Down Expand Up @@ -149,11 +154,29 @@ fn rpc_schema() -> Vec<FrozenUnit> {
}]
}

/// A plain protocol with no `@framing` — it rides the `jsonrpc` package default
/// the test sets, so the default-driven JSON-RPC output also gets compiled.
fn rpc_default_schema() -> Vec<FrozenUnit> {
vec![FrozenUnit::Protocol {
docstring: "Clock".into(),
parameters: vec![],
name: "Clock".into(),
functions: vec![function(
"now",
vec![arg("tz", KindValue::Namespaced("string".into(), None))],
Some(KindValue::Primitive(Primitive::U64(None))),
vec![],
)],
span: (0, 0),
}]
}

#[test]
fn a_generated_protocol_crate_builds() {
let schemas = vec![
("chat".to_string(), chat_schema()),
("rpc".to_string(), rpc_schema()),
("clock".to_string(), rpc_default_schema()),
];
let req = GenRequest {
mode: Mode::Lib,
Expand All @@ -162,6 +185,9 @@ fn a_generated_protocol_crate_builds() {
name: "comline-codegen-rust-compiletest".into(),
version: "0.0.0".into(),
},
// `chat` opts back to datagram with `@framing = "datagram"`; `clock`
// (unannotated) takes this default; `rpc` names `jsonrpc` itself.
default_framing: Some("jsonrpc".into()),
};
let files = generate_rust(&req).expect("generation");

Expand Down
66 changes: 66 additions & 0 deletions codegen/tests/generate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ fn code_req(schemas: &[(String, Vec<FrozenUnit>)]) -> GenRequest<'_> {
mode: Mode::Code,
schemas,
package: PackageMeta { name: "test".into(), version: "0.1.0".into() },
default_framing: None,
}
}

Expand All @@ -16,6 +17,7 @@ fn lib_req(schemas: &[(String, Vec<FrozenUnit>)]) -> GenRequest<'_> {
mode: Mode::Lib,
schemas,
package: PackageMeta { name: "chat".into(), version: "0.3.0".into() },
default_framing: None,
}
}

Expand Down Expand Up @@ -325,6 +327,70 @@ fn framing_annotation_selects_jsonrpc_for_the_connect_and_serve_helpers() {
assert!(!src.contains("Client::connect(transport, format, hs)"));
}

/// A protocol with no `@framing` of its own, plus a plain (annotated) one for
/// contrast.
fn one_plain_protocol(name: &str) -> Vec<FrozenUnit> {
vec![FrozenUnit::Protocol {
docstring: name.to_string(),
parameters: vec![],
name: name.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),
}]
}

#[test]
fn package_default_framing_applies_when_a_protocol_has_no_annotation() {
let schemas = vec![("rpc".to_string(), one_plain_protocol("Rpc"))];
let req = GenRequest {
default_framing: Some("jsonrpc".to_string()),
..code_req(&schemas)
};
let src = generate_rust(&req).unwrap().remove(0).contents;

assert!(src.contains(
"pub struct RpcClient<T, W>(pub Client<T, W, comline_runtime::framing::JsonRpcFraming>);"
));
assert!(src.contains("Server::with_framing(self, format, framing).serve_handshaked"));
assert!(!src.contains("FRAMING_DATAGRAM"));
}

#[test]
fn an_explicit_datagram_annotation_opts_out_of_the_package_default() {
// @framing = "datagram" on the protocol, package default is jsonrpc.
let mut units = one_plain_protocol("Rpc");
if let FrozenUnit::Protocol { parameters, .. } = &mut units[0] {
parameters.push(FrozenUnit::Property {
name: "framing".to_string(),
expression: Some("datagram".to_string()),
});
}
let schemas = vec![("rpc".to_string(), units)];
let with_default = GenRequest {
default_framing: Some("jsonrpc".to_string()),
..code_req(&schemas)
};
let src = generate_rust(&with_default).unwrap().remove(0).contents;

// the datagram stack — the `@framing = "datagram"` opt-out beats the
// `jsonrpc` package default (the `IR_HASH` still moves: the annotation is
// part of the frozen IR)
assert!(!src.contains("JsonRpcFraming"));
assert!(!src.contains("Framing,")); // trait not imported
assert!(src.contains("pub struct RpcClient<T, W>(pub Client<T, W>);"));
assert!(src.contains("let hs = Handshake::new(IR_HASH, format.name(), FRAMING_DATAGRAM, 0);"));
assert!(src.contains("Server::new(self, format).serve_handshaked(transport, hs)"));
assert!(src.contains("Ok(Self(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