fix(interface): harden generated public API
This commit is contained in:
@@ -1,4 +1,10 @@
|
|||||||
//! Procedural macros backing the public `wip-interface` authoring facade.
|
//! 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;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
@@ -470,13 +476,18 @@ fn parse_operation_result(output: &ReturnType) -> syn::Result<Type> {
|
|||||||
_ => None,
|
_ => None,
|
||||||
});
|
});
|
||||||
let result = types.next().cloned();
|
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(
|
return Err(Error::new_spanned(
|
||||||
arguments,
|
arguments,
|
||||||
"`OperationResult` must have exactly one application result type",
|
"`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) {
|
if direct_option(&result)?.is_some() || contains_option(&result) {
|
||||||
return Err(Error::new_spanned(
|
return Err(Error::new_spanned(
|
||||||
result,
|
result,
|
||||||
@@ -490,8 +501,13 @@ fn argument_item(operation: &Operation) -> TokenStream2 {
|
|||||||
let arguments_ident = &operation.arguments_ident;
|
let arguments_ident = &operation.arguments_ident;
|
||||||
let fields = operation.parameters.iter().map(|parameter| {
|
let fields = operation.parameters.iter().map(|parameter| {
|
||||||
let ident = ¶meter.ident;
|
let ident = ¶meter.ident;
|
||||||
|
let name = ¶meter.name;
|
||||||
let ty = parameter_rust_type(parameter);
|
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 decoders = operation.parameters.iter().map(|parameter| {
|
||||||
let ident = ¶meter.ident;
|
let ident = ¶meter.ident;
|
||||||
@@ -698,7 +714,12 @@ fn derive_record(
|
|||||||
let mut parsed_fields = Vec::new();
|
let mut parsed_fields = Vec::new();
|
||||||
let mut names = HashSet::new();
|
let mut names = HashSet::new();
|
||||||
for field in fields {
|
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);
|
let name = source_name(&field_ident);
|
||||||
if !names.insert(name.clone()) {
|
if !names.insert(name.clone()) {
|
||||||
return Err(Error::new(
|
return Err(Error::new(
|
||||||
@@ -984,12 +1005,17 @@ fn derive_union_enum(
|
|||||||
let mut variants = Vec::new();
|
let mut variants = Vec::new();
|
||||||
for variant in &data.variants {
|
for variant in &data.variants {
|
||||||
let Fields::Unnamed(fields) = &variant.fields else {
|
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) {
|
if direct_option(&field.ty)?.is_some() || contains_option(&field.ty) {
|
||||||
return Err(Error::new_spanned(
|
return Err(Error::new_spanned(
|
||||||
&field.ty,
|
&field.ty,
|
||||||
|
|||||||
@@ -48,6 +48,21 @@
|
|||||||
//! an application return enum/union and is returned as `Ok(value)`.
|
//! an application return enum/union and is returned as `Ok(value)`.
|
||||||
//! [`HostError`] is reserved for host/runtime failure, so the required
|
//! [`HostError`] is reserved for host/runtime failure, so the required
|
||||||
//! [`OperationResult`] convention keeps those two failure classes distinct.
|
//! [`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<T>`. `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)]
|
#![deny(missing_docs)]
|
||||||
|
|
||||||
@@ -619,7 +634,6 @@ pub mod __private {
|
|||||||
/// Decoder for a protocol record or operation's named argument collection.
|
/// Decoder for a protocol record or operation's named argument collection.
|
||||||
pub struct RecordDecoder {
|
pub struct RecordDecoder {
|
||||||
fields: Vec<NamedValue>,
|
fields: Vec<NamedValue>,
|
||||||
seen: HashSet<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RecordDecoder {
|
impl RecordDecoder {
|
||||||
@@ -633,7 +647,7 @@ pub mod __private {
|
|||||||
.at(field.name.clone()));
|
.at(field.name.clone()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(Self { fields, seen })
|
Ok(Self { fields })
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Decodes a required field.
|
/// Decodes a required field.
|
||||||
@@ -666,11 +680,5 @@ pub mod __private {
|
|||||||
let index = self.fields.iter().position(|field| field.name == name)?;
|
let index = self.fields.iter().position(|field| field.name == name)?;
|
||||||
Some(self.fields.remove(index).value)
|
Some(self.fields.remove(index).value)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the set of names supplied to this decoder.
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn supplied_names(&self) -> &HashSet<String> {
|
|
||||||
&self.seen
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
use wip_interface::interface;
|
||||||
|
|
||||||
|
#[interface]
|
||||||
|
pub trait AsyncOperation {
|
||||||
|
async fn status(&self) -> wip_interface::OperationResult<String>;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {}
|
||||||
@@ -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<String>;
|
||||||
|
| ^^^^^
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
use wip_interface::interface;
|
||||||
|
|
||||||
|
#[interface]
|
||||||
|
pub trait DefaultMethod {
|
||||||
|
fn status(&self) -> wip_interface::OperationResult<String> {
|
||||||
|
Ok(String::new())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {}
|
||||||
@@ -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<String> {
|
||||||
|
| ________________________________________________________________^
|
||||||
|
6 | | Ok(String::new())
|
||||||
|
7 | | }
|
||||||
|
| |_____^
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
use wip_interface::WipType;
|
||||||
|
|
||||||
|
#[derive(WipType)]
|
||||||
|
struct GenericDto<T> {
|
||||||
|
value: T,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
error: generic DTOs are not supported by `WipType`
|
||||||
|
--> tests/ui/generic_dto.rs:4:18
|
||||||
|
|
|
||||||
|
4 | struct GenericDto<T> {
|
||||||
|
| ^^^
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
use wip_interface::WipType;
|
||||||
|
|
||||||
|
#[derive(WipType)]
|
||||||
|
struct NestedOption {
|
||||||
|
values: Vec<Option<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
error: `Option<T>` must appear directly as the DTO field type
|
||||||
|
--> tests/ui/nested_option.rs:5:13
|
||||||
|
|
|
||||||
|
5 | values: Vec<Option<String>>,
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^
|
||||||
Reference in New Issue
Block a user