use std::collections::{BTreeMap, HashMap};
use heck::ToUpperCamelCase;
use proc_macro2::TokenStream as Ts;
use quote::{format_ident, quote};
use crate::codegen::bridge_api::{UserHttpBridgeApi, UserHttpEndpoint, UserMqttBridge};
use crate::codegen::cell_api::{ApiCommand, ApiEvent, ApiField, ApiType, CellApi};
use crate::codegen::status::status_variant_name;
use crate::codegen::template::{RawSeg, Seg, Segments};
mod schema;
pub fn http_bridge(root: &Ts, api: UserHttpBridgeApi) -> Result<Ts, String> {
let UserHttpBridgeApi {
name,
base_url: _, types,
endpoints,
} = api;
let (type_defs, type_idents) = match types {
Some(schema) => schema::generate(root, schema)?,
None => (Ts::new(), BTreeMap::new()),
};
let mut defs = Vec::new();
let mut methods = Vec::new();
for endpoint in &endpoints {
let (reply_def, payload_def, method) = http_endpoint(root, endpoint, &type_idents)?;
defs.push(reply_def);
defs.push(payload_def);
methods.push(method);
}
let client = client_struct(&name, &methods);
Ok(quote! {
#type_defs
#(#defs)*
#client
})
}
fn http_endpoint(
root: &Ts,
endpoint: &UserHttpEndpoint,
type_idents: &BTreeMap<String, Ts>,
) -> Result<(Ts, Ts, Ts), String> {
let camel = endpoint.id.to_upper_camel_case();
let reply_ident = format_ident!("{camel}Reply");
let payload_ident = format_ident!("__{camel}Payload");
if type_idents.contains_key(&format!("{camel}Reply")) {
return Err(format!(
"endpoint `{}` generates `{camel}Reply`, which collides with a `types` definition of the same name",
endpoint.id
));
}
let params = request_params(endpoint)?;
if params.iter().any(|(_, n)| n == "__callback") {
return Err(format!(
"endpoint `{}` has a request placeholder named `__callback`, which is reserved",
endpoint.id
));
}
let mut sig = Vec::new();
let mut field_decls = Vec::new();
let mut inits = Vec::new();
for (kind, name) in ¶ms {
let (s, f, i) = request_param_tokens(root, kind, name, type_idents);
sig.push(s);
field_decls.push(f);
inits.push(i);
}
if let Some(body) = &endpoint.request.body
&& let Some((kind, name)) = body.0.split()
{
if params.iter().any(|(_, n)| n == "body") {
return Err(format!(
"endpoint `{}` has both a `body` and a request parameter named `body`",
endpoint.id
));
}
let (s, f, i) = body_param_tokens(root, kind, name, type_idents);
sig.push(s);
field_decls.push(f);
inits.push(i);
}
let reply_def = reply_enum(root, &reply_ident, endpoint, type_idents)?;
let serde_crate = format!("{root}::codegen::exports::serde");
let payload_def = quote! {
#[derive(
#root::codegen::exports::serde::Serialize,
#root::codegen::exports::serde::Deserialize,
#root::Message,
)]
#[serde(crate = #serde_crate)]
struct #payload_ident {
#(#field_decls,)*
__callback: #root::String,
}
};
let method_ident = format_ident!("{}", endpoint.id);
let command_name = &endpoint.id;
let method = quote! {
pub fn #method_ident(&self, #(#sig,)* cb: #root::Callback<#reply_ident>) -> #root::Result<()> {
let sri = #root::Sri::from_target(self.target)
.map_err(|_| "invalid bridge target")?;
let __cb: #root::Command = cb.into();
let __payload = #payload_ident {
#(#inits,)*
__callback: __cb.as_ref().into(),
};
#root::send(sri, #command_name, &__payload)
}
};
Ok((reply_def, payload_def, method))
}
fn request_params(endpoint: &UserHttpEndpoint) -> Result<Vec<(String, String)>, String> {
let req = &endpoint.request;
let mut params: Vec<(String, String)> = Vec::new();
let mut push = |kind: &str, name: &str| -> Result<(), String> {
if params.iter().any(|(_, n)| n == name) {
return Err(format!(
"request placeholder `{name}` appears more than once in endpoint `{}`; each parameter must be unique",
endpoint.id
));
}
params.push((kind.to_string(), name.to_string()));
Ok(())
};
for seg in req.path.0.iter() {
if let Some((kind, name)) = seg.split() {
push(kind, name)?;
}
}
for segs in req.query.values() {
for seg in segs.0.iter() {
if let Some((kind, name)) = seg.split() {
push(kind, name)?;
}
}
}
for segs in req.headers.values() {
for seg in segs.0.iter() {
if let Some((kind, name)) = seg.split() {
push(kind, name)?;
}
}
}
Ok(params)
}
fn request_param_tokens(
root: &Ts,
kind: &str,
name: &str,
type_idents: &BTreeMap<String, Ts>,
) -> (Ts, Ts, Ts) {
let ident = format_ident!("{name}");
match kind {
"bool" => scalar(&ident, "e! { bool }),
"u8" => scalar(&ident, "e! { u8 }),
"u16" => scalar(&ident, "e! { u16 }),
"u32" => scalar(&ident, "e! { u32 }),
"u64" => scalar(&ident, "e! { u64 }),
"i8" => scalar(&ident, "e! { i8 }),
"i16" => scalar(&ident, "e! { i16 }),
"i32" => scalar(&ident, "e! { i32 }),
"i64" => scalar(&ident, "e! { i64 }),
"f32" => scalar(&ident, "e! { f32 }),
"f64" => scalar(&ident, "e! { f64 }),
"json" => {
let jt = json_ref_type(root, name, type_idents);
(
quote! { #ident: &#jt },
quote! { #ident: #root::JsonValue },
quote! { #ident: #root::codegen::exports::serde_json::to_value(#ident)
.map_err(|_| "unable to serialise json argument")? },
)
}
_ => (
quote! { #ident: &str },
quote! { #ident: #root::String },
quote! { #ident: #ident.into() },
),
}
}
fn body_param_tokens(
root: &Ts,
kind: &str,
name: &str,
type_idents: &BTreeMap<String, Ts>,
) -> (Ts, Ts, Ts) {
match kind {
"json" => {
let jt = json_ref_type(root, name, type_idents);
(
quote! { body: &#jt },
quote! { body: #root::JsonValue },
quote! { body: #root::codegen::exports::serde_json::to_value(body)
.map_err(|_| "unable to serialise json body")? },
)
}
"bytes" => (
quote! { body: &[u8] },
quote! { body: #root::Vec<u8> },
quote! { body: body.to_vec() },
),
_ => (
quote! { body: &str },
quote! { body: #root::String },
quote! { body: body.into() },
),
}
}
fn scalar(ident: &syn::Ident, ty: &Ts) -> (Ts, Ts, Ts) {
(
quote! { #ident: #ty },
quote! { #ident: #ty },
quote! { #ident },
)
}
fn reply_enum(
root: &Ts,
reply_ident: &syn::Ident,
endpoint: &UserHttpEndpoint,
type_idents: &BTreeMap<String, Ts>,
) -> Result<Ts, String> {
let mut variants = Vec::new();
let mut seen: Vec<String> = Vec::new();
for (code, variant) in &endpoint.response {
let name = status_variant_name(*code)
.map_err(|err| format!("endpoint `{}`: {err}", endpoint.id))?;
if seen.iter().any(|s| s == &name) {
return Err(format!(
"endpoint `{}` lists two statuses that map to variant `{name}`",
endpoint.id
));
}
seen.push(name.clone());
let ident = format_ident!("{name}");
let variant = &variant.0;
let mut header_fields = Vec::new();
let mut header_names: Vec<String> = Vec::new();
for hdr in variant.headers.values() {
if let Some((_, hname)) = hdr.0.split() {
if header_names.iter().any(|n| n == hname) {
return Err(format!(
"endpoint `{}` status `{code}` uses response placeholder `{hname}` more than once",
endpoint.id
));
}
header_names.push(hname.to_string());
let field = format_ident!("{hname}");
header_fields.push(quote! { #field: #root::String });
}
}
let body_ty =
variant
.body
.as_ref()
.and_then(|b| b.0.split())
.map(|(kind, bname)| match kind {
"json" => json_ref_type(root, bname, type_idents),
"bytes" => quote! { #root::Vec<u8> },
_ => quote! { #root::String },
});
if !header_fields.is_empty() {
if let Some(body_ty) = body_ty {
header_fields.push(quote! { body: #body_ty });
}
variants.push(quote! { #ident { #(#header_fields),* } });
} else if let Some(body_ty) = body_ty {
variants.push(quote! { #ident(#body_ty) });
} else {
variants.push(quote! { #ident });
}
}
variants.push(quote! { Unknown(u16) });
let serde_crate = format!("{root}::codegen::exports::serde");
Ok(quote! {
#[derive(
Debug,
Clone,
#root::codegen::exports::serde::Serialize,
#root::codegen::exports::serde::Deserialize,
#root::Message,
)]
#[serde(crate = #serde_crate)]
pub enum #reply_ident {
#(#variants,)*
}
})
}
fn json_ref_type(root: &Ts, name: &str, type_idents: &BTreeMap<String, Ts>) -> Ts {
type_idents
.get(name)
.cloned()
.unwrap_or_else(|| quote! { #root::JsonValue })
}
pub fn mqtt_bridge(root: &Ts, api: UserMqttBridge) -> Result<Ts, String> {
let cell_api = convert_mqtt(api)?;
Ok(cell_api_tokens(root, &cell_api))
}
fn command_method(root: &Ts, name: &str, arg_ty: Option<&Ts>, by_value: bool) -> Ts {
let method = format_ident!("{}", name);
let command_name = name;
let resolve_sri = quote! {
let sri = #root::Sri::from_target(self.target)
.map_err(|_| "invalid bridge target")?;
};
match arg_ty {
Some(ty) if by_value => quote! {
pub fn #method(&self, value: #ty) -> #root::Result<()> {
#resolve_sri
#root::send(sri, #command_name, &value)
}
},
Some(ty) => quote! {
pub fn #method(&self, value: &#ty) -> #root::Result<()> {
#resolve_sri
#root::send(sri, #command_name, value)
}
},
None => quote! {
pub fn #method(&self) -> #root::Result<()> {
#resolve_sri
#root::send(sri, #command_name, &#root::Void)
}
},
}
}
fn client_struct(bridge_name: &str, methods: &[Ts]) -> Ts {
let client = format_ident!("{}Client", bridge_name.to_upper_camel_case());
quote! {
pub struct #client {
target: &'static str,
}
impl #client {
pub const fn new(target: &'static str) -> Self {
Self { target }
}
#(#methods)*
}
}
}
fn cell_api_tokens(root: &Ts, api: &CellApi) -> Ts {
let mut type_defs = Vec::new();
if let Some(types) = &api.types {
for (name, ty) in types {
type_defs.push(struct_def(root, name, ty, None));
}
}
let mut event_defs = Vec::new();
for (name, event) in &api.events {
event_defs.push(struct_def(root, name, event.as_ref(), Some(name)));
}
let mut methods = Vec::new();
for (name, cmd) in &api.commands {
methods.extend(command_methods(root, name, cmd));
}
let client = client_struct(&api.cell, &methods);
quote! {
#(#type_defs)*
#(#event_defs)*
#client
}
}
fn struct_def(root: &Ts, name: &str, ty: &ApiType, event_name: Option<&str>) -> Ts {
let ident = format_ident!("{}", name);
let serde_crate = format!("{root}::codegen::exports::serde");
let fields = ty.fields.iter().map(|f| {
let field_ident = format_ident!("{}", f.name);
let field_ty: syn::Type =
syn::parse_str(&f.field_type).unwrap_or_else(|_| syn::parse_quote!(#root::String));
quote! { pub #field_ident: #field_ty }
});
let event_impl = event_name.map(|ev| {
quote! {
impl #root::CellEvent for #ident {
fn event_name() -> &'static str { #ev }
}
}
});
quote! {
#[derive(
Debug,
Clone,
#root::codegen::exports::serde::Serialize,
#root::codegen::exports::serde::Deserialize,
#root::Message,
)]
#[serde(crate = #serde_crate)]
pub struct #ident {
#(#fields,)*
}
#event_impl
}
}
fn command_methods(root: &Ts, name: &str, cmd: &ApiCommand) -> Vec<Ts> {
let arg_ty = cmd
.args
.as_deref()
.filter(|s| *s != "None")
.map(|s| format_ident!("{}", s));
let arg = arg_ty.as_ref().map(|ty| quote! { #ty });
let method = command_method(root, name, arg.as_ref(), true);
vec![method]
}
fn convert_mqtt(api: UserMqttBridge) -> Result<CellApi, String> {
let UserMqttBridge {
name,
broker_url: _,
ingress,
egress,
} = api;
let name = name.to_upper_camel_case();
let mut types: HashMap<String, ApiType> = HashMap::new();
let mut events: HashMap<String, ApiEvent> = HashMap::new();
let mut commands: HashMap<String, ApiCommand> = HashMap::new();
for egress in egress {
let cmd_name = egress.id.to_upper_camel_case();
let mut fields = FieldCollector::default();
fields.scan_segments(&egress.topic.0)?;
fields.push(&egress.payload.0)?;
types.insert(
cmd_name.clone(),
ApiType {
description: None,
fields: fields.into_fields(),
},
);
commands.insert(
egress.id,
ApiCommand {
description: None,
args: Some(cmd_name),
},
);
}
for ingress in ingress {
let event_name = ingress.id.to_upper_camel_case();
let mut fields = FieldCollector::default();
fields.push(&ingress.payload.0)?;
events.insert(
event_name,
ApiEvent(ApiType {
description: None,
fields: fields.into_fields(),
}),
);
}
Ok(CellApi {
cell: name,
types: if types.is_empty() { None } else { Some(types) },
commands,
events,
})
}
#[derive(Default)]
struct FieldCollector {
fields: Vec<ApiField>,
seen: HashMap<String, String>,
}
impl FieldCollector {
fn scan_segments<S: RawSeg>(&mut self, segments: &Segments<S>) -> Result<(), String> {
for seg in segments.iter() {
self.push(seg)?;
}
Ok(())
}
fn push<S: Seg>(&mut self, seg: &S) -> Result<(), String> {
let Some((ty, name)) = seg.split() else {
return Ok(());
};
if ty == "db" {
return Ok(());
}
if let Some(prev) = self.seen.insert(name.to_string(), ty.to_string())
&& prev != ty
{
return Err(format!("placeholder `{name}` used with conflicting types"));
}
self.fields.push(ApiField {
name: name.to_string(),
serde_with: None,
field_type: map_placeholder_type(ty).to_string(),
description: None,
});
Ok(())
}
fn into_fields(self) -> Vec<ApiField> {
self.fields
}
}
fn map_placeholder_type(ty: &str) -> &'static str {
match ty {
"bool" => "bool",
"u8" => "u8",
"u16" => "u16",
"u32" => "u32",
"u64" => "u64",
"i8" => "i8",
"i16" => "i16",
"i32" => "i32",
"i64" => "i64",
"f32" => "f32",
"f64" => "f64",
"bytes" => "::myrmic_sdk::Bytes",
"json" => "::myrmic_sdk::JsonValue",
_ => "::myrmic_sdk::String",
}
}
#[cfg(test)]
mod tests;