1#![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#[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#[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 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 pub fn descriptor() -> ::wip_interface::wip_protocol::InterfaceDescriptor {
208 <Self as ::wip_interface::InterfaceDefinition>::descriptor()
209 }
210
211 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 = ¶meter.ident;
512 let name = ¶meter.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 = ¶meter.ident;
522 let name = ¶meter.name;
523 let ty = ¶meter.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 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 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 = ¶meter.name;
582 let parameter_doc = documentation_tokens(parameter.documentation.as_ref());
583 let ty = ¶meter.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 = ¶meter.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 = ¶meter.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 = ¶meter.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}