use proc_macro2::TokenStream;
use quote::quote;
use syn::visit::Visit;
use syn::{Data, DeriveInput, Fields, parse_quote};
struct RecursiveType<'a> {
name: &'a syn::Ident,
found: bool,
}
impl<'ast> Visit<'ast> for RecursiveType<'_> {
fn visit_type_path(&mut self, path: &'ast syn::TypePath) {
self.found |= path
.path
.segments
.iter()
.any(|segment| segment.ident == "Self" || segment.ident == *self.name);
syn::visit::visit_type_path(self, path);
}
}
struct Projection {
transparent: bool,
bytes: bool,
}
fn attributes(
attrs: &[syn::Attribute],
allow_transparent: bool,
allow_borrow: bool,
allow_bytes: bool,
) -> syn::Result<Projection> {
let mut transparent = false;
let mut borrow = false;
let mut bytes = false;
let mut deny_unknown = false;
for attr in attrs {
if attr.path().is_ident("wire") {
return Err(syn::Error::new_spanned(
attr,
"Wire follows Serde order; wire IDs are not supported",
));
}
if attr.path().is_ident("serde") {
attr.parse_nested_meta(|meta| {
if allow_transparent && meta.path.is_ident("transparent") && !transparent {
transparent = true;
Ok(())
} else if allow_transparent && meta.path.is_ident("deny_unknown_fields") && !deny_unknown {
deny_unknown = true;
Ok(())
} else if allow_bytes && meta.path.is_ident("with") && !bytes {
let adapter = meta.value()?.parse::<syn::LitStr>()?;
if !matches!(adapter.value().as_str(), "serde_bytes" | "::serde_bytes") {
return Err(meta.error("unsupported Serde byte adapter; custom layouts require explicit verified metadata"));
}
bytes = true;
Ok(())
} else if allow_borrow && meta.path.is_ident("borrow") && !borrow {
if meta.input.peek(syn::Token![=]) {
meta.value()?.parse::<syn::LitStr>()?;
}
borrow = true;
Ok(())
} else {
Err(meta.error("unsupported Serde attribute for Wire; use an explicit verified Wire implementation for custom layouts"))
}
})?;
}
}
Ok(Projection { transparent, bytes })
}
fn fields(fields: &Fields, context: &syn::Ident, transparent: bool) -> syn::Result<TokenStream> {
let values: Vec<_> = fields
.iter()
.map(|f| {
let ty = &f.ty;
let projection = attributes(&f.attrs, false, true, true)?;
Ok(if projection.bytes {
quote!(#context.describe_bytes::<#ty>()?)
} else {
quote!(#context.describe::<#ty>()?)
})
})
.collect::<syn::Result<_>>()?;
let json = quote!(::ic_wire::__private::serde_json::json);
if transparent {
if values.len() != 1 {
return Err(syn::Error::new_spanned(
fields,
"Wire transparent structs require exactly one field",
));
}
return Ok(values[0].clone());
}
Ok(match fields {
Fields::Unit => quote!(#json!({"k":"unit"})),
Fields::Named(fields) => {
let names = fields.named.iter().map(|f| {
f.ident
.as_ref()
.unwrap()
.to_string()
.trim_start_matches("r#")
.to_owned()
});
quote!(#json!({"k":"record","fields":[#({"name":#names,"value":#values}),*]}))
}
Fields::Unnamed(_) if values.len() == 1 => {
let value = &values[0];
quote!(#json!({"k":"newtype","item":#value}))
}
Fields::Unnamed(_) => quote!(#json!({"k":"tuple","items":[#(#values),*]})),
})
}
pub(super) fn expand(input: DeriveInput) -> syn::Result<TokenStream> {
let transparent = attributes(&input.attrs, true, false, false)?.transparent;
let context = super::fresh_ident("e!(#input).to_string(), "__ic_wire_schema");
let body = match &input.data {
Data::Struct(data) => fields(&data.fields, &context, transparent)?,
Data::Enum(data) if !transparent && !data.variants.is_empty() => {
let mut variants = Vec::new();
for (id, variant) in data.variants.iter().enumerate() {
let newtype = matches!(&variant.fields, Fields::Unnamed(f) if f.unnamed.len() == 1);
attributes(&variant.attrs, false, newtype, false)?;
let name = variant
.ident
.to_string()
.trim_start_matches("r#")
.to_owned();
let value = fields(&variant.fields, &context, newtype)?;
variants.push(quote!({"name":#name,"id":#id,"value":#value}));
}
quote!(
::ic_wire::__private::serde_json::json!({"k":"enum","variants":[#(#variants),*]})
)
}
_ => {
return Err(syn::Error::new_spanned(
&input,
"Wire supports structs and nonempty externally tagged enums",
));
}
};
let name = &input.ident;
let mut generics = input.generics.clone();
let fields: Vec<_> = match &input.data {
Data::Struct(data) => data.fields.iter().collect(),
Data::Enum(data) => data.variants.iter().flat_map(|v| &v.fields).collect(),
Data::Union(_) => unreachable!(),
};
let mut recursive = false;
for field in fields {
let mut visitor = RecursiveType { name, found: false };
visitor.visit_type(&field.ty);
recursive |= visitor.found;
if !visitor.found {
let ty = &field.ty;
let bound = if attributes(&field.attrs, false, true, true)?.bytes {
quote!(::ic_wire::typescript::SerdeBytesField)
} else {
quote!(::ic_wire::typescript::Wire)
};
generics
.make_where_clause()
.predicates
.push(parse_quote!(#ty: #bound));
}
}
if recursive {
for parameter in generics.type_params_mut() {
parameter
.bounds
.push(parse_quote!(::ic_wire::typescript::Wire));
}
}
generics
.make_where_clause()
.predicates
.push(parse_quote!(Self: ::ic_wire::__private::serde::Serialize));
let (implementation, ty, constraints) = generics.split_for_impl();
Ok(quote! {
impl #implementation ::ic_wire::typescript::Wire for #name #ty #constraints {
fn native_schema(#context: &mut ::ic_wire::typescript::SchemaContext)
-> ::core::result::Result<::ic_wire::__private::serde_json::Value, ::ic_wire::typescript::GenerationError>
{
let schema = #body;
::core::result::Result::Ok(schema)
}
}
})
}
#[cfg(test)]
mod tests {
#[test]
fn rejects_unsupported_attributes_and_shapes() {
for source in [
"#[serde(untagged)] enum E { A(u8) }",
"#[serde(tag = \"kind\")] enum E { A }",
"#[serde(rename_all = \"snake_case\")] struct A { a: u8 }",
"struct A { #[serde(skip)] a: u8 }",
"struct A { #[serde(flatten)] a: u8 }",
"struct A { #[serde(with = \"custom\")] a: u8 }",
"struct A { #[serde(serialize_with = \"custom\")] a: u8 }",
"struct A { #[serde(deny_unknown_fields)] a: u8 }",
"#[serde(deny_unknown_fields, deny_unknown_fields)] struct A { a: u8 }",
"struct A { #[serde(with = \"serde_bytes\", with = \"serde_bytes\")] a: Vec<u8> }",
"struct A { #[serde(with = \"serde_bytes\", deserialize_with = \"custom\")] a: Vec<u8> }",
"#[serde(borrow)] struct A { a: u8 }",
"enum A { #[serde(borrow)] B }",
"enum A { #[serde(borrow)] B(u8, u8) }",
"enum A { #[serde(borrow)] B { a: u8 } }",
"struct A { #[serde(borrow, borrow)] a: u8 }",
"struct A { #[serde(borrow = 1)] a: u8 }",
"struct A { #[serde(borrow, with = \"custom\")] a: u8 }",
"struct A { #[wire(id = 0)] a: u8 }",
"#[serde(transparent)] struct A(u8, u8);",
"enum E {}",
] {
assert!(
super::expand(syn::parse_str(source).unwrap()).is_err(),
"{source}"
);
}
}
}