diff --git a/crates/wip-interface-macros/src/lib.rs b/crates/wip-interface-macros/src/lib.rs index bf08a5c..976f21b 100644 --- a/crates/wip-interface-macros/src/lib.rs +++ b/crates/wip-interface-macros/src/lib.rs @@ -1,4 +1,10 @@ //! Procedural macros backing the public `wip-interface` authoring facade. +//! +//! This is an implementation crate. Applications should depend on +//! `wip-interface`, which re-exports both macros alongside the traits and types +//! used by their generated code. + +#![deny(missing_docs)] use std::collections::HashSet; @@ -470,13 +476,18 @@ fn parse_operation_result(output: &ReturnType) -> syn::Result { _ => None, }); let result = types.next().cloned(); - if arguments.args.len() != 1 || result.is_none() || types.next().is_some() { + if arguments.args.len() != 1 || types.next().is_some() { return Err(Error::new_spanned( arguments, "`OperationResult` must have exactly one application result type", )); } - let result = result.expect("checked above"); + let result = result.ok_or_else(|| { + Error::new_spanned( + arguments, + "`OperationResult` must have exactly one application result type", + ) + })?; if direct_option(&result)?.is_some() || contains_option(&result) { return Err(Error::new_spanned( result, @@ -490,8 +501,13 @@ fn argument_item(operation: &Operation) -> TokenStream2 { let arguments_ident = &operation.arguments_ident; let fields = operation.parameters.iter().map(|parameter| { let ident = ¶meter.ident; + let name = ¶meter.name; let ty = parameter_rust_type(parameter); - quote!(pub #ident: #ty) + let documentation = format!("Decoded `{name}` operation argument."); + quote!( + #[doc = #documentation] + pub #ident: #ty + ) }); let decoders = operation.parameters.iter().map(|parameter| { let ident = ¶meter.ident; @@ -698,7 +714,12 @@ fn derive_record( let mut parsed_fields = Vec::new(); let mut names = HashSet::new(); for field in fields { - let field_ident = field.ident.clone().expect("named fields have identifiers"); + let Some(field_ident) = field.ident.clone() else { + return Err(Error::new_spanned( + field, + "`WipType` record fields must be named", + )); + }; let name = source_name(&field_ident); if !names.insert(name.clone()) { return Err(Error::new( @@ -984,12 +1005,17 @@ fn derive_union_enum( let mut variants = Vec::new(); for variant in &data.variants { let Fields::Unnamed(fields) = &variant.fields else { - unreachable!("enum shape was checked before union generation"); + return Err(Error::new_spanned( + variant, + "a `WipType` union variant must contain exactly one unnamed field", + )); + }; + let Some(field) = fields.unnamed.first() else { + return Err(Error::new_spanned( + fields, + "a `WipType` union variant must contain exactly one unnamed field", + )); }; - let field = fields - .unnamed - .first() - .expect("union variants have one field"); if direct_option(&field.ty)?.is_some() || contains_option(&field.ty) { return Err(Error::new_spanned( &field.ty, diff --git a/crates/wip-interface/src/lib.rs b/crates/wip-interface/src/lib.rs index 4ee4e0d..14de629 100644 --- a/crates/wip-interface/src/lib.rs +++ b/crates/wip-interface/src/lib.rs @@ -48,6 +48,21 @@ //! an application return enum/union and is returned as `Ok(value)`. //! [`HostError`] is reserved for host/runtime failure, so the required //! [`OperationResult`] convention keeps those two failure classes distinct. +//! +//! # Supported interface syntax +//! +//! An interface is a public, non-generic trait whose operations use exactly +//! `&self`, simple named owned parameters, and an explicit +//! `OperationResult`. `WipType` derives support named-field structs, +//! all-unit enums, and all-single-payload enums. +//! +//! The macros reject trait inheritance and associated items; const, async, +//! unsafe, extern, variadic, generic, default-bodied, or non-`&self` methods; +//! borrowed values and custom lifetimes; tuple, unit, or generic structs; +//! mixed or named-field enum variants; explicit discriminants; Rust unions; +//! and `Option` anywhere except a direct DTO field or operation parameter. +//! Unsupported inputs produce source-located compile errors rather than +//! generating a partial interface. #![deny(missing_docs)] @@ -619,7 +634,6 @@ pub mod __private { /// Decoder for a protocol record or operation's named argument collection. pub struct RecordDecoder { fields: Vec, - seen: HashSet, } impl RecordDecoder { @@ -633,7 +647,7 @@ pub mod __private { .at(field.name.clone())); } } - Ok(Self { fields, seen }) + Ok(Self { fields }) } /// Decodes a required field. @@ -666,11 +680,5 @@ pub mod __private { let index = self.fields.iter().position(|field| field.name == name)?; Some(self.fields.remove(index).value) } - - /// Returns the set of names supplied to this decoder. - #[allow(dead_code)] - pub fn supplied_names(&self) -> &HashSet { - &self.seen - } } } diff --git a/crates/wip-interface/tests/ui/async_operation.rs b/crates/wip-interface/tests/ui/async_operation.rs new file mode 100644 index 0000000..f883a1c --- /dev/null +++ b/crates/wip-interface/tests/ui/async_operation.rs @@ -0,0 +1,8 @@ +use wip_interface::interface; + +#[interface] +pub trait AsyncOperation { + async fn status(&self) -> wip_interface::OperationResult; +} + +fn main() {} diff --git a/crates/wip-interface/tests/ui/async_operation.stderr b/crates/wip-interface/tests/ui/async_operation.stderr new file mode 100644 index 0000000..c3331e5 --- /dev/null +++ b/crates/wip-interface/tests/ui/async_operation.stderr @@ -0,0 +1,5 @@ +error: async operations are not supported + --> tests/ui/async_operation.rs:5:5 + | +5 | async fn status(&self) -> wip_interface::OperationResult; + | ^^^^^ diff --git a/crates/wip-interface/tests/ui/default_method.rs b/crates/wip-interface/tests/ui/default_method.rs new file mode 100644 index 0000000..1a53368 --- /dev/null +++ b/crates/wip-interface/tests/ui/default_method.rs @@ -0,0 +1,10 @@ +use wip_interface::interface; + +#[interface] +pub trait DefaultMethod { + fn status(&self) -> wip_interface::OperationResult { + Ok(String::new()) + } +} + +fn main() {} diff --git a/crates/wip-interface/tests/ui/default_method.stderr b/crates/wip-interface/tests/ui/default_method.stderr new file mode 100644 index 0000000..d02104b --- /dev/null +++ b/crates/wip-interface/tests/ui/default_method.stderr @@ -0,0 +1,8 @@ +error: WIP operation methods must not have a body + --> tests/ui/default_method.rs:5:64 + | +5 | fn status(&self) -> wip_interface::OperationResult { + | ________________________________________________________________^ +6 | | Ok(String::new()) +7 | | } + | |_____^ diff --git a/crates/wip-interface/tests/ui/generic_dto.rs b/crates/wip-interface/tests/ui/generic_dto.rs new file mode 100644 index 0000000..1a98bb5 --- /dev/null +++ b/crates/wip-interface/tests/ui/generic_dto.rs @@ -0,0 +1,8 @@ +use wip_interface::WipType; + +#[derive(WipType)] +struct GenericDto { + value: T, +} + +fn main() {} diff --git a/crates/wip-interface/tests/ui/generic_dto.stderr b/crates/wip-interface/tests/ui/generic_dto.stderr new file mode 100644 index 0000000..e0ca58e --- /dev/null +++ b/crates/wip-interface/tests/ui/generic_dto.stderr @@ -0,0 +1,5 @@ +error: generic DTOs are not supported by `WipType` + --> tests/ui/generic_dto.rs:4:18 + | +4 | struct GenericDto { + | ^^^ diff --git a/crates/wip-interface/tests/ui/nested_option.rs b/crates/wip-interface/tests/ui/nested_option.rs new file mode 100644 index 0000000..bb3154b --- /dev/null +++ b/crates/wip-interface/tests/ui/nested_option.rs @@ -0,0 +1,8 @@ +use wip_interface::WipType; + +#[derive(WipType)] +struct NestedOption { + values: Vec>, +} + +fn main() {} diff --git a/crates/wip-interface/tests/ui/nested_option.stderr b/crates/wip-interface/tests/ui/nested_option.stderr new file mode 100644 index 0000000..b373d3d --- /dev/null +++ b/crates/wip-interface/tests/ui/nested_option.stderr @@ -0,0 +1,5 @@ +error: `Option` must appear directly as the DTO field type + --> tests/ui/nested_option.rs:5:13 + | +5 | values: Vec>, + | ^^^^^^^^^^^^^^^^^^^