fix(interface): harden generated public API

This commit is contained in:
2026-09-29 00:04:04 +09:00
parent d0683f0b6d
commit 5992322371
10 changed files with 108 additions and 17 deletions
+35 -9
View File
@@ -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<Type> {
_ => 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 = &parameter.ident;
let name = &parameter.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 = &parameter.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,