use std::collections::{BTreeMap, BTreeSet};
use cratestack_core::Field;
use quote::quote;
use crate::shared::ident;
use super::patch_field::render_patch_field;
pub(crate) struct RenderedUpdateMessage {
pub(crate) tokens: proc_macro2::TokenStream,
}
pub(crate) fn render_update_message(
message_name: &str,
domain_path: proc_macro2::TokenStream,
fields: &[&Field],
numbers: &BTreeMap<String, i32>,
enum_names: &BTreeSet<&str>,
) -> Result<RenderedUpdateMessage, String> {
let ident_tok = ident(message_name);
let mut prost_fields = Vec::with_capacity(fields.len());
let mut from_domain_inits = Vec::with_capacity(fields.len());
let mut try_from_wire_lets = Vec::with_capacity(fields.len());
let mut try_from_wire_inits = Vec::with_capacity(fields.len());
for field in fields {
let number = *numbers
.get(&field.name)
.ok_or_else(|| format!("no `.pb.lock` entry for `{message_name}.{}`", field.name))?;
let plan = render_patch_field(message_name, field, number, enum_names);
prost_fields.push(plan.prost_field);
from_domain_inits.push(plan.from_domain_init);
try_from_wire_lets.push(plan.try_from_wire_let);
let field_ident = ident(&field.name);
try_from_wire_inits.push(quote! { #field_ident, });
}
let tokens = quote! {
#[derive(Clone, PartialEq, ::cratestack::grpc::prost::Message)]
pub struct #ident_tok {
#(#prost_fields)*
}
impl ::core::convert::From<&#domain_path> for #ident_tok {
fn from(value: &#domain_path) -> Self {
Self {
#(#from_domain_inits)*
}
}
}
impl ::core::convert::TryFrom<#ident_tok> for #domain_path {
type Error = ::cratestack::CoolError;
fn try_from(value: #ident_tok) -> ::core::result::Result<Self, Self::Error> {
#(#try_from_wire_lets)*
Ok(Self {
#(#try_from_wire_inits)*
})
}
}
};
Ok(RenderedUpdateMessage { tokens })
}
#[cfg(test)]
mod tests {
use cratestack_core::TypeArity;
use super::*;
fn field(name: &str, ty: &str, arity: TypeArity) -> Field {
Field {
docs: vec![],
name: name.to_owned(),
name_span: cratestack_core::SourceSpan {
start: 0,
end: 0,
line: 0,
},
ty: cratestack_core::TypeRef {
name: ty.to_owned(),
name_span: cratestack_core::SourceSpan {
start: 0,
end: 0,
line: 0,
},
arity,
generic_args: vec![],
int_args: Vec::new(),
},
attributes: Vec::new(),
span: cratestack_core::SourceSpan {
start: 0,
end: 0,
line: 0,
},
}
}
#[test]
fn required_field_wraps_in_single_option() {
let name_field = field("name", "String", TypeArity::Required);
let fields = vec![&name_field];
let numbers = BTreeMap::from([("name".to_owned(), 1)]);
let rendered = render_update_message(
"UpdateWidgetInput",
quote! { super::super::UpdateWidgetInput },
&fields,
&numbers,
&BTreeSet::new(),
)
.expect("should render");
let rendered_str = rendered.tokens.to_string();
assert!(rendered_str.contains("pub name : Option < String >"));
assert!(!rendered_str.contains("Some (Some ("));
}
#[test]
fn optional_field_double_wraps_on_decode() {
let email_field = field("email", "String", TypeArity::Optional);
let fields = vec![&email_field];
let numbers = BTreeMap::from([("email".to_owned(), 1)]);
let rendered = render_update_message(
"UpdateWidgetInput",
quote! { super::super::UpdateWidgetInput },
&fields,
&numbers,
&BTreeSet::new(),
)
.expect("should render");
let rendered_str = rendered.tokens.to_string();
assert!(rendered_str.contains("Some (Some ("));
assert!(!rendered_str.contains("Some (None)"));
}
#[test]
fn missing_lock_entry_is_reported_not_panicked() {
let name_field = field("name", "String", TypeArity::Required);
let fields = vec![&name_field];
let result = render_update_message(
"UpdateWidgetInput",
quote! { super::super::UpdateWidgetInput },
&fields,
&BTreeMap::new(),
&BTreeSet::new(),
);
assert!(result.is_err());
}
}