use std::collections::BTreeMap;
use proc_macro2::TokenStream as Ts;
use quote::ToTokens;
use schemars::schema::RootSchema;
use syn::visit_mut::{self, VisitMut};
use typify_impl::{MapType, TypeSpace, TypeSpaceSettings};
pub(super) fn generate(
root: &Ts,
schema: RootSchema,
) -> Result<(Ts, BTreeMap<String, Ts>), String> {
let mut settings = TypeSpaceSettings::default();
settings
.with_derive(format!("{root}::Message"))
.with_map_type(MapType::new("::std::collections::BTreeMap"));
let mut type_space = TypeSpace::new(&settings);
type_space
.add_root_schema(schema)
.map_err(|err| format!("invalid `types` JSON Schema: {err}"))?;
if type_space.uses_regress() {
return Err(
"bridge `types` use a string `pattern` (regex), which is not supported yet".to_string(),
);
}
let mut rw = ExportsRewriter;
let idents = type_space
.iter_types()
.map(|t| {
let mut ty: syn::Type =
syn::parse2(t.ident()).expect("typify type ident should parse as a type");
rw.visit_type_mut(&mut ty);
(t.name(), ty.to_token_stream())
})
.collect();
let mut file: syn::File = syn::parse2(type_space.to_stream())
.map_err(|err| format!("typify output did not parse as a Rust file: {err}"))?;
rw.visit_file_mut(&mut file);
Ok((file.to_token_stream(), idents))
}
struct ExportsRewriter;
impl VisitMut for ExportsRewriter {
fn visit_type_mut(&mut self, ty: &mut syn::Type) {
if let syn::Type::Path(type_path) = ty
&& let Some(qself) = &mut type_path.qself
{
self.visit_type_mut(&mut qself.ty);
let before = type_path.path.segments.len();
self.visit_path_mut(&mut type_path.path);
let added = type_path.path.segments.len().saturating_sub(before);
qself.position += added;
return;
}
visit_mut::visit_type_mut(self, ty);
}
fn visit_path_mut(&mut self, path: &mut syn::Path) {
visit_mut::visit_path_mut(self, path);
if path.leading_colon.is_some() {
if path
.segments
.first()
.is_some_and(|s| s.ident == "myrmic_sdk")
{
return;
}
let mut relative = path.clone();
relative.leading_colon = None;
*path = syn::parse_quote!(::myrmic_sdk::codegen::exports::#relative);
} else if path.segments.len() == 1 {
let seg = &path.segments[0];
let module = match seg.ident.to_string().as_str() {
"String" => "string",
"Vec" => "vec",
"Box" => "boxed",
_ => return,
};
let module = syn::Ident::new(module, seg.ident.span());
let ty = seg.ident.clone();
let args = seg.arguments.clone();
*path = syn::parse_quote!(::myrmic_sdk::codegen::exports::std::#module::#ty);
path.segments.last_mut().unwrap().arguments = args;
}
}
fn visit_attribute_mut(&mut self, attr: &mut syn::Attribute) {
if attr.path().is_ident("derive") {
let paths = attr.parse_args_with(
syn::punctuated::Punctuated::<syn::Path, syn::Token![,]>::parse_terminated,
);
if let Ok(mut paths) = paths {
for p in paths.iter_mut() {
self.visit_path_mut(p);
}
attr.meta = syn::parse_quote!(derive(#paths));
}
} else if attr.path().is_ident("serde") {
if let syn::Meta::List(list) = &mut attr.meta {
list.tokens = redirect_std_in_strings(std::mem::take(&mut list.tokens));
}
}
}
fn visit_item_struct_mut(&mut self, item: &mut syn::ItemStruct) {
visit_mut::visit_item_struct_mut(self, item);
inject_serde_crate(&mut item.attrs);
}
fn visit_item_enum_mut(&mut self, item: &mut syn::ItemEnum) {
visit_mut::visit_item_enum_mut(self, item);
inject_serde_crate(&mut item.attrs);
}
}
fn inject_serde_crate(attrs: &mut Vec<syn::Attribute>) {
let idx = attrs
.iter()
.position(|a| a.path().is_ident("derive") && derive_mentions_serde(a));
let Some(idx) = idx else {
return;
};
attrs.insert(
idx + 1,
syn::Attribute {
pound_token: syn::token::Pound::default(),
style: syn::AttrStyle::Outer,
bracket_token: syn::token::Bracket::default(),
meta: syn::parse_quote!(serde(crate = "::myrmic_sdk::codegen::exports::serde")),
},
);
}
fn derive_mentions_serde(attr: &syn::Attribute) -> bool {
attr.parse_args_with(syn::punctuated::Punctuated::<syn::Path, syn::Token![,]>::parse_terminated)
.is_ok_and(|paths| {
paths
.iter()
.any(|p| p.segments.iter().any(|s| s.ident == "serde"))
})
}
fn redirect_std_in_strings(ts: Ts) -> Ts {
use proc_macro2::{Group, Literal, TokenTree};
ts.into_iter()
.map(|tt| match tt {
TokenTree::Group(g) => {
let mut regrouped = Group::new(g.delimiter(), redirect_std_in_strings(g.stream()));
regrouped.set_span(g.span());
TokenTree::Group(regrouped)
}
TokenTree::Literal(lit) => match syn::Lit::new(lit.clone()) {
syn::Lit::Str(s) if s.value().contains("::std::") => {
let v = s
.value()
.replace("::std::", "::myrmic_sdk::codegen::exports::std::");
TokenTree::Literal(Literal::string(&v))
}
_ => TokenTree::Literal(lit),
},
other => other,
})
.collect()
}