Skip to main content

ic_wire_derive/
lib.rs

1//! Explicit-layout derives for IC Wire contracts.
2use proc_macro::TokenStream;
3use quote::{format_ident, quote};
4use std::collections::BTreeSet;
5use syn::{Data, DeriveInput, Fields, parse_macro_input};
6
7/// Derive a bounded record or tagged enum. Fields/variants require `#[wire(id = N)]`.
8#[proc_macro_derive(Wire, attributes(wire))]
9pub fn derive_wire(input: TokenStream) -> TokenStream {
10    expand(parse_macro_input!(input as DeriveInput))
11        .unwrap_or_else(syn::Error::into_compile_error)
12        .into()
13}
14
15fn identity(attrs: &[syn::Attribute]) -> syn::Result<u32> {
16    let mut id = None;
17    for attr in attrs.iter().filter(|a| a.path().is_ident("wire")) {
18        attr.parse_nested_meta(|meta| {
19            if !meta.path.is_ident("id") || id.is_some() {
20                return Err(meta.error("expected one wire id"));
21            }
22            id = Some(meta.value()?.parse::<syn::LitInt>()?.base10_parse()?);
23            Ok(())
24        })?;
25    }
26    id.ok_or_else(|| {
27        syn::Error::new(
28            proc_macro2::Span::call_site(),
29            "explicit #[wire(id = N)] required",
30        )
31    })
32}
33
34fn expand(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
35    if !input.generics.params.is_empty() || input.generics.where_clause.is_some() {
36        return Err(syn::Error::new_spanned(
37            input.generics,
38            "Wire requires concrete contracts",
39        ));
40    }
41    if input.attrs.iter().any(|a| a.path().is_ident("wire")) {
42        return Err(syn::Error::new_spanned(
43            &input.ident,
44            "wire ids belong on fields or variants",
45        ));
46    }
47    let name = input.ident;
48    let mut ids = BTreeSet::new();
49    let (serialize, deserialize, shape, schema) = match input.data {
50        Data::Struct(data) => {
51            let Fields::Named(fields) = data.fields else {
52                return Err(syn::Error::new_spanned(
53                    name,
54                    "use a named-field record (empty records are allowed)",
55                ));
56            };
57            if fields.named.len() > 16 {
58                return Err(syn::Error::new_spanned(
59                    name,
60                    "at most 16 fields; group related fields in a nested record",
61                ));
62            }
63            let mut fields = fields
64                .named
65                .into_iter()
66                .map(|field| {
67                    let id = identity(&field.attrs)?;
68                    if !ids.insert(id) {
69                        return Err(syn::Error::new_spanned(&field, "duplicate wire id"));
70                    }
71                    Ok((id, field.ident.unwrap(), field.ty))
72                })
73                .collect::<syn::Result<Vec<_>>>()?;
74            fields.sort_by_key(|f| f.0);
75            let names: Vec<_> = fields.iter().map(|f| &f.1).collect();
76            let types: Vec<_> = fields.iter().map(|f| &f.2).collect();
77            let labels: Vec<_> = names
78                .iter()
79                .map(|name| name.to_string().trim_start_matches("r#").to_owned())
80                .collect();
81            let field_ids: Vec<_> = fields.iter().map(|f| f.0).collect();
82            (
83                quote! { ::ic_wire::__private::serde::Serialize::serialize(&(#(&self.#names,)*), serializer) },
84                quote! {
85                    let (#(#names,)*): (#(#types,)*) = ::ic_wire::__private::serde::Deserialize::deserialize(deserializer)?;
86                    Ok(Self { #(#names,)* })
87                },
88                quote! { ::ic_wire::Shape::record(&[#(<#types as ::ic_wire::Wire>::SHAPE,)*]) },
89                quote! { ::ic_wire::schema::Schema::Record(&[#(::ic_wire::schema::Field { name: #labels, id: #field_ids, value: &<#types as ::ic_wire::Wire>::SCHEMA },)*]) },
90            )
91        }
92        Data::Enum(data) => {
93            if data.variants.is_empty() {
94                return Err(syn::Error::new_spanned(name, "empty enum is unsupported"));
95            }
96            let visitor = format_ident!("__IcWireVisitorFor{}", name);
97            let mut serializers = Vec::new();
98            let mut decoders = Vec::new();
99            let mut shapes = Vec::new();
100            let mut variants = Vec::new();
101            for variant in data.variants {
102                let id = identity(&variant.attrs)?;
103                if !ids.insert(id) {
104                    return Err(syn::Error::new_spanned(&variant, "duplicate wire id"));
105                }
106                if variant.discriminant.is_some() {
107                    return Err(syn::Error::new_spanned(
108                        &variant,
109                        "use wire id, not Rust discriminants",
110                    ));
111                }
112                let v = variant.ident;
113                let label = v.to_string().trim_start_matches("r#").to_owned();
114                match variant.fields {
115                    Fields::Unit => {
116                        serializers.push(quote! { Self::#v => ::ic_wire::__private::serde::Serialize::serialize(&(#id, ()), serializer) });
117                        decoders.push(quote! { #id => { let _: () = seq.next_element()?.ok_or_else(|| ::ic_wire::__private::serde::de::Error::custom("missing variant payload"))?; Ok(#name::#v) } });
118                        shapes.push(quote! { <() as ::ic_wire::Wire>::SHAPE });
119                        variants.push(quote! { ::ic_wire::schema::Variant { name: #label, id: #id, value: &<() as ::ic_wire::Wire>::SCHEMA } });
120                    }
121                    Fields::Unnamed(fields) if fields.unnamed.len() == 1 => {
122                        let field = &fields.unnamed[0];
123                        if field.attrs.iter().any(|a| a.path().is_ident("wire")) {
124                            return Err(syn::Error::new_spanned(
125                                field,
126                                "variant payload inherits its variant id",
127                            ));
128                        }
129                        let ty = &field.ty;
130                        serializers.push(quote! { Self::#v(value) => ::ic_wire::__private::serde::Serialize::serialize(&(#id, value), serializer) });
131                        decoders.push(quote! { #id => Ok(#name::#v(seq.next_element::<#ty>()?.ok_or_else(|| ::ic_wire::__private::serde::de::Error::custom("missing variant payload"))?)) });
132                        shapes.push(quote! { <#ty as ::ic_wire::Wire>::SHAPE });
133                        variants.push(quote! { ::ic_wire::schema::Variant { name: #label, id: #id, value: &<#ty as ::ic_wire::Wire>::SCHEMA } });
134                    }
135                    _ => {
136                        return Err(syn::Error::new_spanned(
137                            v,
138                            "use unit variants or one named-record payload",
139                        ));
140                    }
141                }
142            }
143            (
144                quote! { match self { #(#serializers,)* } },
145                quote! {
146                    struct #visitor;
147                    impl<'de> ::ic_wire::__private::serde::de::Visitor<'de> for #visitor {
148                        type Value = #name;
149                        fn expecting(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { f.write_str("explicit wire variant") }
150                        fn visit_seq<A: ::ic_wire::__private::serde::de::SeqAccess<'de>>(self, mut seq: A) -> ::core::result::Result<Self::Value, A::Error> {
151                            let tag: u32 = seq.next_element()?.ok_or_else(|| ::ic_wire::__private::serde::de::Error::custom("missing variant id"))?;
152                            match tag { #(#decoders,)* _ => Err(::ic_wire::__private::serde::de::Error::custom("unknown variant id")) }
153                        }
154                    }
155                    deserializer.deserialize_tuple(2, #visitor)
156                },
157                quote! { ::ic_wire::Shape::choice(5, &[#(#shapes,)*]) },
158                quote! { ::ic_wire::schema::Schema::Enum(&[#(#variants,)*]) },
159            )
160        }
161        Data::Union(_) => return Err(syn::Error::new_spanned(name, "unions are unsupported")),
162    };
163    Ok(quote! {
164        impl ::ic_wire::__private::serde::Serialize for #name {
165            fn serialize<S: ::ic_wire::__private::serde::Serializer>(&self, serializer: S) -> ::core::result::Result<S::Ok, S::Error> { #serialize }
166        }
167        impl<'de> ::ic_wire::__private::serde::Deserialize<'de> for #name {
168            fn deserialize<D: ::ic_wire::__private::serde::Deserializer<'de>>(deserializer: D) -> ::core::result::Result<Self, D::Error> { #deserialize }
169        }
170        impl ::ic_wire::Wire for #name {
171            const SHAPE: ::ic_wire::Shape = #shape;
172            const SCHEMA: ::ic_wire::schema::Schema = #schema;
173        }
174    })
175}
176
177#[cfg(feature = "adapter-fixture")]
178mod adapter_fixture;
179
180/// Evaluation-only query adapter; not a supported Canic macro API.
181#[cfg(feature = "adapter-fixture")]
182#[proc_macro_attribute]
183pub fn query(args: TokenStream, item: TokenStream) -> TokenStream {
184    adapter_fixture::expand(args.into(), item.into(), false)
185        .unwrap_or_else(syn::Error::into_compile_error)
186        .into()
187}
188
189/// Evaluation-only sync/async update adapter; not a supported Canic macro API.
190#[cfg(feature = "adapter-fixture")]
191#[proc_macro_attribute]
192pub fn update(args: TokenStream, item: TokenStream) -> TokenStream {
193    adapter_fixture::expand(args.into(), item.into(), true)
194        .unwrap_or_else(syn::Error::into_compile_error)
195        .into()
196}