Skip to main content

wip_interface_macros/
lib.rs

1//! Procedural macros backing the public `wip-interface` authoring facade.
2//!
3//! This is an implementation crate. Applications should depend on
4//! `wip-interface`, which re-exports both macros alongside the traits and types
5//! used by their generated code.
6
7#![deny(missing_docs)]
8
9use std::collections::HashSet;
10
11use proc_macro::TokenStream;
12use proc_macro2::{Span, TokenStream as TokenStream2};
13use quote::{format_ident, quote};
14use syn::ext::IdentExt;
15use syn::spanned::Spanned;
16use syn::visit::{self, Visit};
17use syn::{
18    Attribute, Data, DataEnum, DeriveInput, Error, Fields, FnArg, GenericArgument, Ident,
19    ItemTrait, Lit, Meta, Pat, PathArguments, ReturnType, TraitItem, TraitItemFn, Type,
20    parse_macro_input,
21};
22
23/// Declares a public Rust trait as a WIP interface.
24#[proc_macro_attribute]
25pub fn interface(attribute: TokenStream, item: TokenStream) -> TokenStream {
26    let attribute = TokenStream2::from(attribute);
27    let item = parse_macro_input!(item as ItemTrait);
28
29    match expand_interface(attribute, item) {
30        Ok(tokens) => tokens.into(),
31        Err(error) => error.into_compile_error().into(),
32    }
33}
34
35/// Derives the WIP type declaration and logical value codec for a DTO.
36#[proc_macro_derive(WipType)]
37pub fn derive_wip_type(item: TokenStream) -> TokenStream {
38    let item = parse_macro_input!(item as DeriveInput);
39
40    match expand_wip_type(item) {
41        Ok(tokens) => tokens.into(),
42        Err(error) => error.into_compile_error().into(),
43    }
44}
45
46#[derive(Clone)]
47struct Parameter {
48    ident: Ident,
49    name: String,
50    ty: Type,
51    optional: bool,
52    documentation: Option<Documentation>,
53}
54
55struct Operation {
56    ident: Ident,
57    name: String,
58    arguments_ident: Ident,
59    parameters: Vec<Parameter>,
60    result_ty: Type,
61    unit_result: bool,
62    documentation: Option<Documentation>,
63    return_documentation: Option<Documentation>,
64}
65
66#[derive(Clone)]
67struct Documentation {
68    summary: String,
69    details: Option<String>,
70}
71
72fn expand_interface(attribute: TokenStream2, item: ItemTrait) -> syn::Result<TokenStream2> {
73    if !attribute.is_empty() {
74        return Err(Error::new_spanned(
75            attribute,
76            "the interface attribute does not accept arguments",
77        ));
78    }
79
80    validate_interface_header(&item)?;
81
82    let trait_ident = &item.ident;
83    let definition_ident = format_ident!("{}Definition", trait_ident, span = trait_ident.span());
84    let trait_documentation = documentation(&item.attrs);
85    let mut operation_names = HashSet::new();
86    let mut operations = Vec::new();
87
88    for trait_item in &item.items {
89        let method = match trait_item {
90            TraitItem::Fn(method) => method,
91            TraitItem::Type(item) => {
92                return Err(Error::new_spanned(
93                    item,
94                    "associated types are not supported in WIP interfaces",
95                ));
96            }
97            TraitItem::Const(item) => {
98                return Err(Error::new_spanned(
99                    item,
100                    "associated constants are not supported in WIP interfaces",
101                ));
102            }
103            other => {
104                return Err(Error::new_spanned(
105                    other,
106                    "only operation methods are supported in WIP interfaces",
107                ));
108            }
109        };
110
111        let operation = parse_operation(trait_ident, method)?;
112        if !operation_names.insert(operation.name.clone()) {
113            return Err(Error::new(
114                operation.ident.span(),
115                format!("duplicate operation name `{}`", operation.name),
116            ));
117        }
118        operations.push(operation);
119    }
120
121    // PascalCase normalization is convenient for the public generated item,
122    // but distinct Rust names such as `item` and `item_` normalize identically.
123    // Add a stable source-name suffix to every member of a colliding group so
124    // otherwise-valid operations always produce distinct public Rust items.
125    let mut seen_argument_items = HashSet::new();
126    let mut colliding_argument_items = HashSet::new();
127    for operation in &operations {
128        let name = operation.arguments_ident.to_string();
129        if !seen_argument_items.insert(name.clone()) {
130            colliding_argument_items.insert(name);
131        }
132    }
133    for operation in &mut operations {
134        if colliding_argument_items.contains(&operation.arguments_ident.to_string()) {
135            let suffix = operation
136                .name
137                .as_bytes()
138                .iter()
139                .map(|byte| format!("{byte:02x}"))
140                .collect::<String>();
141            operation.arguments_ident = format_ident!(
142                "{}{}ArgumentsN{}",
143                trait_ident,
144                pascal_case(&operation.name),
145                suffix,
146                span = operation.ident.span()
147            );
148        }
149    }
150
151    let argument_items = operations.iter().map(argument_item);
152    let operation_declarations = operations.iter().map(operation_declaration);
153    let declaration_collectors = operations.iter().flat_map(operation_collectors);
154    let dispatch_arms = operations.iter().map(dispatch_arm);
155    let trait_doc = documentation_tokens(trait_documentation.as_ref());
156    let mut emitted_item = item.clone();
157    strip_parameter_documentation(&mut emitted_item);
158
159    Ok(quote! {
160        #emitted_item
161
162        #[doc = concat!("Generated WIP interface definition for [`", stringify!(#trait_ident), "`].")]
163        pub struct #definition_ident;
164
165        impl ::wip_interface::InterfaceDefinition for #definition_ident {
166            fn descriptor() -> ::wip_interface::wip_protocol::InterfaceDescriptor {
167                let mut declarations = ::std::vec::Vec::new();
168                #(#declaration_collectors)*
169
170                ::wip_interface::wip_protocol::InterfaceDescriptor {
171                    format: ::std::string::String::from(
172                        ::wip_interface::wip_protocol::INTERFACE_FORMAT_V1,
173                    ),
174                    documentation: #trait_doc,
175                    types: declarations,
176                    operations: ::std::vec![#(#operation_declarations),*],
177                }
178            }
179        }
180
181        impl<T: #trait_ident> ::wip_interface::InterfaceImplementation<T> for #definition_ident {
182            fn dispatch(
183                implementation: &T,
184                operation: &str,
185                arguments: ::std::collections::BTreeMap<
186                    ::std::string::String,
187                    ::wip_interface::wip_protocol::Value,
188                >,
189            ) -> ::std::result::Result<
190                ::wip_interface::wip_protocol::Value,
191                ::wip_interface::DispatchError,
192            > {
193                let descriptor = Self::descriptor();
194                match operation {
195                    #(#dispatch_arms)*
196                    _ => ::std::result::Result::Err(
197                        ::wip_interface::DispatchError::UnknownOperation(
198                            ::std::string::String::from(operation),
199                        ),
200                    ),
201                }
202            }
203        }
204
205        impl #definition_ident {
206            /// Builds the interface descriptor generated from the trait declaration.
207            pub fn descriptor() -> ::wip_interface::wip_protocol::InterfaceDescriptor {
208                <Self as ::wip_interface::InterfaceDefinition>::descriptor()
209            }
210
211            /// Dispatches one validated logical operation call to a typed implementation.
212            pub fn dispatch<T: #trait_ident>(
213                implementation: &T,
214                operation: &str,
215                arguments: ::std::collections::BTreeMap<
216                    ::std::string::String,
217                    ::wip_interface::wip_protocol::Value,
218                >,
219            ) -> ::std::result::Result<
220                ::wip_interface::wip_protocol::Value,
221                ::wip_interface::DispatchError,
222            > {
223                <Self as ::wip_interface::InterfaceImplementation<T>>::dispatch(
224                    implementation,
225                    operation,
226                    arguments,
227                )
228            }
229        }
230
231        #(#argument_items)*
232    })
233}
234
235fn strip_parameter_documentation(item: &mut ItemTrait) {
236    for trait_item in &mut item.items {
237        let TraitItem::Fn(method) = trait_item else {
238            continue;
239        };
240        for input in &mut method.sig.inputs {
241            if let FnArg::Typed(typed) = input {
242                typed
243                    .attrs
244                    .retain(|attribute| !attribute.path().is_ident("doc"));
245            }
246        }
247    }
248}
249
250fn validate_interface_header(item: &ItemTrait) -> syn::Result<()> {
251    if !matches!(item.vis, syn::Visibility::Public(_)) {
252        return Err(Error::new_spanned(
253            &item.vis,
254            "a WIP interface trait must be public",
255        ));
256    }
257    if item.unsafety.is_some() {
258        return Err(Error::new_spanned(
259            item.unsafety,
260            "unsafe interface traits are not supported",
261        ));
262    }
263    if item.auto_token.is_some() {
264        return Err(Error::new_spanned(
265            item.auto_token,
266            "auto traits are not supported as WIP interfaces",
267        ));
268    }
269    if !item.generics.params.is_empty() || item.generics.where_clause.is_some() {
270        return Err(Error::new_spanned(
271            &item.generics,
272            "generic WIP interface traits are not supported",
273        ));
274    }
275    if item.colon_token.is_some() || !item.supertraits.is_empty() {
276        return Err(Error::new_spanned(
277            &item.supertraits,
278            "WIP interface trait inheritance is not supported",
279        ));
280    }
281    Ok(())
282}
283
284fn parse_operation(trait_ident: &Ident, method: &TraitItemFn) -> syn::Result<Operation> {
285    let signature = &method.sig;
286
287    if signature.constness.is_some() {
288        return Err(Error::new_spanned(
289            signature.constness,
290            "const operations are not supported",
291        ));
292    }
293    if signature.asyncness.is_some() {
294        return Err(Error::new_spanned(
295            signature.asyncness,
296            "async operations are not supported",
297        ));
298    }
299    if signature.unsafety.is_some() {
300        return Err(Error::new_spanned(
301            signature.unsafety,
302            "unsafe operations are not supported",
303        ));
304    }
305    if signature.abi.is_some() {
306        return Err(Error::new_spanned(
307            &signature.abi,
308            "extern operations are not supported",
309        ));
310    }
311    if signature.variadic.is_some() {
312        return Err(Error::new_spanned(
313            &signature.variadic,
314            "variadic operations are not supported",
315        ));
316    }
317    if !signature.generics.params.is_empty() || signature.generics.where_clause.is_some() {
318        return Err(Error::new_spanned(
319            &signature.generics,
320            "generic operations are not supported",
321        ));
322    }
323    if let Some(default) = &method.default {
324        return Err(Error::new_spanned(
325            default,
326            "WIP operation methods must not have a body",
327        ));
328    }
329
330    let mut inputs = signature.inputs.iter();
331    let receiver = inputs.next().ok_or_else(|| {
332        Error::new(
333            signature.ident.span(),
334            "a WIP operation must have an `&self` receiver",
335        )
336    })?;
337    validate_receiver(receiver)?;
338
339    let mut parameters = Vec::new();
340    let mut parameter_names = HashSet::new();
341    for input in inputs {
342        let typed = match input {
343            FnArg::Typed(typed) => typed,
344            FnArg::Receiver(receiver) => {
345                return Err(Error::new_spanned(
346                    receiver,
347                    "the `&self` receiver must be the first operation argument",
348                ));
349            }
350        };
351        let ident = match typed.pat.as_ref() {
352            Pat::Ident(pattern)
353                if pattern.by_ref.is_none()
354                    && pattern.mutability.is_none()
355                    && pattern.subpat.is_none() =>
356            {
357                pattern.ident.clone()
358            }
359            pattern => {
360                return Err(Error::new_spanned(
361                    pattern,
362                    "operation parameters must be simple named parameters",
363                ));
364            }
365        };
366        let name = source_name(&ident);
367        if !parameter_names.insert(name.clone()) {
368            return Err(Error::new(
369                ident.span(),
370                format!("duplicate operation parameter `{name}`"),
371            ));
372        }
373        let (optional, ty) = optional_type(typed.ty.as_ref(), "operation parameter")?;
374        validate_value_type(&ty, "operation parameter")?;
375        parameters.push(Parameter {
376            ident,
377            name,
378            ty,
379            optional,
380            documentation: documentation(&typed.attrs),
381        });
382    }
383
384    let result_ty = parse_operation_result(&signature.output)?;
385    let unit_result = is_unit(&result_ty);
386    if !unit_result {
387        validate_value_type(&result_ty, "operation return type")?;
388    }
389
390    let name = source_name(&signature.ident);
391    let method_docs = doc_lines(&method.attrs);
392    let (operation_doc_lines, return_doc_lines) = split_returns_section(method_docs);
393    let pascal_name = pascal_case(&name);
394    let arguments_ident = format_ident!(
395        "{}{}Arguments",
396        trait_ident,
397        pascal_name,
398        span = signature.ident.span()
399    );
400
401    Ok(Operation {
402        ident: signature.ident.clone(),
403        name,
404        arguments_ident,
405        parameters,
406        result_ty,
407        unit_result,
408        documentation: documentation_from_lines(operation_doc_lines),
409        return_documentation: documentation_from_lines(return_doc_lines),
410    })
411}
412
413fn validate_receiver(input: &FnArg) -> syn::Result<()> {
414    let receiver = match input {
415        FnArg::Receiver(receiver) => receiver,
416        FnArg::Typed(typed) => {
417            return Err(Error::new_spanned(
418                typed,
419                "a WIP operation must start with an `&self` receiver",
420            ));
421        }
422    };
423
424    let reference = receiver.reference.as_ref();
425    let is_plain_shared_reference = reference.is_some()
426        && receiver.mutability.is_none()
427        && receiver.colon_token.is_none()
428        && reference
429            .and_then(|(_, lifetime)| lifetime.as_ref())
430            .is_none();
431    if !is_plain_shared_reference {
432        return Err(Error::new_spanned(
433            receiver,
434            "the only supported receiver is exactly `&self`",
435        ));
436    }
437    Ok(())
438}
439
440fn parse_operation_result(output: &ReturnType) -> syn::Result<Type> {
441    let ty = match output {
442        ReturnType::Default => {
443            return Err(Error::new_spanned(
444                output,
445                "an operation must explicitly return `OperationResult<T>`",
446            ));
447        }
448        ReturnType::Type(_, ty) => ty.as_ref(),
449    };
450
451    reject_custom_lifetimes(ty, "operation return type")?;
452    let path = match ty {
453        Type::Path(path) if path.qself.is_none() => &path.path,
454        _ => {
455            return Err(Error::new_spanned(
456                ty,
457                "an operation must explicitly return `OperationResult<T>`",
458            ));
459        }
460    };
461    let segment = path.segments.last().ok_or_else(|| {
462        Error::new_spanned(
463            ty,
464            "an operation must explicitly return `OperationResult<T>`",
465        )
466    })?;
467    if segment.ident != "OperationResult" {
468        return Err(Error::new_spanned(
469            ty,
470            "an operation must explicitly return `OperationResult<T>`",
471        ));
472    }
473    let arguments = match &segment.arguments {
474        PathArguments::AngleBracketed(arguments) => arguments,
475        _ => {
476            return Err(Error::new_spanned(
477                segment,
478                "`OperationResult` must have exactly one application result type",
479            ));
480        }
481    };
482    let mut types = arguments.args.iter().filter_map(|argument| match argument {
483        GenericArgument::Type(ty) => Some(ty),
484        _ => None,
485    });
486    let result = types.next().cloned();
487    if arguments.args.len() != 1 || types.next().is_some() {
488        return Err(Error::new_spanned(
489            arguments,
490            "`OperationResult` must have exactly one application result type",
491        ));
492    }
493    let result = result.ok_or_else(|| {
494        Error::new_spanned(
495            arguments,
496            "`OperationResult` must have exactly one application result type",
497        )
498    })?;
499    if direct_option(&result)?.is_some() || contains_option(&result) {
500        return Err(Error::new_spanned(
501            result,
502            "`Option<T>` is only supported directly on DTO fields and operation parameters",
503        ));
504    }
505    Ok(result)
506}
507
508fn argument_item(operation: &Operation) -> TokenStream2 {
509    let arguments_ident = &operation.arguments_ident;
510    let fields = operation.parameters.iter().map(|parameter| {
511        let ident = &parameter.ident;
512        let name = &parameter.name;
513        let ty = parameter_rust_type(parameter);
514        let documentation = format!("Decoded `{name}` operation argument.");
515        quote!(
516            #[doc = #documentation]
517            pub #ident: #ty
518        )
519    });
520    let decoders = operation.parameters.iter().map(|parameter| {
521        let ident = &parameter.ident;
522        let name = &parameter.name;
523        let ty = &parameter.ty;
524        if parameter.optional {
525            quote!(#ident: decoder.optional::<#ty>(#name)?)
526        } else {
527            quote!(#ident: decoder.required::<#ty>(#name)?)
528        }
529    });
530    let result_ty = &operation.result_ty;
531    let encode_result = if operation.unit_result {
532        quote! {
533            let () = result;
534            ::std::result::Result::Ok(::wip_interface::wip_protocol::Value::Unit)
535        }
536    } else {
537        quote! {
538            <#result_ty as ::wip_interface::WipType>::encode(result)
539        }
540    };
541
542    quote! {
543        #[doc = concat!("Decoded arguments for `", stringify!(#arguments_ident), "`'s operation.")]
544        pub struct #arguments_ident {
545            #(#fields,)*
546        }
547
548        impl #arguments_ident {
549            /// Decodes named logical values into typed operation arguments.
550            pub fn decode(
551                arguments: ::std::collections::BTreeMap<
552                    ::std::string::String,
553                    ::wip_interface::wip_protocol::Value,
554                >,
555            ) -> ::std::result::Result<Self, ::wip_interface::CodecError> {
556                let mut decoder = ::wip_interface::__private::RecordDecoder::new(arguments);
557                let decoded = Self {
558                    #(#decoders,)*
559                };
560                decoder.finish()?;
561                ::std::result::Result::Ok(decoded)
562            }
563
564            /// Encodes this operation's application result.
565            pub fn encode_result(
566                result: #result_ty,
567            ) -> ::std::result::Result<
568                ::wip_interface::wip_protocol::Value,
569                ::wip_interface::CodecError,
570            > {
571                #encode_result
572            }
573        }
574    }
575}
576
577fn operation_declaration(operation: &Operation) -> TokenStream2 {
578    let name = &operation.name;
579    let documentation = documentation_tokens(operation.documentation.as_ref());
580    let parameters = operation.parameters.iter().map(|parameter| {
581        let parameter_name = &parameter.name;
582        let parameter_doc = documentation_tokens(parameter.documentation.as_ref());
583        let ty = &parameter.ty;
584        let optional = parameter.optional;
585        quote! {
586            ::wip_interface::wip_protocol::ParameterDeclaration {
587                name: ::std::string::String::from(#parameter_name),
588                required: !#optional,
589                documentation: #parameter_doc,
590                r#type: <#ty as ::wip_interface::WipType>::type_expr(),
591            }
592        }
593    });
594    let result_ty = &operation.result_ty;
595    let return_doc = documentation_tokens(operation.return_documentation.as_ref());
596    let returns = quote! {
597        ::wip_interface::wip_protocol::ReturnDeclaration {
598            documentation: #return_doc,
599            r#type: <#result_ty as ::wip_interface::WipType>::type_expr(),
600        }
601    };
602
603    quote! {
604        ::wip_interface::wip_protocol::OperationDeclaration {
605            name: ::std::string::String::from(#name),
606            documentation: #documentation,
607            parameters: ::std::vec![#(#parameters),*],
608            returns: #returns,
609        }
610    }
611}
612
613fn operation_collectors(operation: &Operation) -> Vec<TokenStream2> {
614    let mut collectors = operation
615        .parameters
616        .iter()
617        .map(|parameter| {
618            let ty = &parameter.ty;
619            quote!(<#ty as ::wip_interface::WipType>::collect_declarations(&mut declarations);)
620        })
621        .collect::<Vec<_>>();
622    if !operation.unit_result {
623        let ty = &operation.result_ty;
624        collectors.push(
625            quote!(<#ty as ::wip_interface::WipType>::collect_declarations(&mut declarations);),
626        );
627    }
628    collectors
629}
630
631fn dispatch_arm(operation: &Operation) -> TokenStream2 {
632    let operation_name = &operation.name;
633    let method_ident = &operation.ident;
634    let arguments_ident = &operation.arguments_ident;
635    let arguments = operation.parameters.iter().map(|parameter| {
636        let ident = &parameter.ident;
637        quote!(decoded.#ident)
638    });
639
640    quote! {
641        #operation_name => {
642            ::wip_interface::__private::validate_arguments(
643                &descriptor,
644                #operation_name,
645                &arguments,
646            )
647            .map_err(::wip_interface::DispatchError::InvalidArguments)?;
648            let decoded = #arguments_ident::decode(arguments)
649                .map_err(::wip_interface::DispatchError::Decode)?;
650            ::std::panic::catch_unwind(::std::panic::AssertUnwindSafe(|| {
651                let result = implementation.#method_ident(#(#arguments),*)
652                    .map_err(::wip_interface::DispatchError::Host)?;
653                let encoded = #arguments_ident::encode_result(result)
654                    .map_err(::wip_interface::DispatchError::Encode)?;
655                descriptor
656                    .validate_result(#operation_name, &encoded)
657                    .map_err(::wip_interface::DispatchError::InvalidResult)?;
658                ::std::result::Result::Ok(encoded)
659            }))
660            .map_err(|_| ::wip_interface::DispatchError::Panic)?
661        }
662    }
663}
664
665fn parameter_rust_type(parameter: &Parameter) -> TokenStream2 {
666    let ty = &parameter.ty;
667    if parameter.optional {
668        quote!(::std::option::Option<#ty>)
669    } else {
670        quote!(#ty)
671    }
672}
673
674fn expand_wip_type(item: DeriveInput) -> syn::Result<TokenStream2> {
675    if !item.generics.params.is_empty() || item.generics.where_clause.is_some() {
676        return Err(Error::new_spanned(
677            &item.generics,
678            "generic DTOs are not supported by `WipType`",
679        ));
680    }
681
682    let ident = &item.ident;
683    let type_name = source_name(ident);
684    let type_documentation = documentation(&item.attrs);
685
686    let implementation = match &item.data {
687        Data::Struct(data) => {
688            let fields = match &data.fields {
689                Fields::Named(fields) => &fields.named,
690                Fields::Unnamed(fields) => {
691                    return Err(Error::new_spanned(
692                        fields,
693                        "`WipType` supports only structs with named fields",
694                    ));
695                }
696                Fields::Unit => {
697                    return Err(Error::new_spanned(
698                        &item.ident,
699                        "unit structs are not supported by `WipType`",
700                    ));
701                }
702            };
703            derive_record(ident, &type_name, type_documentation.as_ref(), fields)?
704        }
705        Data::Enum(data) => derive_enum(ident, &type_name, type_documentation.as_ref(), data)?,
706        Data::Union(data) => {
707            return Err(Error::new_spanned(
708                data.union_token,
709                "Rust unions are not supported by `WipType`",
710            ));
711        }
712    };
713
714    Ok(implementation)
715}
716
717fn derive_record(
718    ident: &Ident,
719    type_name: &str,
720    type_documentation: Option<&Documentation>,
721    fields: &syn::punctuated::Punctuated<syn::Field, syn::token::Comma>,
722) -> syn::Result<TokenStream2> {
723    let mut parsed_fields = Vec::new();
724    let mut names = HashSet::new();
725    for field in fields {
726        let Some(field_ident) = field.ident.clone() else {
727            return Err(Error::new_spanned(
728                field,
729                "`WipType` record fields must be named",
730            ));
731        };
732        let name = source_name(&field_ident);
733        if !names.insert(name.clone()) {
734            return Err(Error::new(
735                field_ident.span(),
736                format!("duplicate record field `{name}`"),
737            ));
738        }
739        let (optional, ty) = optional_type(&field.ty, "DTO field")?;
740        validate_value_type(&ty, "DTO field")?;
741        parsed_fields.push(Parameter {
742            ident: field_ident,
743            name,
744            ty,
745            optional,
746            documentation: documentation(&field.attrs),
747        });
748    }
749
750    let declaration_doc = documentation_tokens(type_documentation);
751    let field_declarations = parsed_fields.iter().map(|field| {
752        let name = &field.name;
753        let documentation = documentation_tokens(field.documentation.as_ref());
754        let ty = &field.ty;
755        let optional = field.optional;
756        quote! {
757            ::wip_interface::wip_protocol::FieldDeclaration {
758                name: ::std::string::String::from(#name),
759                required: !#optional,
760                documentation: #documentation,
761                r#type: <#ty as ::wip_interface::WipType>::type_expr(),
762            }
763        }
764    });
765    let dependency_collectors = parsed_fields.iter().map(|field| {
766        let ty = &field.ty;
767        quote!(<#ty as ::wip_interface::WipType>::collect_declarations(declarations);)
768    });
769    let destructured_fields = parsed_fields.iter().map(|field| &field.ident);
770    let encoders = parsed_fields.iter().map(|field| {
771        let field_ident = &field.ident;
772        let field_name = &field.name;
773        let ty = &field.ty;
774        if field.optional {
775            quote! {
776                if let ::std::option::Option::Some(value) = #field_ident {
777                    fields.insert(
778                        ::std::string::String::from(#field_name),
779                        <#ty as ::wip_interface::WipType>::encode(value)?,
780                    );
781                }
782            }
783        } else {
784            quote! {
785                fields.insert(
786                    ::std::string::String::from(#field_name),
787                    <#ty as ::wip_interface::WipType>::encode(#field_ident)?,
788                );
789            }
790        }
791    });
792    let decoders = parsed_fields.iter().map(|field| {
793        let field_ident = &field.ident;
794        let field_name = &field.name;
795        let ty = &field.ty;
796        if field.optional {
797            quote!(#field_ident: decoder.optional::<#ty>(#field_name)?)
798        } else {
799            quote!(#field_ident: decoder.required::<#ty>(#field_name)?)
800        }
801    });
802
803    Ok(quote! {
804        impl ::wip_interface::WipType for #ident {
805            fn type_expr() -> ::wip_interface::wip_protocol::TypeExpr {
806                ::wip_interface::wip_protocol::TypeExpr::Named {
807                    name: ::std::string::String::from(#type_name),
808                }
809            }
810
811            fn collect_declarations(
812                declarations: &mut ::std::vec::Vec<
813                    ::wip_interface::wip_protocol::TypeDeclaration,
814                >,
815            ) {
816                let declaration = ::wip_interface::wip_protocol::TypeDeclaration {
817                    name: ::std::string::String::from(#type_name),
818                    documentation: #declaration_doc,
819                    definition: ::wip_interface::wip_protocol::TypeExpr::Record {
820                        fields: ::std::vec![#(#field_declarations),*],
821                    },
822                };
823                if ::wip_interface::__private::register_declaration(declarations, declaration) {
824                    #(#dependency_collectors)*
825                }
826            }
827
828            fn encode(
829                self,
830            ) -> ::std::result::Result<
831                ::wip_interface::wip_protocol::Value,
832                ::wip_interface::CodecError,
833            > {
834                let Self { #(#destructured_fields),* } = self;
835                let mut fields = ::std::collections::BTreeMap::new();
836                #(#encoders)*
837                ::std::result::Result::Ok(
838                    ::wip_interface::wip_protocol::Value::Record(fields),
839                )
840            }
841
842            fn decode(
843                value: ::wip_interface::wip_protocol::Value,
844            ) -> ::std::result::Result<Self, ::wip_interface::CodecError> {
845                match value {
846                    ::wip_interface::wip_protocol::Value::Record(fields) => {
847                        let mut decoder = ::wip_interface::__private::RecordDecoder::new(fields);
848                        let decoded = Self {
849                            #(#decoders,)*
850                        };
851                        decoder.finish()?;
852                        ::std::result::Result::Ok(decoded)
853                    }
854                    actual => ::std::result::Result::Err(
855                        ::wip_interface::__private::decode_type_mismatch("record", actual),
856                    ),
857                }
858            }
859        }
860    })
861}
862
863fn derive_enum(
864    ident: &Ident,
865    type_name: &str,
866    type_documentation: Option<&Documentation>,
867    data: &DataEnum,
868) -> syn::Result<TokenStream2> {
869    for variant in &data.variants {
870        if let Some((_, discriminant)) = &variant.discriminant {
871            return Err(Error::new_spanned(
872                discriminant,
873                "explicit enum discriminants are not supported by `WipType`",
874            ));
875        }
876    }
877
878    let all_unit = data
879        .variants
880        .iter()
881        .all(|variant| matches!(variant.fields, Fields::Unit));
882    let valid_union = data.variants.iter().all(|variant| {
883        matches!(variant.fields, Fields::Unit)
884            || matches!(&variant.fields, Fields::Unnamed(fields) if fields.unnamed.len() == 1)
885    });
886
887    if all_unit {
888        derive_unit_enum(ident, type_name, type_documentation, data)
889    } else if valid_union {
890        derive_union_enum(ident, type_name, type_documentation, data)
891    } else {
892        let span = data
893            .variants
894            .iter()
895            .find(|variant| {
896                !matches!(variant.fields, Fields::Unit)
897                    && !matches!(&variant.fields, Fields::Unnamed(fields) if fields.unnamed.len() == 1)
898            })
899            .map_or_else(|| data.enum_token.span, Spanned::span);
900        Err(Error::new(
901            span,
902            "a `WipType` enum must contain unit variants and/or single-field tuple variants; named-field and multi-field variants are unsupported",
903        ))
904    }
905}
906
907fn derive_unit_enum(
908    ident: &Ident,
909    type_name: &str,
910    type_documentation: Option<&Documentation>,
911    data: &DataEnum,
912) -> syn::Result<TokenStream2> {
913    let declaration_doc = documentation_tokens(type_documentation);
914    let declarations = data.variants.iter().map(|variant| {
915        let name = source_name(&variant.ident);
916        let documentation = documentation_tokens(documentation(&variant.attrs).as_ref());
917        quote! {
918            ::wip_interface::wip_protocol::EnumCase {
919                name: ::std::string::String::from(#name),
920                documentation: #documentation,
921            }
922        }
923    });
924    let encoders = data.variants.iter().map(|variant| {
925        let variant_ident = &variant.ident;
926        let name = source_name(variant_ident);
927        quote!(Self::#variant_ident => #name)
928    });
929    let decoders = data.variants.iter().map(|variant| {
930        let variant_ident = &variant.ident;
931        let name = source_name(variant_ident);
932        quote!(#name => ::std::result::Result::Ok(Self::#variant_ident))
933    });
934
935    Ok(quote! {
936        impl ::wip_interface::WipType for #ident {
937            fn type_expr() -> ::wip_interface::wip_protocol::TypeExpr {
938                ::wip_interface::wip_protocol::TypeExpr::Named {
939                    name: ::std::string::String::from(#type_name),
940                }
941            }
942
943            fn collect_declarations(
944                declarations: &mut ::std::vec::Vec<
945                    ::wip_interface::wip_protocol::TypeDeclaration,
946                >,
947            ) {
948                let declaration = ::wip_interface::wip_protocol::TypeDeclaration {
949                    name: ::std::string::String::from(#type_name),
950                    documentation: #declaration_doc,
951                    definition: ::wip_interface::wip_protocol::TypeExpr::Enum {
952                        cases: ::std::vec![#(#declarations),*],
953                    },
954                };
955                let _ = ::wip_interface::__private::register_declaration(
956                    declarations,
957                    declaration,
958                );
959            }
960
961            fn encode(
962                self,
963            ) -> ::std::result::Result<
964                ::wip_interface::wip_protocol::Value,
965                ::wip_interface::CodecError,
966            > {
967                let variant = match self {
968                    #(#encoders,)*
969                };
970                ::std::result::Result::Ok(
971                    ::wip_interface::wip_protocol::Value::String(
972                        ::std::string::String::from(variant),
973                    ),
974                )
975            }
976
977            fn decode(
978                value: ::wip_interface::wip_protocol::Value,
979            ) -> ::std::result::Result<Self, ::wip_interface::CodecError> {
980                match value {
981                    ::wip_interface::wip_protocol::Value::String(variant) => {
982                        match variant.as_str() {
983                            #(#decoders,)*
984                            _ => ::std::result::Result::Err(
985                                ::wip_interface::__private::decode_unknown_variant(
986                                    #type_name,
987                                    variant,
988                                ),
989                            ),
990                        }
991                    }
992                    actual => ::std::result::Result::Err(
993                        ::wip_interface::__private::decode_type_mismatch("enum", actual),
994                    ),
995                }
996            }
997        }
998    })
999}
1000
1001fn derive_union_enum(
1002    ident: &Ident,
1003    type_name: &str,
1004    type_documentation: Option<&Documentation>,
1005    data: &DataEnum,
1006) -> syn::Result<TokenStream2> {
1007    struct Variant<'a> {
1008        ident: &'a Ident,
1009        name: String,
1010        payload: Option<&'a Type>,
1011        documentation: Option<Documentation>,
1012    }
1013
1014    let mut variants = Vec::new();
1015    for variant in &data.variants {
1016        let payload = match &variant.fields {
1017            Fields::Unit => None,
1018            Fields::Unnamed(fields) if fields.unnamed.len() == 1 => {
1019                let field = fields.unnamed.first().expect("length checked");
1020                if direct_option(&field.ty)?.is_some() || contains_option(&field.ty) {
1021                    return Err(Error::new_spanned(
1022                        &field.ty,
1023                        "`Option<T>` is only supported directly on DTO fields and operation parameters",
1024                    ));
1025                }
1026                validate_value_type(&field.ty, "union case payload")?;
1027                Some(&field.ty)
1028            }
1029            _ => {
1030                return Err(Error::new_spanned(
1031                    variant,
1032                    "a `WipType` union case must be unit-like or contain exactly one unnamed payload",
1033                ));
1034            }
1035        };
1036        variants.push(Variant {
1037            ident: &variant.ident,
1038            name: source_name(&variant.ident),
1039            payload,
1040            documentation: documentation(&variant.attrs),
1041        });
1042    }
1043
1044    let declaration_doc = documentation_tokens(type_documentation);
1045    let declarations = variants.iter().map(|variant| {
1046        let name = &variant.name;
1047        let documentation = documentation_tokens(variant.documentation.as_ref());
1048        let payload = match variant.payload {
1049            Some(ty) => quote! {
1050                ::std::option::Option::Some(
1051                    <#ty as ::wip_interface::WipType>::type_expr(),
1052                )
1053            },
1054            None => quote!(::std::option::Option::None),
1055        };
1056        quote! {
1057            ::wip_interface::wip_protocol::UnionCase {
1058                name: ::std::string::String::from(#name),
1059                documentation: #documentation,
1060                payload: #payload,
1061            }
1062        }
1063    });
1064    let dependency_collectors = variants.iter().filter_map(|variant| {
1065        variant.payload.map(
1066            |ty| quote!(<#ty as ::wip_interface::WipType>::collect_declarations(declarations);),
1067        )
1068    });
1069    let encoders = variants.iter().map(|variant| {
1070        let variant_ident = variant.ident;
1071        let name = &variant.name;
1072        match variant.payload {
1073            Some(ty) => quote! {
1074                Self::#variant_ident(value) => {
1075                    let mut fields = ::std::collections::BTreeMap::new();
1076                    fields.insert(
1077                        ::std::string::String::from(
1078                            ::wip_interface::wip_protocol::UNION_CASE_FIELD,
1079                        ),
1080                        ::wip_interface::wip_protocol::Value::String(
1081                            ::std::string::String::from(#name),
1082                        ),
1083                    );
1084                    fields.insert(
1085                        ::std::string::String::from(
1086                            ::wip_interface::wip_protocol::UNION_VALUE_FIELD,
1087                        ),
1088                        <#ty as ::wip_interface::WipType>::encode(value)?,
1089                    );
1090                    ::wip_interface::wip_protocol::Value::Record(fields)
1091                }
1092            },
1093            None => quote! {
1094                Self::#variant_ident => {
1095                    let mut fields = ::std::collections::BTreeMap::new();
1096                    fields.insert(
1097                        ::std::string::String::from(
1098                            ::wip_interface::wip_protocol::UNION_CASE_FIELD,
1099                        ),
1100                        ::wip_interface::wip_protocol::Value::String(
1101                            ::std::string::String::from(#name),
1102                        ),
1103                    );
1104                    ::wip_interface::wip_protocol::Value::Record(fields)
1105                }
1106            },
1107        }
1108    });
1109    let decoders = variants.iter().map(|variant| {
1110        let variant_ident = variant.ident;
1111        let name = &variant.name;
1112        match variant.payload {
1113            Some(ty) => quote! {
1114                #name => {
1115                    let value = decoder.required::<#ty>(
1116                        ::wip_interface::wip_protocol::UNION_VALUE_FIELD,
1117                    )?;
1118                    decoder.finish()?;
1119                    ::std::result::Result::Ok(Self::#variant_ident(value))
1120                }
1121            },
1122            None => quote! {
1123                #name => {
1124                    decoder.finish()?;
1125                    ::std::result::Result::Ok(Self::#variant_ident)
1126                }
1127            },
1128        }
1129    });
1130
1131    Ok(quote! {
1132        impl ::wip_interface::WipType for #ident {
1133            fn type_expr() -> ::wip_interface::wip_protocol::TypeExpr {
1134                ::wip_interface::wip_protocol::TypeExpr::Named {
1135                    name: ::std::string::String::from(#type_name),
1136                }
1137            }
1138
1139            fn collect_declarations(
1140                declarations: &mut ::std::vec::Vec<
1141                    ::wip_interface::wip_protocol::TypeDeclaration,
1142                >,
1143            ) {
1144                let declaration = ::wip_interface::wip_protocol::TypeDeclaration {
1145                    name: ::std::string::String::from(#type_name),
1146                    documentation: #declaration_doc,
1147                    definition: ::wip_interface::wip_protocol::TypeExpr::Union {
1148                        cases: ::std::vec![#(#declarations),*],
1149                    },
1150                };
1151                if ::wip_interface::__private::register_declaration(declarations, declaration) {
1152                    #(#dependency_collectors)*
1153                }
1154            }
1155
1156            fn encode(
1157                self,
1158            ) -> ::std::result::Result<
1159                ::wip_interface::wip_protocol::Value,
1160                ::wip_interface::CodecError,
1161            > {
1162                let value = match self {
1163                    #(#encoders,)*
1164                };
1165                ::std::result::Result::Ok(value)
1166            }
1167
1168            fn decode(
1169                value: ::wip_interface::wip_protocol::Value,
1170            ) -> ::std::result::Result<Self, ::wip_interface::CodecError> {
1171                match value {
1172                    ::wip_interface::wip_protocol::Value::Record(fields) => {
1173                        let mut decoder =
1174                            ::wip_interface::__private::RecordDecoder::new(fields);
1175                        let case = decoder.required::<::std::string::String>(
1176                            ::wip_interface::wip_protocol::UNION_CASE_FIELD,
1177                        )?;
1178                        match case.as_str() {
1179                            #(#decoders,)*
1180                            _ => ::std::result::Result::Err(
1181                                ::wip_interface::__private::decode_unknown_variant(
1182                                    #type_name,
1183                                    case,
1184                                ),
1185                            ),
1186                        }
1187                    }
1188                    actual => ::std::result::Result::Err(
1189                        ::wip_interface::__private::decode_type_mismatch("union record", actual),
1190                    ),
1191                }
1192            }
1193        }
1194    })
1195}
1196fn optional_type(ty: &Type, context: &str) -> syn::Result<(bool, Type)> {
1197    if let Some(inner) = direct_option(ty)? {
1198        if contains_option(&inner) {
1199            return Err(Error::new_spanned(
1200                inner,
1201                format!("nested `Option<T>` is not supported for {context}"),
1202            ));
1203        }
1204        Ok((true, inner))
1205    } else {
1206        if contains_option(ty) {
1207            return Err(Error::new_spanned(
1208                ty,
1209                format!("`Option<T>` must appear directly as the {context} type"),
1210            ));
1211        }
1212        Ok((false, ty.clone()))
1213    }
1214}
1215
1216fn direct_option(ty: &Type) -> syn::Result<Option<Type>> {
1217    let Type::Path(path) = ty else {
1218        return Ok(None);
1219    };
1220    if path.qself.is_some() {
1221        return Ok(None);
1222    }
1223    let Some(segment) = path.path.segments.last() else {
1224        return Ok(None);
1225    };
1226    if segment.ident != "Option" {
1227        return Ok(None);
1228    }
1229    let PathArguments::AngleBracketed(arguments) = &segment.arguments else {
1230        return Err(Error::new_spanned(
1231            segment,
1232            "`Option` must have exactly one type argument",
1233        ));
1234    };
1235    if arguments.args.len() != 1 {
1236        return Err(Error::new_spanned(
1237            arguments,
1238            "`Option` must have exactly one type argument",
1239        ));
1240    }
1241    match arguments.args.first() {
1242        Some(GenericArgument::Type(inner)) => Ok(Some(inner.clone())),
1243        _ => Err(Error::new_spanned(
1244            arguments,
1245            "`Option` must have exactly one type argument",
1246        )),
1247    }
1248}
1249
1250fn validate_value_type(ty: &Type, context: &str) -> syn::Result<()> {
1251    reject_custom_lifetimes(ty, context)?;
1252    if let Some(span) = first_reference(ty) {
1253        return Err(Error::new(
1254            span,
1255            format!("borrowed types are not supported as a WIP {context}"),
1256        ));
1257    }
1258    if let Some(span) = first_unit(ty) {
1259        return Err(Error::new(
1260            span,
1261            format!(
1262                "`()` is only supported as an operation application return, not as a {context}"
1263            ),
1264        ));
1265    }
1266    if contains_option(ty) {
1267        return Err(Error::new_spanned(
1268            ty,
1269            "`Option<T>` is only supported directly on DTO fields and operation parameters",
1270        ));
1271    }
1272    Ok(())
1273}
1274
1275fn reject_custom_lifetimes(ty: &Type, context: &str) -> syn::Result<()> {
1276    struct Finder(Option<Span>);
1277    impl<'ast> Visit<'ast> for Finder {
1278        fn visit_lifetime(&mut self, lifetime: &'ast syn::Lifetime) {
1279            if self.0.is_none() {
1280                self.0 = Some(lifetime.span());
1281            }
1282        }
1283    }
1284
1285    let mut finder = Finder(None);
1286    finder.visit_type(ty);
1287    if let Some(span) = finder.0 {
1288        Err(Error::new(
1289            span,
1290            format!("custom lifetimes are not supported in a WIP {context}"),
1291        ))
1292    } else {
1293        Ok(())
1294    }
1295}
1296
1297fn contains_option(ty: &Type) -> bool {
1298    struct Finder(bool);
1299    impl<'ast> Visit<'ast> for Finder {
1300        fn visit_type_path(&mut self, path: &'ast syn::TypePath) {
1301            if path
1302                .path
1303                .segments
1304                .last()
1305                .is_some_and(|segment| segment.ident == "Option")
1306            {
1307                self.0 = true;
1308            }
1309            visit::visit_type_path(self, path);
1310        }
1311    }
1312
1313    let mut finder = Finder(false);
1314    finder.visit_type(ty);
1315    finder.0
1316}
1317
1318fn first_reference(ty: &Type) -> Option<Span> {
1319    struct Finder(Option<Span>);
1320    impl<'ast> Visit<'ast> for Finder {
1321        fn visit_type_reference(&mut self, reference: &'ast syn::TypeReference) {
1322            if self.0.is_none() {
1323                self.0 = Some(reference.span());
1324            }
1325            visit::visit_type_reference(self, reference);
1326        }
1327    }
1328
1329    let mut finder = Finder(None);
1330    finder.visit_type(ty);
1331    finder.0
1332}
1333
1334fn first_unit(ty: &Type) -> Option<Span> {
1335    struct Finder(Option<Span>);
1336    impl<'ast> Visit<'ast> for Finder {
1337        fn visit_type_tuple(&mut self, tuple: &'ast syn::TypeTuple) {
1338            if tuple.elems.is_empty() && self.0.is_none() {
1339                self.0 = Some(tuple.span());
1340            }
1341            visit::visit_type_tuple(self, tuple);
1342        }
1343    }
1344
1345    let mut finder = Finder(None);
1346    finder.visit_type(ty);
1347    finder.0
1348}
1349
1350fn is_unit(ty: &Type) -> bool {
1351    matches!(ty, Type::Tuple(tuple) if tuple.elems.is_empty())
1352}
1353
1354fn source_name(ident: &Ident) -> String {
1355    ident.unraw().to_string()
1356}
1357
1358fn pascal_case(name: &str) -> String {
1359    let mut result = String::new();
1360    let mut uppercase = true;
1361    for character in name.chars() {
1362        if character == '_' {
1363            uppercase = true;
1364        } else if uppercase {
1365            result.extend(character.to_uppercase());
1366            uppercase = false;
1367        } else {
1368            result.push(character);
1369        }
1370    }
1371    result
1372}
1373
1374fn documentation(attributes: &[Attribute]) -> Option<Documentation> {
1375    documentation_from_lines(doc_lines(attributes))
1376}
1377
1378fn doc_lines(attributes: &[Attribute]) -> Vec<String> {
1379    attributes
1380        .iter()
1381        .filter_map(|attribute| {
1382            if !attribute.path().is_ident("doc") {
1383                return None;
1384            }
1385            match &attribute.meta {
1386                Meta::NameValue(name_value) => match &name_value.value {
1387                    syn::Expr::Lit(expression) => match &expression.lit {
1388                        Lit::Str(value) => {
1389                            let value = value.value();
1390                            Some(value.strip_prefix(' ').unwrap_or(&value).to_owned())
1391                        }
1392                        _ => None,
1393                    },
1394                    _ => None,
1395                },
1396                _ => None,
1397            }
1398        })
1399        .collect()
1400}
1401
1402fn split_returns_section(lines: Vec<String>) -> (Vec<String>, Vec<String>) {
1403    let Some(start) = lines.iter().position(|line| line.trim() == "# Returns") else {
1404        return (lines, Vec::new());
1405    };
1406    let end = lines[start + 1..]
1407        .iter()
1408        .position(|line| line.trim_start().starts_with("# "))
1409        .map_or(lines.len(), |offset| start + 1 + offset);
1410
1411    let mut operation = lines[..start].to_vec();
1412    operation.extend_from_slice(&lines[end..]);
1413    (operation, lines[start + 1..end].to_vec())
1414}
1415
1416fn documentation_from_lines(mut lines: Vec<String>) -> Option<Documentation> {
1417    while lines.first().is_some_and(|line| line.trim().is_empty()) {
1418        lines.remove(0);
1419    }
1420    while lines.last().is_some_and(|line| line.trim().is_empty()) {
1421        lines.pop();
1422    }
1423    if lines.is_empty() {
1424        return None;
1425    }
1426
1427    let split = lines
1428        .iter()
1429        .position(|line| line.trim().is_empty())
1430        .unwrap_or(lines.len());
1431    let summary = lines[..split]
1432        .iter()
1433        .map(|line| line.trim())
1434        .collect::<Vec<_>>()
1435        .join(" ");
1436    let mut details_lines = if split < lines.len() {
1437        lines[split + 1..].to_vec()
1438    } else {
1439        Vec::new()
1440    };
1441    while details_lines
1442        .first()
1443        .is_some_and(|line| line.trim().is_empty())
1444    {
1445        details_lines.remove(0);
1446    }
1447    while details_lines
1448        .last()
1449        .is_some_and(|line| line.trim().is_empty())
1450    {
1451        details_lines.pop();
1452    }
1453    let details = (!details_lines.is_empty()).then(|| details_lines.join("\n"));
1454
1455    Some(Documentation { summary, details })
1456}
1457
1458fn documentation_tokens(documentation: Option<&Documentation>) -> TokenStream2 {
1459    match documentation {
1460        Some(documentation) => {
1461            let summary = &documentation.summary;
1462            match &documentation.details {
1463                Some(details) => quote! {
1464                    ::wip_interface::__private::documentation(#summary, ::std::option::Option::Some(#details))
1465                },
1466                None => quote! {
1467                    ::wip_interface::__private::documentation(#summary, ::std::option::Option::None)
1468                },
1469            }
1470        }
1471        None => quote! {
1472            ::wip_interface::__private::documentation("", ::std::option::Option::None)
1473        },
1474    }
1475}