fix: align interface macros with canonical values

This commit is contained in:
2026-09-29 06:11:07 +09:00
parent 7f4e0e4460
commit 2d524723a4
14 changed files with 740 additions and 528 deletions
+28 -12
View File
@@ -30,8 +30,9 @@ wip-public-api-example (workspace-only executable, integration tests, doctest)
The boundary is intentional: protocol types describe what can cross a The boundary is intentional: protocol types describe what can cross a
transport; application DTOs opt into that model with `WipType`; a host-private transport; application DTOs opt into that model with `WipType`; a host-private
implementation owns credentials, handles, and backend state; and a future implementation owns credentials, handles, and backend state; and a future
transport maps its own routes/envelopes to `OperationCall`/`OperationResult` transport maps its own routes/envelopes to `CallOperationRequest` and
without placing transport-specific types in descriptors or application traits. `CallOperationResponse` without placing transport-specific types in descriptors
or application traits.
## End-to-end example ## End-to-end example
@@ -104,12 +105,13 @@ A supported interface is a public, non-generic trait containing methods with:
- `()` only as the application return when no value is produced; and - `()` only as the application return when no value is produced; and
- direct `Option<T>` only for an omittable parameter. - direct `Option<T>` only for an omittable parameter.
`WipType` derives support named-field structs, all-unit enums, and enums whose `WipType` derives support named-field structs, all-unit enums, and unions whose
variants each contain exactly one unnamed payload. The macros reject private, cases may be unit variants or variants containing exactly one unnamed payload.
The macros reject private,
generic, inherited, unsafe, auto, or associated-item traits; const, async, generic, inherited, unsafe, auto, or associated-item traits; const, async,
unsafe, extern, variadic, generic, default-bodied, or non-`&self` methods; unsafe, extern, variadic, generic, default-bodied, or non-`&self` methods;
borrowed/custom-lifetime values; tuple/unit/generic structs; Rust unions; borrowed/custom-lifetime values; tuple/unit/generic structs; Rust unions;
mixed/named-field enum variants; explicit discriminants; nested `Option`; and named-field/multi-field enum variants; explicit discriminants; nested `Option`; and
`Option` in returns, collections, entries, or union payloads. These diagnostics `Option` in returns, collections, entries, or union payloads. These diagnostics
are compile-fail tested under `crates/wip-interface/tests/ui`. are compile-fail tested under `crates/wip-interface/tests/ui`.
@@ -117,23 +119,36 @@ are compile-fail tested under `crates/wip-interface/tests/ui`.
| Rust | Protocol | | Rust | Protocol |
| --- | --- | | --- | --- |
| `()` as an operation result | no returned value | | `()` | unit / `Value::Unit` |
| `bool` | boolean | | `bool` | boolean |
| integer primitives | JavaScript-lossless integer | | integer primitives | JavaScript-lossless integer |
| `f32`, `f64` | finite number | | `f32`, `f64` | finite number |
| `String` | string | | `String` | string |
| `wip::Bytes` | bytes | | `wip::Bytes` | bytes |
| `wip::JsonValue` | JSON | | `wip::JsonValue` | JSON interpreted over the schema-neutral `Value` tree |
| `wip::Entry<K, V>` | entry | | `wip::Entry` | canonical absolute Worldspace path encoded as a string |
| `Vec<T>` | list | | `Vec<T>` | list |
| named-field `#[derive(wip::WipType)]` struct | named record | | named-field `#[derive(wip::WipType)]` struct | named record |
| all-unit `#[derive(wip::WipType)]` enum | named enum | | all-unit `#[derive(wip::WipType)]` enum | named enum encoded as a string |
| all-newtype `#[derive(wip::WipType)]` enum | named tagged union | | mixed unit/newtype or all-newtype enum | named union encoded as a discriminator record |
`Option<T>` means that a direct DTO field or operation parameter may be `Option<T>` means that a direct DTO field or operation parameter may be
omitted; it is not protocol null. JSON null remains available inside omitted; it is not protocol null. JSON null remains available inside
`wip::JsonValue`. `wip::JsonValue`.
#### Migration from the pre-canonical value API
The old generic `wip::Entry<K, V>` represented an application key/value pair
and is no longer available. Use `wip::Entry::new("/canonical/path")` for a
Worldspace entry path and place any associated application data in a record.
`Entry` encodes as `Value::String` and validates the canonical absolute path.
The semantic `Value::Json`, `Value::Entry`, `Value::Enum`, and `Value::Union`
variants have also been removed. JSON recursively uses `Unit`, primitive,
`List`, and `Record` values and rejects `Bytes`; enums use `String`; unions use
a `Record` containing `$case` and, for payload-bearing cases, `value`. Unit
operation results are represented explicitly as `Value::Unit`.
Rust doc comments become descriptor metadata. For an operation, a `# Returns` Rust doc comments become descriptor metadata. For an operation, a `# Returns`
section documents the return declaration; text before that section documents section documents the return declaration; text before that section documents
the operation itself. the operation itself.
@@ -155,8 +170,9 @@ let protocol_object = published.protocol_object();
`expose` compiles only when the object implements the generated definition's `expose` compiles only when the object implements the generated definition's
interface trait. Implementing a trait does not expose it automatically, empty interface trait. Implementing a trait does not expose it automatically, empty
interface sets are valid, insertion order is preserved, and duplicate interface sets are valid, insertion order is preserved, and duplicate
references are rejected. Registry references are opaque; descriptor names and references are rejected. Registry references are opaque and remain outside the
Rust type names are not used as independent wire identity. descriptor; generated descriptors contain neither those references nor Rust
trait identity.
Dispatch is qualified by interface reference and operation name. The host Dispatch is qualified by interface reference and operation name. The host
checks current object identity and validator policy, current interface checks current object identity and validator policy, current interface
+34 -32
View File
@@ -31,7 +31,7 @@
#![deny(missing_docs)] #![deny(missing_docs)]
use std::any::TypeId; use std::any::TypeId;
use std::collections::{HashMap, HashSet}; use std::collections::{BTreeMap, HashMap, HashSet};
use std::error::Error; use std::error::Error;
use std::fmt::{self, Display, Formatter}; use std::fmt::{self, Display, Formatter};
use std::marker::PhantomData; use std::marker::PhantomData;
@@ -42,8 +42,7 @@ use wip_interface::DispatchError;
pub use wip_interface::{InterfaceDefinition, InterfaceImplementation}; pub use wip_interface::{InterfaceDefinition, InterfaceImplementation};
pub use wip_protocol; pub use wip_protocol;
use wip_protocol::{ use wip_protocol::{
InterfaceDescriptor, NamedValue, Object, ObjectId, OperationCall, OperationTarget, InterfaceDescriptor, Object, ProtocolError, ProtocolErrorCode, ValidationError, Value,
ProtocolError, ProtocolErrorCategory, ValidationError, Value,
}; };
/// Opaque bytes used as a conditional execution precondition. /// Opaque bytes used as a conditional execution precondition.
@@ -64,6 +63,14 @@ impl Validator {
} }
} }
/// Host-local identity used by the legacy request-scoped publication API.
///
/// Canonical protocol calls target a Worldspace path. This wrapper remains
/// temporarily host-local so T-5 can adapt generated values without assigning
/// object identity semantics to the protocol descriptor model.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ObjectId(pub String);
/// Opaque host-assigned reference to one registered interface artifact. /// Opaque host-assigned reference to one registered interface artifact.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct InterfaceRef(String); pub struct InterfaceRef(String);
@@ -161,8 +168,8 @@ impl InterfaceRegistry {
/// `validator` should be present when the host intends the reference to /// `validator` should be present when the host intends the reference to
/// represent a versioned descriptor. Its presence makes the same value a /// represent a versioned descriptor. Its presence makes the same value a
/// mandatory precondition for dispatch. Registration finalizes the /// mandatory precondition for dispatch. Registration finalizes the
/// descriptor's protocol `name` to the assigned opaque reference so it /// descriptor remains independent of both registration and its assigned
/// corresponds directly to [`Object::interfaces`]. /// opaque reference.
pub fn register<D>( pub fn register<D>(
&mut self, &mut self,
_definition: D, _definition: D,
@@ -176,7 +183,7 @@ impl InterfaceRegistry {
return Err(RegistrationError::AlreadyRegistered); return Err(RegistrationError::AlreadyRegistered);
} }
let mut descriptor = D::descriptor(); let descriptor = D::descriptor();
descriptor descriptor
.validate() .validate()
.map_err(RegistrationError::InvalidDescriptor)?; .map_err(RegistrationError::InvalidDescriptor)?;
@@ -185,11 +192,6 @@ impl InterfaceRegistry {
"wip-interface:{}:{}", "wip-interface:{}:{}",
self.namespace, self.next_reference self.namespace, self.next_reference
)); ));
// The protocol model currently carries the interface identifier in the
// descriptor's `name` field. Finalize that identity to the same opaque
// reference placed on published objects; the generated Rust trait name
// is registration-local metadata, not wire identity.
descriptor.name = reference.0.clone();
self.next_reference = self self.next_reference = self
.next_reference .next_reference
.checked_add(1) .checked_add(1)
@@ -289,7 +291,7 @@ pub trait WipObject {
} }
} }
type DispatchFn<T> = fn(&T, &str, Vec<NamedValue>) -> Result<Option<Value>, DispatchError>; type DispatchFn<T> = fn(&T, &str, BTreeMap<String, Value>) -> Result<Value, DispatchError>;
struct BoundInterface<T> { struct BoundInterface<T> {
artifact: Arc<InterfaceArtifact>, artifact: Arc<InterfaceArtifact>,
@@ -350,13 +352,20 @@ impl<T: WipObject> PublishedObject<T> {
/// Builds the protocol object metadata with interface insertion order intact. /// Builds the protocol object metadata with interface insertion order intact.
#[must_use] #[must_use]
pub fn protocol_object(&self) -> Object { pub fn protocol_object(&self) -> Object {
let id = self.implementation.id();
let object = Object { let object = Object {
id: self.implementation.id(), name: id.0.clone(),
description: None,
interfaces: self interfaces: self
.interfaces .interfaces
.iter() .iter()
.map(|bound| bound.artifact.reference.0.clone()) .map(|bound| bound.artifact.reference.0.clone())
.collect(), .collect(),
r#ref: Some(id.0),
validator: self
.implementation
.validator()
.map(|validator| validator.0.clone()),
}; };
debug_assert!(object.validate().is_ok()); debug_assert!(object.validate().is_ok());
object object
@@ -438,14 +447,7 @@ impl<T: WipObject> PublishedObject<T> {
bound bound
.artifact .artifact
.descriptor .descriptor
.validate_call(&OperationCall { .validate_arguments(&request.operation, &request.arguments)
target: OperationTarget {
object: request.object.clone(),
interface: bound.artifact.descriptor.name.clone(),
operation: request.operation.clone(),
},
arguments: request.arguments.clone(),
})
.map_err(|error| CallError::new(CallErrorKind::InvalidArguments, error.to_string()))?; .map_err(|error| CallError::new(CallErrorKind::InvalidArguments, error.to_string()))?;
let value = (bound.dispatch)(&self.implementation, &request.operation, request.arguments) let value = (bound.dispatch)(&self.implementation, &request.operation, request.arguments)
@@ -454,7 +456,7 @@ impl<T: WipObject> PublishedObject<T> {
bound bound
.artifact .artifact
.descriptor .descriptor
.validate_return(&request.operation, value.as_ref()) .validate_result(&request.operation, &value)
.map_err(|error| CallError::new(CallErrorKind::InvalidResult, error.to_string()))?; .map_err(|error| CallError::new(CallErrorKind::InvalidResult, error.to_string()))?;
Ok(CallResult { Ok(CallResult {
@@ -512,14 +514,14 @@ pub struct CallRequest {
/// Operation name within the selected interface descriptor. /// Operation name within the selected interface descriptor.
pub operation: String, pub operation: String,
/// Named protocol arguments. /// Named protocol arguments.
pub arguments: Vec<NamedValue>, pub arguments: BTreeMap<String, Value>,
} }
/// Successful host dispatch result. /// Successful host dispatch result.
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub struct CallResult { pub struct CallResult {
/// Encoded application value, or `None` for a no-return operation. /// Encoded application result. No-data operations return [`Value::Unit`].
pub value: Option<Value>, pub value: Value,
/// Object validator observed after the implementation returned. /// Object validator observed after the implementation returned.
pub validator: Option<Validator>, pub validator: Option<Validator>,
} }
@@ -610,20 +612,20 @@ impl CallError {
#[must_use] #[must_use]
pub fn protocol_error(&self) -> ProtocolError { pub fn protocol_error(&self) -> ProtocolError {
let category = match self.kind { let category = match self.kind {
CallErrorKind::ObjectNotFound => ProtocolErrorCategory::ObjectNotFound, CallErrorKind::ObjectNotFound => ProtocolErrorCode::NotFound,
CallErrorKind::InterfaceMismatch => ProtocolErrorCategory::InterfaceNotFound, CallErrorKind::InterfaceMismatch => ProtocolErrorCode::InterfaceMismatch,
CallErrorKind::OperationNotFound => ProtocolErrorCategory::OperationNotFound, CallErrorKind::OperationNotFound => ProtocolErrorCode::OperationNotFound,
CallErrorKind::InvalidArguments => ProtocolErrorCategory::InvalidArguments, CallErrorKind::InvalidArguments => ProtocolErrorCode::InvalidArguments,
CallErrorKind::ValidatorRequired CallErrorKind::ValidatorRequired
| CallErrorKind::ValidatorMismatch | CallErrorKind::ValidatorMismatch
| CallErrorKind::InterfaceValidatorRequired | CallErrorKind::InterfaceValidatorRequired
| CallErrorKind::InterfaceValidatorMismatch => ProtocolErrorCategory::InvalidRequest, | CallErrorKind::InterfaceValidatorMismatch => ProtocolErrorCode::InvalidRequest,
CallErrorKind::InvalidResult | CallErrorKind::Implementation => { CallErrorKind::InvalidResult | CallErrorKind::Implementation => {
ProtocolErrorCategory::Internal ProtocolErrorCode::Internal
} }
}; };
ProtocolError { ProtocolError {
category, code: category,
message: self.message.clone(), message: self.message.clone(),
} }
} }
+43 -48
View File
@@ -1,14 +1,14 @@
use std::cell::{Cell, RefCell}; use std::cell::{Cell, RefCell};
use std::collections::BTreeMap;
use wip_host::{ use wip_host::{
CallErrorKind, CallRequest, InterfaceRegistry, InterfaceTarget, PublishedObject, Validator, CallErrorKind, CallRequest, InterfaceRegistry, InterfaceTarget, ObjectId, PublishedObject,
WipObject, Validator, WipObject,
}; };
use wip_interface::{ use wip_interface::{
CodecError, HostError, NamedValue, OperationResult, TypeDeclaration, TypeExpr, Value, WipType, CodecError, HostError, OperationResult, TypeDeclaration, TypeExpr, Value, WipType, interface,
interface,
}; };
use wip_protocol::{ObjectId, ProtocolErrorCategory}; use wip_protocol::ProtocolErrorCode;
#[interface] #[interface]
pub trait Reader { pub trait Reader {
@@ -143,7 +143,7 @@ fn request(
reference: &wip_host::InterfaceRef, reference: &wip_host::InterfaceRef,
interface_validator: Option<&[u8]>, interface_validator: Option<&[u8]>,
operation: &str, operation: &str,
arguments: Vec<NamedValue>, arguments: BTreeMap<String, Value>,
) -> CallRequest { ) -> CallRequest {
CallRequest { CallRequest {
object: ObjectId(object.into()), object: ObjectId(object.into()),
@@ -170,7 +170,7 @@ fn registry_exposes_opaque_validated_descriptor_observations() {
assert!(registry.resolve("wip-interface:unknown:0").is_none()); assert!(registry.resolve("wip-interface:unknown:0").is_none());
let observation = registry.observe(reader.reference()).unwrap(); let observation = registry.observe(reader.reference()).unwrap();
assert_eq!(observation.reference, *reader.reference()); assert_eq!(observation.reference, *reader.reference());
assert_eq!(observation.descriptor.name, reader.reference().as_str()); assert_eq!(observation.descriptor, ReaderDefinition::descriptor());
assert_eq!(observation.validator, None); assert_eq!(observation.validator, None);
observation.descriptor.validate().unwrap(); observation.descriptor.validate().unwrap();
@@ -207,13 +207,16 @@ fn publication_is_explicit_ordered_qualified_and_request_scoped() {
reader.reference().as_str().to_owned() reader.reference().as_str().to_owned()
] ]
); );
wip_protocol::InterfaceObservation { registry
object: object.clone(), .observe(writer.reference())
descriptors: vec![ .unwrap()
registry.observe(writer.reference()).unwrap().descriptor, .descriptor
registry.observe(reader.reference()).unwrap().descriptor, .validate()
], .unwrap();
} registry
.observe(reader.reference())
.unwrap()
.descriptor
.validate() .validate()
.unwrap(); .unwrap();
assert!(!format!("{object:?}").contains("private/backend/path")); assert!(!format!("{object:?}").contains("private/backend/path"));
@@ -225,7 +228,7 @@ fn publication_is_explicit_ordered_qualified_and_request_scoped() {
writer.reference(), writer.reference(),
None, None,
"label", "label",
vec![], BTreeMap::new(),
)) ))
.unwrap(); .unwrap();
let reader_result = publication let reader_result = publication
@@ -235,11 +238,11 @@ fn publication_is_explicit_ordered_qualified_and_request_scoped() {
reader.reference(), reader.reference(),
None, None,
"label", "label",
vec![], BTreeMap::new(),
)) ))
.unwrap(); .unwrap();
assert_eq!(writer_result.value, Some(Value::String("writer".into()))); assert_eq!(writer_result.value, Value::String("writer".into()));
assert_eq!(reader_result.value, Some(Value::String("reader".into()))); assert_eq!(reader_result.value, Value::String("reader".into()));
assert_eq!(publication.implementation().calls.get(), 2); assert_eq!(publication.implementation().calls.get(), 2);
assert_eq!( assert_eq!(
publication publication
@@ -267,13 +270,13 @@ fn implemented_but_unexposed_interface_is_not_a_member() {
hidden.reference(), hidden.reference(),
None, None,
"hidden", "hidden",
vec![], BTreeMap::new(),
)) ))
.unwrap_err(); .unwrap_err();
assert_eq!(error.kind(), CallErrorKind::InterfaceMismatch); assert_eq!(error.kind(), CallErrorKind::InterfaceMismatch);
assert_eq!( assert_eq!(
error.protocol_error().category, error.protocol_error().code,
ProtocolErrorCategory::InterfaceNotFound ProtocolErrorCode::InterfaceMismatch
); );
assert_eq!(publication.implementation().calls.get(), 0); assert_eq!(publication.implementation().calls.get(), 0);
} }
@@ -282,7 +285,8 @@ fn implemented_but_unexposed_interface_is_not_a_member() {
fn zero_interface_objects_are_valid() { fn zero_interface_objects_are_valid() {
let publication = PublishedObject::new(Document::new("empty")); let publication = PublishedObject::new(Document::new("empty"));
let object = publication.protocol_object(); let object = publication.protocol_object();
assert_eq!(object.id, ObjectId("empty".into())); assert_eq!(object.name, "empty");
assert_eq!(object.r#ref.as_deref(), Some("empty"));
assert!(object.interfaces.is_empty()); assert!(object.interfaces.is_empty());
object.validate().unwrap(); object.validate().unwrap();
} }
@@ -316,13 +320,13 @@ fn operation_and_argument_failures_are_mapped_without_execution() {
reader.reference(), reader.reference(),
None, None,
"missing", "missing",
vec![], BTreeMap::new(),
)) ))
.unwrap_err(); .unwrap_err();
assert_eq!(missing.kind(), CallErrorKind::OperationNotFound); assert_eq!(missing.kind(), CallErrorKind::OperationNotFound);
assert_eq!( assert_eq!(
missing.protocol_error().category, missing.protocol_error().code,
ProtocolErrorCategory::OperationNotFound ProtocolErrorCode::OperationNotFound
); );
let invalid = publication let invalid = publication
@@ -332,16 +336,13 @@ fn operation_and_argument_failures_are_mapped_without_execution() {
reader.reference(), reader.reference(),
None, None,
"add", "add",
vec![NamedValue { BTreeMap::from([("left".into(), Value::String("wrong".into()))]),
name: "left".into(),
value: Value::String("wrong".into()),
}],
)) ))
.unwrap_err(); .unwrap_err();
assert_eq!(invalid.kind(), CallErrorKind::InvalidArguments); assert_eq!(invalid.kind(), CallErrorKind::InvalidArguments);
assert_eq!( assert_eq!(
invalid.protocol_error().category, invalid.protocol_error().code,
ProtocolErrorCategory::InvalidArguments ProtocolErrorCode::InvalidArguments
); );
assert_eq!(publication.implementation().calls.get(), 0); assert_eq!(publication.implementation().calls.get(), 0);
} }
@@ -361,14 +362,11 @@ fn invalid_implementation_result_is_internal_and_not_returned() {
broken.reference(), broken.reference(),
None, None,
"broken", "broken",
vec![], BTreeMap::new(),
)) ))
.unwrap_err(); .unwrap_err();
assert_eq!(error.kind(), CallErrorKind::InvalidResult); assert_eq!(error.kind(), CallErrorKind::InvalidResult);
assert_eq!( assert_eq!(error.protocol_error().code, ProtocolErrorCode::Internal);
error.protocol_error().category,
ProtocolErrorCategory::Internal
);
assert_eq!(publication.implementation().calls.get(), 1); assert_eq!(publication.implementation().calls.get(), 1);
} }
@@ -387,7 +385,7 @@ fn object_validator_missing_and_mismatch_are_distinct_and_prevent_execution() {
reader.reference(), reader.reference(),
None, None,
"label", "label",
vec![], BTreeMap::new(),
)) ))
.unwrap_err(); .unwrap_err();
assert_eq!(missing.kind(), CallErrorKind::ValidatorRequired); assert_eq!(missing.kind(), CallErrorKind::ValidatorRequired);
@@ -399,7 +397,7 @@ fn object_validator_missing_and_mismatch_are_distinct_and_prevent_execution() {
reader.reference(), reader.reference(),
None, None,
"label", "label",
vec![], BTreeMap::new(),
)) ))
.unwrap_err(); .unwrap_err();
assert_eq!(mismatch.kind(), CallErrorKind::ValidatorMismatch); assert_eq!(mismatch.kind(), CallErrorKind::ValidatorMismatch);
@@ -412,7 +410,7 @@ fn object_validator_missing_and_mismatch_are_distinct_and_prevent_execution() {
reader.reference(), reader.reference(),
None, None,
"label", "label",
vec![], BTreeMap::new(),
)) ))
.unwrap(); .unwrap();
assert_eq!(result.validator, Some(Validator::new(b"object-v1"))); assert_eq!(result.validator, Some(Validator::new(b"object-v1")));
@@ -439,7 +437,7 @@ fn interface_validator_missing_and_mismatch_are_distinct_and_prevent_execution()
writer.reference(), writer.reference(),
None, None,
"label", "label",
vec![], BTreeMap::new(),
)) ))
.unwrap_err(); .unwrap_err();
assert_eq!(missing.kind(), CallErrorKind::InterfaceValidatorRequired); assert_eq!(missing.kind(), CallErrorKind::InterfaceValidatorRequired);
@@ -451,7 +449,7 @@ fn interface_validator_missing_and_mismatch_are_distinct_and_prevent_execution()
writer.reference(), writer.reference(),
Some(b"schema-v0"), Some(b"schema-v0"),
"label", "label",
vec![], BTreeMap::new(),
)) ))
.unwrap_err(); .unwrap_err();
assert_eq!(mismatch.kind(), CallErrorKind::InterfaceValidatorMismatch); assert_eq!(mismatch.kind(), CallErrorKind::InterfaceValidatorMismatch);
@@ -464,10 +462,10 @@ fn interface_validator_missing_and_mismatch_are_distinct_and_prevent_execution()
writer.reference(), writer.reference(),
Some(b"schema-v1"), Some(b"schema-v1"),
"label", "label",
vec![], BTreeMap::new(),
)) ))
.unwrap(); .unwrap();
assert_eq!(result.value, Some(Value::String("writer".into()))); assert_eq!(result.value, Value::String("writer".into()));
} }
#[test] #[test]
@@ -496,12 +494,9 @@ fn host_failures_map_to_internal_without_becoming_domain_values() {
failing.reference(), failing.reference(),
None, None,
"fail", "fail",
vec![], BTreeMap::new(),
)) ))
.unwrap_err(); .unwrap_err();
assert_eq!(error.kind(), CallErrorKind::Implementation); assert_eq!(error.kind(), CallErrorKind::Implementation);
assert_eq!( assert_eq!(error.protocol_error().code, ProtocolErrorCode::Internal);
error.protocol_error().category,
ProtocolErrorCategory::Internal
);
} }
@@ -1,6 +1,5 @@
use wip_host::{InterfaceRegistry, PublishedObject, WipObject}; use wip_host::{InterfaceRegistry, ObjectId, PublishedObject, WipObject};
use wip_interface::{OperationResult, interface}; use wip_interface::{OperationResult, interface};
use wip_protocol::ObjectId;
#[interface] #[interface]
pub trait Search { pub trait Search {
@@ -1,27 +1,27 @@
error[E0277]: the trait bound `Object: Search` is not satisfied error[E0277]: the trait bound `Object: Search` is not satisfied
--> tests/ui/unimplemented_interface.rs:21:58 --> tests/ui/unimplemented_interface.rs:20:58
| |
21 | let _published = PublishedObject::new(Object).expose(&search).unwrap(); 20 | let _published = PublishedObject::new(Object).expose(&search).unwrap();
| ------ ^^^^^^^ unsatisfied trait bound | ------ ^^^^^^^ unsatisfied trait bound
| | | |
| required by a bound introduced by this call | required by a bound introduced by this call
| |
help: the trait `Search` is not implemented for `Object` help: the trait `Search` is not implemented for `Object`
--> tests/ui/unimplemented_interface.rs:10:1 --> tests/ui/unimplemented_interface.rs:9:1
| |
10 | struct Object; 9 | struct Object;
| ^^^^^^^^^^^^^ | ^^^^^^^^^^^^^
help: this trait has no implementations, consider adding one help: this trait has no implementations, consider adding one
--> tests/ui/unimplemented_interface.rs:6:1
|
6 | pub trait Search {
| ^^^^^^^^^^^^^^^^
note: required for `SearchDefinition` to implement `InterfaceImplementation<Object>`
--> tests/ui/unimplemented_interface.rs:5:1 --> tests/ui/unimplemented_interface.rs:5:1
| |
5 | #[interface] 5 | pub trait Search {
| ^^^^^^^^^^^^^^^^
note: required for `SearchDefinition` to implement `InterfaceImplementation<Object>`
--> tests/ui/unimplemented_interface.rs:4:1
|
4 | #[interface]
| ^^^^^^^^^^^^ | ^^^^^^^^^^^^
6 | pub trait Search { 5 | pub trait Search {
| ^^^^^^ | ^^^^^^
note: required by a bound in `PublishedObject::<T>::expose` note: required by a bound in `PublishedObject::<T>::expose`
--> src/lib.rs --> src/lib.rs
+153 -97
View File
@@ -168,9 +168,11 @@ fn expand_interface(attribute: TokenStream2, item: ItemTrait) -> syn::Result<Tok
#(#declaration_collectors)* #(#declaration_collectors)*
::wip_interface::wip_protocol::InterfaceDescriptor { ::wip_interface::wip_protocol::InterfaceDescriptor {
name: ::std::string::String::from(stringify!(#trait_ident)), format: ::std::string::String::from(
::wip_interface::wip_protocol::INTERFACE_FORMAT_V1,
),
documentation: #trait_doc, documentation: #trait_doc,
declarations, types: declarations,
operations: ::std::vec![#(#operation_declarations),*], operations: ::std::vec![#(#operation_declarations),*],
} }
} }
@@ -180,9 +182,12 @@ fn expand_interface(attribute: TokenStream2, item: ItemTrait) -> syn::Result<Tok
fn dispatch( fn dispatch(
implementation: &T, implementation: &T,
operation: &str, operation: &str,
arguments: ::std::vec::Vec<::wip_interface::wip_protocol::NamedValue>, arguments: ::std::collections::BTreeMap<
::std::string::String,
::wip_interface::wip_protocol::Value,
>,
) -> ::std::result::Result< ) -> ::std::result::Result<
::std::option::Option<::wip_interface::wip_protocol::Value>, ::wip_interface::wip_protocol::Value,
::wip_interface::DispatchError, ::wip_interface::DispatchError,
> { > {
let descriptor = Self::descriptor(); let descriptor = Self::descriptor();
@@ -207,9 +212,12 @@ fn expand_interface(attribute: TokenStream2, item: ItemTrait) -> syn::Result<Tok
pub fn dispatch<T: #trait_ident>( pub fn dispatch<T: #trait_ident>(
implementation: &T, implementation: &T,
operation: &str, operation: &str,
arguments: ::std::vec::Vec<::wip_interface::wip_protocol::NamedValue>, arguments: ::std::collections::BTreeMap<
::std::string::String,
::wip_interface::wip_protocol::Value,
>,
) -> ::std::result::Result< ) -> ::std::result::Result<
::std::option::Option<::wip_interface::wip_protocol::Value>, ::wip_interface::wip_protocol::Value,
::wip_interface::DispatchError, ::wip_interface::DispatchError,
> { > {
<Self as ::wip_interface::InterfaceImplementation<T>>::dispatch( <Self as ::wip_interface::InterfaceImplementation<T>>::dispatch(
@@ -523,12 +531,11 @@ fn argument_item(operation: &Operation) -> TokenStream2 {
let encode_result = if operation.unit_result { let encode_result = if operation.unit_result {
quote! { quote! {
let () = result; let () = result;
::std::result::Result::Ok(::std::option::Option::None) ::std::result::Result::Ok(::wip_interface::wip_protocol::Value::Unit)
} }
} else { } else {
quote! { quote! {
<#result_ty as ::wip_interface::WipType>::encode(result) <#result_ty as ::wip_interface::WipType>::encode(result)
.map(::std::option::Option::Some)
} }
}; };
@@ -541,9 +548,12 @@ fn argument_item(operation: &Operation) -> TokenStream2 {
impl #arguments_ident { impl #arguments_ident {
/// Decodes named logical values into typed operation arguments. /// Decodes named logical values into typed operation arguments.
pub fn decode( pub fn decode(
arguments: ::std::vec::Vec<::wip_interface::wip_protocol::NamedValue>, arguments: ::std::collections::BTreeMap<
::std::string::String,
::wip_interface::wip_protocol::Value,
>,
) -> ::std::result::Result<Self, ::wip_interface::CodecError> { ) -> ::std::result::Result<Self, ::wip_interface::CodecError> {
let mut decoder = ::wip_interface::__private::RecordDecoder::new(arguments)?; let mut decoder = ::wip_interface::__private::RecordDecoder::new(arguments);
let decoded = Self { let decoded = Self {
#(#decoders,)* #(#decoders,)*
}; };
@@ -555,7 +565,7 @@ fn argument_item(operation: &Operation) -> TokenStream2 {
pub fn encode_result( pub fn encode_result(
result: #result_ty, result: #result_ty,
) -> ::std::result::Result< ) -> ::std::result::Result<
::std::option::Option<::wip_interface::wip_protocol::Value>, ::wip_interface::wip_protocol::Value,
::wip_interface::CodecError, ::wip_interface::CodecError,
> { > {
#encode_result #encode_result
@@ -575,22 +585,18 @@ fn operation_declaration(operation: &Operation) -> TokenStream2 {
quote! { quote! {
::wip_interface::wip_protocol::ParameterDeclaration { ::wip_interface::wip_protocol::ParameterDeclaration {
name: ::std::string::String::from(#parameter_name), name: ::std::string::String::from(#parameter_name),
required: !#optional,
documentation: #parameter_doc, documentation: #parameter_doc,
ty: <#ty as ::wip_interface::WipType>::type_expr(), r#type: <#ty as ::wip_interface::WipType>::type_expr(),
optional: #optional,
} }
} }
}); });
let returns = if operation.unit_result {
quote!(::std::option::Option::None)
} else {
let result_ty = &operation.result_ty; let result_ty = &operation.result_ty;
let return_doc = documentation_tokens(operation.return_documentation.as_ref()); let return_doc = documentation_tokens(operation.return_documentation.as_ref());
quote! { let returns = quote! {
::std::option::Option::Some(::wip_interface::wip_protocol::ReturnDeclaration { ::wip_interface::wip_protocol::ReturnDeclaration {
documentation: #return_doc, documentation: #return_doc,
ty: <#result_ty as ::wip_interface::WipType>::type_expr(), r#type: <#result_ty as ::wip_interface::WipType>::type_expr(),
})
} }
}; };
@@ -646,7 +652,7 @@ fn dispatch_arm(operation: &Operation) -> TokenStream2 {
let encoded = #arguments_ident::encode_result(result) let encoded = #arguments_ident::encode_result(result)
.map_err(::wip_interface::DispatchError::Encode)?; .map_err(::wip_interface::DispatchError::Encode)?;
descriptor descriptor
.validate_return(#operation_name, encoded.as_ref()) .validate_result(#operation_name, &encoded)
.map_err(::wip_interface::DispatchError::InvalidResult)?; .map_err(::wip_interface::DispatchError::InvalidResult)?;
::std::result::Result::Ok(encoded) ::std::result::Result::Ok(encoded)
} }
@@ -747,9 +753,9 @@ fn derive_record(
quote! { quote! {
::wip_interface::wip_protocol::FieldDeclaration { ::wip_interface::wip_protocol::FieldDeclaration {
name: ::std::string::String::from(#name), name: ::std::string::String::from(#name),
required: !#optional,
documentation: #documentation, documentation: #documentation,
ty: <#ty as ::wip_interface::WipType>::type_expr(), r#type: <#ty as ::wip_interface::WipType>::type_expr(),
optional: #optional,
} }
} }
}); });
@@ -765,18 +771,18 @@ fn derive_record(
if field.optional { if field.optional {
quote! { quote! {
if let ::std::option::Option::Some(value) = #field_ident { if let ::std::option::Option::Some(value) = #field_ident {
fields.push(::wip_interface::wip_protocol::NamedValue { fields.insert(
name: ::std::string::String::from(#field_name), ::std::string::String::from(#field_name),
value: <#ty as ::wip_interface::WipType>::encode(value)?, <#ty as ::wip_interface::WipType>::encode(value)?,
}); );
} }
} }
} else { } else {
quote! { quote! {
fields.push(::wip_interface::wip_protocol::NamedValue { fields.insert(
name: ::std::string::String::from(#field_name), ::std::string::String::from(#field_name),
value: <#ty as ::wip_interface::WipType>::encode(#field_ident)?, <#ty as ::wip_interface::WipType>::encode(#field_ident)?,
}); );
} }
} }
}); });
@@ -794,9 +800,9 @@ fn derive_record(
Ok(quote! { Ok(quote! {
impl ::wip_interface::WipType for #ident { impl ::wip_interface::WipType for #ident {
fn type_expr() -> ::wip_interface::wip_protocol::TypeExpr { fn type_expr() -> ::wip_interface::wip_protocol::TypeExpr {
::wip_interface::wip_protocol::TypeExpr::Named( ::wip_interface::wip_protocol::TypeExpr::Named {
::std::string::String::from(#type_name), name: ::std::string::String::from(#type_name),
) }
} }
fn collect_declarations( fn collect_declarations(
@@ -804,13 +810,13 @@ fn derive_record(
::wip_interface::wip_protocol::TypeDeclaration, ::wip_interface::wip_protocol::TypeDeclaration,
>, >,
) { ) {
let declaration = ::wip_interface::wip_protocol::TypeDeclaration::Record( let declaration = ::wip_interface::wip_protocol::TypeDeclaration {
::wip_interface::wip_protocol::RecordDeclaration {
name: ::std::string::String::from(#type_name), name: ::std::string::String::from(#type_name),
documentation: #declaration_doc, documentation: #declaration_doc,
definition: ::wip_interface::wip_protocol::TypeExpr::Record {
fields: ::std::vec![#(#field_declarations),*], fields: ::std::vec![#(#field_declarations),*],
}, },
); };
if ::wip_interface::__private::register_declaration(declarations, declaration) { if ::wip_interface::__private::register_declaration(declarations, declaration) {
#(#dependency_collectors)* #(#dependency_collectors)*
} }
@@ -823,7 +829,7 @@ fn derive_record(
::wip_interface::CodecError, ::wip_interface::CodecError,
> { > {
let Self { #(#destructured_fields),* } = self; let Self { #(#destructured_fields),* } = self;
let mut fields = ::std::vec::Vec::new(); let mut fields = ::std::collections::BTreeMap::new();
#(#encoders)* #(#encoders)*
::std::result::Result::Ok( ::std::result::Result::Ok(
::wip_interface::wip_protocol::Value::Record(fields), ::wip_interface::wip_protocol::Value::Record(fields),
@@ -835,7 +841,7 @@ fn derive_record(
) -> ::std::result::Result<Self, ::wip_interface::CodecError> { ) -> ::std::result::Result<Self, ::wip_interface::CodecError> {
match value { match value {
::wip_interface::wip_protocol::Value::Record(fields) => { ::wip_interface::wip_protocol::Value::Record(fields) => {
let mut decoder = ::wip_interface::__private::RecordDecoder::new(fields)?; let mut decoder = ::wip_interface::__private::RecordDecoder::new(fields);
let decoded = Self { let decoded = Self {
#(#decoders,)* #(#decoders,)*
}; };
@@ -870,14 +876,14 @@ fn derive_enum(
.variants .variants
.iter() .iter()
.all(|variant| matches!(variant.fields, Fields::Unit)); .all(|variant| matches!(variant.fields, Fields::Unit));
let all_payload = !data.variants.is_empty() let valid_union = data.variants.iter().all(|variant| {
&& data.variants.iter().all(|variant| { matches!(variant.fields, Fields::Unit)
matches!(&variant.fields, Fields::Unnamed(fields) if fields.unnamed.len() == 1) || matches!(&variant.fields, Fields::Unnamed(fields) if fields.unnamed.len() == 1)
}); });
if all_unit { if all_unit {
derive_unit_enum(ident, type_name, type_documentation, data) derive_unit_enum(ident, type_name, type_documentation, data)
} else if all_payload { } else if valid_union {
derive_union_enum(ident, type_name, type_documentation, data) derive_union_enum(ident, type_name, type_documentation, data)
} else { } else {
let span = data let span = data
@@ -890,7 +896,7 @@ fn derive_enum(
.map_or_else(|| data.enum_token.span, Spanned::span); .map_or_else(|| data.enum_token.span, Spanned::span);
Err(Error::new( Err(Error::new(
span, span,
"a `WipType` enum must contain either only unit variants or only single-field tuple variants; mixed and named-field variants are unsupported", "a `WipType` enum must contain unit variants and/or single-field tuple variants; named-field and multi-field variants are unsupported",
)) ))
} }
} }
@@ -906,7 +912,7 @@ fn derive_unit_enum(
let name = source_name(&variant.ident); let name = source_name(&variant.ident);
let documentation = documentation_tokens(documentation(&variant.attrs).as_ref()); let documentation = documentation_tokens(documentation(&variant.attrs).as_ref());
quote! { quote! {
::wip_interface::wip_protocol::EnumVariantDeclaration { ::wip_interface::wip_protocol::EnumCase {
name: ::std::string::String::from(#name), name: ::std::string::String::from(#name),
documentation: #documentation, documentation: #documentation,
} }
@@ -926,9 +932,9 @@ fn derive_unit_enum(
Ok(quote! { Ok(quote! {
impl ::wip_interface::WipType for #ident { impl ::wip_interface::WipType for #ident {
fn type_expr() -> ::wip_interface::wip_protocol::TypeExpr { fn type_expr() -> ::wip_interface::wip_protocol::TypeExpr {
::wip_interface::wip_protocol::TypeExpr::Named( ::wip_interface::wip_protocol::TypeExpr::Named {
::std::string::String::from(#type_name), name: ::std::string::String::from(#type_name),
) }
} }
fn collect_declarations( fn collect_declarations(
@@ -936,13 +942,13 @@ fn derive_unit_enum(
::wip_interface::wip_protocol::TypeDeclaration, ::wip_interface::wip_protocol::TypeDeclaration,
>, >,
) { ) {
let declaration = ::wip_interface::wip_protocol::TypeDeclaration::Enum( let declaration = ::wip_interface::wip_protocol::TypeDeclaration {
::wip_interface::wip_protocol::EnumDeclaration {
name: ::std::string::String::from(#type_name), name: ::std::string::String::from(#type_name),
documentation: #declaration_doc, documentation: #declaration_doc,
variants: ::std::vec![#(#declarations),*], definition: ::wip_interface::wip_protocol::TypeExpr::Enum {
cases: ::std::vec![#(#declarations),*],
}, },
); };
let _ = ::wip_interface::__private::register_declaration( let _ = ::wip_interface::__private::register_declaration(
declarations, declarations,
declaration, declaration,
@@ -959,7 +965,7 @@ fn derive_unit_enum(
#(#encoders,)* #(#encoders,)*
}; };
::std::result::Result::Ok( ::std::result::Result::Ok(
::wip_interface::wip_protocol::Value::Enum( ::wip_interface::wip_protocol::Value::String(
::std::string::String::from(variant), ::std::string::String::from(variant),
), ),
) )
@@ -969,7 +975,7 @@ fn derive_unit_enum(
value: ::wip_interface::wip_protocol::Value, value: ::wip_interface::wip_protocol::Value,
) -> ::std::result::Result<Self, ::wip_interface::CodecError> { ) -> ::std::result::Result<Self, ::wip_interface::CodecError> {
match value { match value {
::wip_interface::wip_protocol::Value::Enum(variant) => { ::wip_interface::wip_protocol::Value::String(variant) => {
match variant.as_str() { match variant.as_str() {
#(#decoders,)* #(#decoders,)*
_ => ::std::result::Result::Err( _ => ::std::result::Result::Err(
@@ -998,35 +1004,36 @@ fn derive_union_enum(
struct Variant<'a> { struct Variant<'a> {
ident: &'a Ident, ident: &'a Ident,
name: String, name: String,
ty: &'a Type, payload: Option<&'a Type>,
documentation: Option<Documentation>, documentation: Option<Documentation>,
} }
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 payload = match &variant.fields {
return Err(Error::new_spanned( Fields::Unit => None,
variant, Fields::Unnamed(fields) if fields.unnamed.len() == 1 => {
"a `WipType` union variant must contain exactly one unnamed field", let field = fields.unnamed.first().expect("length checked");
));
};
let Some(field) = fields.unnamed.first() else {
return Err(Error::new_spanned(
fields,
"a `WipType` union variant must contain exactly one unnamed 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,
"`Option<T>` is only supported directly on DTO fields and operation parameters", "`Option<T>` is only supported directly on DTO fields and operation parameters",
)); ));
} }
validate_value_type(&field.ty, "union variant payload")?; validate_value_type(&field.ty, "union case payload")?;
Some(&field.ty)
}
_ => {
return Err(Error::new_spanned(
variant,
"a `WipType` union case must be unit-like or contain exactly one unnamed payload",
));
}
};
variants.push(Variant { variants.push(Variant {
ident: &variant.ident, ident: &variant.ident,
name: source_name(&variant.ident), name: source_name(&variant.ident),
ty: &field.ty, payload,
documentation: documentation(&variant.attrs), documentation: documentation(&variant.attrs),
}); });
} }
@@ -1035,50 +1042,95 @@ fn derive_union_enum(
let declarations = variants.iter().map(|variant| { let declarations = variants.iter().map(|variant| {
let name = &variant.name; let name = &variant.name;
let documentation = documentation_tokens(variant.documentation.as_ref()); let documentation = documentation_tokens(variant.documentation.as_ref());
let ty = variant.ty; let payload = match variant.payload {
Some(ty) => quote! {
::std::option::Option::Some(
<#ty as ::wip_interface::WipType>::type_expr(),
)
},
None => quote!(::std::option::Option::None),
};
quote! { quote! {
::wip_interface::wip_protocol::UnionVariantDeclaration { ::wip_interface::wip_protocol::UnionCase {
name: ::std::string::String::from(#name), name: ::std::string::String::from(#name),
documentation: #documentation, documentation: #documentation,
ty: <#ty as ::wip_interface::WipType>::type_expr(), payload: #payload,
} }
} }
}); });
let dependency_collectors = variants.iter().map(|variant| { let dependency_collectors = variants.iter().filter_map(|variant| {
let ty = variant.ty; variant.payload.map(
quote!(<#ty as ::wip_interface::WipType>::collect_declarations(declarations);) |ty| quote!(<#ty as ::wip_interface::WipType>::collect_declarations(declarations);),
)
}); });
let encoders = variants.iter().map(|variant| { let encoders = variants.iter().map(|variant| {
let variant_ident = variant.ident; let variant_ident = variant.ident;
let name = &variant.name; let name = &variant.name;
let ty = variant.ty; match variant.payload {
quote! { Some(ty) => quote! {
Self::#variant_ident(value) => { Self::#variant_ident(value) => {
::wip_interface::wip_protocol::Value::Union { let mut fields = ::std::collections::BTreeMap::new();
variant: ::std::string::String::from(#name), fields.insert(
value: ::std::boxed::Box::new( ::std::string::String::from(
<#ty as ::wip_interface::WipType>::encode(value)?, ::wip_interface::wip_protocol::UNION_CASE_FIELD,
), ),
::wip_interface::wip_protocol::Value::String(
::std::string::String::from(#name),
),
);
fields.insert(
::std::string::String::from(
::wip_interface::wip_protocol::UNION_VALUE_FIELD,
),
<#ty as ::wip_interface::WipType>::encode(value)?,
);
::wip_interface::wip_protocol::Value::Record(fields)
} }
},
None => quote! {
Self::#variant_ident => {
let mut fields = ::std::collections::BTreeMap::new();
fields.insert(
::std::string::String::from(
::wip_interface::wip_protocol::UNION_CASE_FIELD,
),
::wip_interface::wip_protocol::Value::String(
::std::string::String::from(#name),
),
);
::wip_interface::wip_protocol::Value::Record(fields)
} }
},
} }
}); });
let decoders = variants.iter().map(|variant| { let decoders = variants.iter().map(|variant| {
let variant_ident = variant.ident; let variant_ident = variant.ident;
let name = &variant.name; let name = &variant.name;
let ty = variant.ty; match variant.payload {
quote! { Some(ty) => quote! {
#name => <#ty as ::wip_interface::WipType>::decode(*value) #name => {
.map(Self::#variant_ident) let value = decoder.required::<#ty>(
::wip_interface::wip_protocol::UNION_VALUE_FIELD,
)?;
decoder.finish()?;
::std::result::Result::Ok(Self::#variant_ident(value))
}
},
None => quote! {
#name => {
decoder.finish()?;
::std::result::Result::Ok(Self::#variant_ident)
}
},
} }
}); });
Ok(quote! { Ok(quote! {
impl ::wip_interface::WipType for #ident { impl ::wip_interface::WipType for #ident {
fn type_expr() -> ::wip_interface::wip_protocol::TypeExpr { fn type_expr() -> ::wip_interface::wip_protocol::TypeExpr {
::wip_interface::wip_protocol::TypeExpr::Named( ::wip_interface::wip_protocol::TypeExpr::Named {
::std::string::String::from(#type_name), name: ::std::string::String::from(#type_name),
) }
} }
fn collect_declarations( fn collect_declarations(
@@ -1086,13 +1138,13 @@ fn derive_union_enum(
::wip_interface::wip_protocol::TypeDeclaration, ::wip_interface::wip_protocol::TypeDeclaration,
>, >,
) { ) {
let declaration = ::wip_interface::wip_protocol::TypeDeclaration::Union( let declaration = ::wip_interface::wip_protocol::TypeDeclaration {
::wip_interface::wip_protocol::UnionDeclaration {
name: ::std::string::String::from(#type_name), name: ::std::string::String::from(#type_name),
documentation: #declaration_doc, documentation: #declaration_doc,
variants: ::std::vec![#(#declarations),*], definition: ::wip_interface::wip_protocol::TypeExpr::Union {
cases: ::std::vec![#(#declarations),*],
}, },
); };
if ::wip_interface::__private::register_declaration(declarations, declaration) { if ::wip_interface::__private::register_declaration(declarations, declaration) {
#(#dependency_collectors)* #(#dependency_collectors)*
} }
@@ -1114,26 +1166,30 @@ fn derive_union_enum(
value: ::wip_interface::wip_protocol::Value, value: ::wip_interface::wip_protocol::Value,
) -> ::std::result::Result<Self, ::wip_interface::CodecError> { ) -> ::std::result::Result<Self, ::wip_interface::CodecError> {
match value { match value {
::wip_interface::wip_protocol::Value::Union { variant, value } => { ::wip_interface::wip_protocol::Value::Record(fields) => {
match variant.as_str() { let mut decoder =
::wip_interface::__private::RecordDecoder::new(fields);
let case = decoder.required::<::std::string::String>(
::wip_interface::wip_protocol::UNION_CASE_FIELD,
)?;
match case.as_str() {
#(#decoders,)* #(#decoders,)*
_ => ::std::result::Result::Err( _ => ::std::result::Result::Err(
::wip_interface::__private::decode_unknown_variant( ::wip_interface::__private::decode_unknown_variant(
#type_name, #type_name,
variant, case,
), ),
), ),
} }
} }
actual => ::std::result::Result::Err( actual => ::std::result::Result::Err(
::wip_interface::__private::decode_type_mismatch("union", actual), ::wip_interface::__private::decode_type_mismatch("union record", actual),
), ),
} }
} }
} }
}) })
} }
fn optional_type(ty: &Type, context: &str) -> syn::Result<(bool, Type)> { fn optional_type(ty: &Type, context: &str) -> syn::Result<(bool, Type)> {
if let Some(inner) = direct_option(ty)? { if let Some(inner) = direct_option(ty)? {
if contains_option(&inner) { if contains_option(&inner) {
+207 -99
View File
@@ -30,18 +30,18 @@
//! //!
//! | Rust type | Protocol type/value | //! | Rust type | Protocol type/value |
//! | --- | --- | //! | --- | --- |
//! | `()` as an operation result | no return declaration / no value | //! | `()` | `Unit` |
//! | `bool` | `Boolean` | //! | `bool` | `Boolean` |
//! | signed and unsigned integer primitives | JavaScript-lossless `Integer` | //! | signed and unsigned integer primitives | JavaScript-lossless `Integer` |
//! | `f32`, `f64` | finite `Number` | //! | `f32`, `f64` | finite `Number` |
//! | `String` | `String` | //! | `String` | `String` |
//! | [`Bytes`] | `Bytes` | //! | [`Bytes`] | `Bytes` |
//! | [`wip_protocol::JsonValue`] | `Json` | //! | [`JsonValue`] | schema-neutral `Json` value tree |
//! | [`Entry<K, V>`] | `Entry<K, V>` | //! | [`Entry`] | canonical absolute Worldspace path (`Entry`) |
//! | `Vec<T>` | `List<T>` | //! | `Vec<T>` | `List<T>` |
//! | a named-field struct deriving [`WipType`] | named record | //! | a named-field struct deriving [`WipType`] | named record |
//! | an all-unit enum deriving [`WipType`] | named enum | //! | an all-unit enum deriving [`WipType`] | named enum |
//! | an all-newtype enum deriving [`WipType`] | named tagged union | //! | an enum mixing unit and single-payload cases deriving [`WipType`] | named union |
//! //!
//! `Option<T>` is supported only directly as a record field or operation //! `Option<T>` is supported only directly as a record field or operation
//! parameter. It means omission, not a null value. A domain failure belongs in //! parameter. It means omission, not a null value. A domain failure belongs in
@@ -49,33 +49,44 @@
//! [`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.
//! //!
//! # Migration to canonical values
//!
//! Earlier releases exposed `Entry<K, V>` and semantic `Value::Json`,
//! `Value::Entry`, `Value::Enum`, and `Value::Union` variants. Those forms are
//! intentionally gone: [`Entry`] is now one validated absolute Worldspace path,
//! while JSON, enum, and union meaning comes only from the descriptor. Codecs
//! exchange only raw [`Value`] strings, records, lists, and primitives. Unit
//! operation results now cross the boundary as [`Value::Unit`] rather than an
//! absent value.
//!
//! # Supported interface syntax //! # Supported interface syntax
//! //!
//! An interface is a public, non-generic trait whose operations use exactly //! An interface is a public, non-generic trait whose operations use exactly
//! `&self`, simple named owned parameters, and an explicit //! `&self`, simple named owned parameters, and an explicit
//! `OperationResult<T>`. `WipType` derives support named-field structs, //! `OperationResult<T>`. `WipType` derives support named-field structs,
//! all-unit enums, and all-single-payload enums. //! all-unit enums as protocol enums, and enums made from any mix of unit and
//! single-payload cases as protocol unions.
//! //!
//! The macros reject trait inheritance and associated items; const, async, //! The macros reject trait inheritance and associated items; const, async,
//! unsafe, extern, variadic, generic, default-bodied, or non-`&self` methods; //! unsafe, extern, variadic, generic, default-bodied, or non-`&self` methods;
//! borrowed values and custom lifetimes; tuple, unit, or generic structs; //! borrowed values and custom lifetimes; tuple, unit, or generic structs;
//! mixed or named-field enum variants; explicit discriminants; Rust unions; //! unsupported named-field or multi-field enum variants; explicit discriminants; Rust unions;
//! and `Option` anywhere except a direct DTO field or operation parameter. //! and `Option` anywhere except a direct DTO field or operation parameter.
//! Unsupported inputs produce source-located compile errors rather than //! Unsupported inputs produce source-located compile errors rather than
//! generating a partial interface. //! generating a partial interface.
#![deny(missing_docs)] #![deny(missing_docs)]
use std::collections::BTreeMap;
use std::error::Error; use std::error::Error;
use std::fmt::{self, Display, Formatter}; use std::fmt::{self, Display, Formatter};
pub use wip_interface_macros::{WipType, interface}; pub use wip_interface_macros::{WipType, interface};
pub use wip_protocol; pub use wip_protocol;
pub use wip_protocol::{ pub use wip_protocol::{
Documentation, EnumDeclaration, EnumVariantDeclaration, FieldDeclaration, InterfaceDescriptor, Documentation, EnumCase, FieldDeclaration, InterfaceDescriptor, MAX_SAFE_INTEGER,
JsonMember, JsonValue, MAX_SAFE_INTEGER, MIN_SAFE_INTEGER, NamedValue, OperationDeclaration, MIN_SAFE_INTEGER, OperationDeclaration, ParameterDeclaration, ReturnDeclaration,
ParameterDeclaration, RecordDeclaration, ReturnDeclaration, TypeDeclaration, TypeExpr, TypeDeclaration, TypeExpr, UnionCase, ValidationError, Value,
UnionDeclaration, UnionVariantDeclaration, ValidationError, Value,
}; };
/// Converts one Rust application type to and from its protocol representation. /// Converts one Rust application type to and from its protocol representation.
@@ -118,8 +129,8 @@ pub trait InterfaceImplementation<T>: InterfaceDefinition {
fn dispatch( fn dispatch(
implementation: &T, implementation: &T,
operation: &str, operation: &str,
arguments: Vec<NamedValue>, arguments: BTreeMap<String, Value>,
) -> Result<Option<Value>, DispatchError>; ) -> Result<Value, DispatchError>;
} }
/// Result convention required for generated interface operations. /// Result convention required for generated interface operations.
@@ -281,13 +292,101 @@ impl From<Bytes> for Vec<u8> {
} }
} }
/// A typed protocol key/value entry. /// A canonical absolute path to an entry in the current Worldspace.
#[derive(Debug, Clone, PartialEq, Eq)] ///
pub struct Entry<K, V> { /// This replaces the pre-canonical `Entry<K, V>` key/value pair. Construct an
/// Entry key. /// entry from a path such as `/articles/1`; application data associated with
pub key: K, /// that entry belongs in an ordinary record alongside this value.
/// Entry value. #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub value: V, pub struct Entry(String);
impl Entry {
/// Validates and creates an entry path.
pub fn new(path: impl Into<String>) -> Result<Self, CodecError> {
let path = path.into();
validate_entry_path(&path)?;
Ok(Self(path))
}
/// Returns the canonical absolute Worldspace path.
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
/// Consumes the entry and returns its path.
#[must_use]
pub fn into_string(self) -> String {
self.0
}
}
impl TryFrom<String> for Entry {
type Error = CodecError;
fn try_from(path: String) -> Result<Self, Self::Error> {
Self::new(path)
}
}
impl std::str::FromStr for Entry {
type Err = CodecError;
fn from_str(path: &str) -> Result<Self, Self::Err> {
Self::new(path)
}
}
/// One member in a [`JsonValue::Object`].
#[derive(Debug, Clone, PartialEq)]
pub struct JsonMember {
/// Member name.
pub name: String,
/// Member value.
pub value: JsonValue,
}
/// JSON data represented independently of any application schema.
///
/// Its codec uses the ordinary schema-neutral [`Value`] variants. JSON null is
/// [`Value::Unit`], arrays are [`Value::List`], and objects are
/// [`Value::Record`]. Protocol bytes have no JSON representation and decoding
/// a tree containing [`Value::Bytes`] is rejected.
#[derive(Debug, Clone, PartialEq)]
pub enum JsonValue {
/// JSON null.
Null,
/// A JSON boolean.
Boolean(bool),
/// An interoperable integral JSON number.
Integer(i64),
/// A finite non-integral JSON number.
Number(f64),
/// A JSON string.
String(String),
/// A JSON array.
Array(Vec<JsonValue>),
/// A JSON object. Duplicate member names are rejected while encoding.
Object(Vec<JsonMember>),
}
impl WipType for () {
fn type_expr() -> TypeExpr {
TypeExpr::Unit
}
fn collect_declarations(_: &mut Vec<TypeDeclaration>) {}
fn encode(self) -> Result<Value, CodecError> {
Ok(Value::Unit)
}
fn decode(value: Value) -> Result<Self, CodecError> {
match value {
Value::Unit => Ok(()),
other => Err(CodecError::expected("unit", &other)),
}
}
} }
impl WipType for bool { impl WipType for bool {
@@ -456,52 +555,87 @@ impl WipType for JsonValue {
fn collect_declarations(_: &mut Vec<TypeDeclaration>) {} fn collect_declarations(_: &mut Vec<TypeDeclaration>) {}
fn encode(self) -> Result<Value, CodecError> { fn encode(self) -> Result<Value, CodecError> {
validate_json(&self)?; encode_json(self)
Ok(Value::Json(self))
} }
fn decode(value: Value) -> Result<Self, CodecError> { fn decode(value: Value) -> Result<Self, CodecError> {
match value { decode_json(value)
Value::Json(value) => {
validate_json(&value)?;
Ok(value)
} }
other => Err(CodecError::expected("json", &other)), }
fn encode_json(value: JsonValue) -> Result<Value, CodecError> {
match value {
JsonValue::Null => Ok(Value::Unit),
JsonValue::Boolean(value) => Ok(Value::Boolean(value)),
JsonValue::Integer(value) => {
ensure_safe_integer(value)?;
Ok(Value::Integer(value))
}
JsonValue::Number(value) => {
ensure_finite(value)?;
Ok(Value::Number(value))
}
JsonValue::String(value) => Ok(Value::String(value)),
JsonValue::Array(values) => values
.into_iter()
.enumerate()
.map(|(index, value)| encode_json(value).map_err(|error| error.at(index.to_string())))
.collect::<Result<Vec<_>, _>>()
.map(Value::List),
JsonValue::Object(members) => {
let mut fields = BTreeMap::new();
for member in members {
let name = member.name;
let value = encode_json(member.value).map_err(|error| error.at(name.clone()))?;
if fields.insert(name.clone(), value).is_some() {
return Err(CodecError::new(format!("duplicate JSON member `{name}`")).at(name));
}
}
Ok(Value::Record(fields))
} }
} }
} }
fn validate_json(value: &JsonValue) -> Result<(), CodecError> { fn decode_json(value: Value) -> Result<JsonValue, CodecError> {
match value { match value {
JsonValue::Integer(value) => ensure_safe_integer(*value), Value::Unit => Ok(JsonValue::Null),
JsonValue::Number(value) => ensure_finite(*value), Value::Boolean(value) => Ok(JsonValue::Boolean(value)),
JsonValue::Array(values) => { Value::Integer(value) => {
for (index, value) in values.iter().enumerate() { ensure_safe_integer(value)?;
validate_json(value).map_err(|error| error.at(index.to_string()))?; Ok(JsonValue::Integer(value))
} }
Ok(()) Value::Number(value) => {
ensure_finite(value)?;
Ok(JsonValue::Number(value))
} }
JsonValue::Object(members) => { Value::String(value) => Ok(JsonValue::String(value)),
let mut names = std::collections::HashSet::new(); Value::Bytes(_) => Err(CodecError::new("bytes are not valid inside JSON")),
for member in members { Value::List(values) => values
if !names.insert(member.name.as_str()) { .into_iter()
return Err(CodecError::new(format!( .enumerate()
"duplicate JSON member `{}`", .map(|(index, value)| decode_json(value).map_err(|error| error.at(index.to_string())))
member.name .collect::<Result<Vec<_>, _>>()
)) .map(JsonValue::Array),
.at(member.name.clone())); Value::Record(fields) => fields
} .into_iter()
validate_json(&member.value).map_err(|error| error.at(member.name.clone()))?; .map(|(name, value)| {
} decode_json(value)
Ok(()) .map(|value| JsonMember {
} name: name.clone(),
JsonValue::Null | JsonValue::Boolean(_) | JsonValue::String(_) => Ok(()), value,
})
.map_err(|error| error.at(name))
})
.collect::<Result<Vec<_>, _>>()
.map(JsonValue::Object),
} }
} }
impl<T: WipType> WipType for Vec<T> { impl<T: WipType> WipType for Vec<T> {
fn type_expr() -> TypeExpr { fn type_expr() -> TypeExpr {
TypeExpr::List(Box::new(T::type_expr())) TypeExpr::List {
items: Box::new(T::type_expr()),
}
} }
fn collect_declarations(declarations: &mut Vec<TypeDeclaration>) { fn collect_declarations(declarations: &mut Vec<TypeDeclaration>) {
@@ -528,37 +662,31 @@ impl<T: WipType> WipType for Vec<T> {
} }
} }
impl<K: WipType, V: WipType> WipType for Entry<K, V> { impl WipType for Entry {
fn type_expr() -> TypeExpr { fn type_expr() -> TypeExpr {
TypeExpr::Entry { TypeExpr::Entry
key: Box::new(K::type_expr()),
value: Box::new(V::type_expr()),
}
} }
fn collect_declarations(declarations: &mut Vec<TypeDeclaration>) { fn collect_declarations(_: &mut Vec<TypeDeclaration>) {}
K::collect_declarations(declarations);
V::collect_declarations(declarations);
}
fn encode(self) -> Result<Value, CodecError> { fn encode(self) -> Result<Value, CodecError> {
Ok(Value::Entry { validate_entry_path(&self.0)?;
key: Box::new(self.key.encode().map_err(|error| error.at("key"))?), Ok(Value::String(self.0))
value: Box::new(self.value.encode().map_err(|error| error.at("value"))?),
})
} }
fn decode(value: Value) -> Result<Self, CodecError> { fn decode(value: Value) -> Result<Self, CodecError> {
let Value::Entry { key, value } = value else { let Value::String(path) = value else {
return Err(CodecError::expected("entry", &value)); return Err(CodecError::expected("entry path string", &value));
}; };
Ok(Self { Self::new(path)
key: K::decode(*key).map_err(|error| error.at("key"))?,
value: V::decode(*value).map_err(|error| error.at("value"))?,
})
} }
} }
fn validate_entry_path(path: &str) -> Result<(), CodecError> {
wip_protocol::validate_path(path)
.map_err(|error| CodecError::new(format!("invalid canonical Worldspace path: {error}")))
}
/// Implementation details used by code emitted from the facade's macros. /// Implementation details used by code emitted from the facade's macros.
/// ///
/// These APIs are public only because macro expansions execute in the /// These APIs are public only because macro expansions execute in the
@@ -566,12 +694,9 @@ impl<K: WipType, V: WipType> WipType for Entry<K, V> {
/// and are not intended for direct use. /// and are not intended for direct use.
#[doc(hidden)] #[doc(hidden)]
pub mod __private { pub mod __private {
use std::collections::HashSet; use std::collections::BTreeMap;
use wip_protocol::{ use wip_protocol::{InterfaceDescriptor, TypeDeclaration, ValidationError, Value};
InterfaceDescriptor, NamedValue, ObjectId, OperationCall, OperationTarget, TypeDeclaration,
ValidationError, Value,
};
use crate::{CodecError, Documentation, WipType}; use crate::{CodecError, Documentation, WipType};
@@ -606,16 +731,9 @@ pub mod __private {
pub fn validate_arguments( pub fn validate_arguments(
descriptor: &InterfaceDescriptor, descriptor: &InterfaceDescriptor,
operation: &str, operation: &str,
arguments: &[NamedValue], arguments: &BTreeMap<String, Value>,
) -> Result<(), ValidationError> { ) -> Result<(), ValidationError> {
descriptor.validate_call(&OperationCall { descriptor.validate_arguments(operation, arguments)
target: OperationTarget {
object: ObjectId("<typed-dispatch>".into()),
interface: descriptor.name.clone(),
operation: operation.into(),
},
arguments: arguments.to_vec(),
})
} }
/// Builds a codec error for a generated structural mismatch. /// Builds a codec error for a generated structural mismatch.
@@ -633,21 +751,13 @@ 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: BTreeMap<String, Value>,
} }
impl RecordDecoder { impl RecordDecoder {
/// Starts decoding record fields or named operation arguments and /// Starts decoding record fields or named operation arguments.
/// rejects duplicate names. pub fn new(fields: BTreeMap<String, Value>) -> Self {
pub fn new(fields: Vec<NamedValue>) -> Result<Self, CodecError> { Self { fields }
let mut seen = HashSet::new();
for field in &fields {
if !seen.insert(field.name.clone()) {
return Err(CodecError::new(format!("duplicate field `{}`", field.name))
.at(field.name.clone()));
}
}
Ok(Self { fields })
} }
/// Decodes a required field. /// Decodes a required field.
@@ -668,17 +778,15 @@ pub mod __private {
/// Rejects any fields that were not consumed by generated code. /// Rejects any fields that were not consumed by generated code.
pub fn finish(self) -> Result<(), CodecError> { pub fn finish(self) -> Result<(), CodecError> {
if let Some(field) = self.fields.first() { if let Some(name) = self.fields.keys().next() {
Err(CodecError::new(format!("unknown field `{}`", field.name)) Err(CodecError::new(format!("unknown field `{name}`")).at(name.clone()))
.at(field.name.clone()))
} else { } else {
Ok(()) Ok(())
} }
} }
fn take(&mut self, name: &str) -> Option<Value> { fn take(&mut self, name: &str) -> Option<Value> {
let index = self.fields.iter().position(|field| field.name == name)?; self.fields.remove(name)
Some(self.fields.remove(index).value)
} }
} }
} }
+115 -52
View File
@@ -1,8 +1,7 @@
use wip_interface::wip_protocol::{ use std::collections::BTreeMap;
EnumDeclaration, JsonMember, JsonValue, NamedValue, RecordDeclaration, TypeDeclaration,
TypeExpr, UnionDeclaration, Value, use wip_interface::wip_protocol::{TypeExpr, UNION_CASE_FIELD, UNION_VALUE_FIELD, Value};
}; use wip_interface::{Bytes, Entry, JsonMember, JsonValue, WipType};
use wip_interface::{Bytes, Entry, WipType};
/// A request record. /// A request record.
#[derive(Debug, Clone, PartialEq, WipType)] #[derive(Debug, Clone, PartialEq, WipType)]
@@ -11,8 +10,8 @@ struct Request {
title: String, title: String,
/// Optional priority. /// Optional priority.
priority: Option<i32>, priority: Option<i32>,
/// Related entries. /// Related Worldspace entries.
related: Vec<Entry<String, bool>>, related: Vec<Entry>,
/// Opaque bytes. /// Opaque bytes.
body: Bytes, body: Bytes,
/// Arbitrary metadata. /// Arbitrary metadata.
@@ -38,33 +37,53 @@ struct Receipt {
/// Application-level publication outcome. /// Application-level publication outcome.
#[derive(Debug, Clone, PartialEq, WipType)] #[derive(Debug, Clone, PartialEq, WipType)]
enum PublishOutcome { enum PublishOutcome {
/// Work has not completed and carries no payload.
Pending,
/// Application success. /// Application success.
Published(Receipt), Published(Receipt),
/// Domain rejection, not a host failure. /// Domain rejection, not a host failure.
Rejected(String), Rejected(String),
} }
fn field<'a>(record: &'a BTreeMap<String, Value>, name: &str) -> &'a Value {
record
.get(name)
.unwrap_or_else(|| panic!("missing `{name}`"))
}
#[test] #[test]
fn supported_values_round_trip() { fn primitive_unit_record_list_entry_and_json_values_round_trip() {
assert_eq!(().encode().unwrap(), Value::Unit);
assert_eq!(<()>::decode(Value::Unit).unwrap(), ());
assert_eq!(<()>::type_expr(), TypeExpr::Unit);
let request = Request { let request = Request {
title: "hello".into(), title: "hello".into(),
priority: None, priority: None,
related: vec![Entry { related: vec![Entry::new("/articles/1").unwrap()],
key: "/articles/1".into(),
value: true,
}],
body: Bytes(vec![1, 2, 3]), body: Bytes(vec![1, 2, 3]),
metadata: JsonValue::Object(vec![JsonMember { metadata: JsonValue::Object(vec![JsonMember {
name: "source".into(), name: "source".into(),
value: JsonValue::String("test".into()), value: JsonValue::Array(vec![JsonValue::String("test".into()), JsonValue::Null]),
}]), }]),
}; };
let encoded = request.clone().encode().unwrap(); let encoded = request.clone().encode().unwrap();
let Value::Record(fields) = &encoded else { let Value::Record(fields) = &encoded else {
panic!("record derive must encode a protocol record") panic!("record derive must encode a raw protocol record")
}; };
assert!(fields.iter().all(|field| field.name != "priority")); assert!(!fields.contains_key("priority"));
assert_eq!(
field(fields, "related"),
&Value::List(vec![Value::String("/articles/1".into())])
);
assert_eq!(
field(fields, "metadata"),
&Value::Record(BTreeMap::from([(
"source".into(),
Value::List(vec![Value::String("test".into()), Value::Unit]),
)]))
);
assert_eq!(Request::decode(encoded).unwrap(), request); assert_eq!(Request::decode(encoded).unwrap(), request);
let number = 42.5_f64; let number = 42.5_f64;
@@ -77,92 +96,136 @@ fn supported_values_round_trip() {
f32::decode(narrow_number.encode().unwrap()).unwrap(), f32::decode(narrow_number.encode().unwrap()).unwrap(),
narrow_number narrow_number
); );
for color in [Color::Red, Color::Blue] {
let encoded = color.clone().encode().unwrap();
let decoded = Color::decode(encoded).unwrap();
assert_eq!(decoded, color);
} }
#[test]
fn enums_are_strings_and_unions_are_discriminated_records() {
for color in [Color::Red, Color::Blue] {
let encoded = color.clone().encode().unwrap();
assert!(matches!(encoded, Value::String(_)));
assert_eq!(Color::decode(encoded).unwrap(), color);
}
let pending = PublishOutcome::Pending.encode().unwrap();
assert_eq!(
pending,
Value::Record(BTreeMap::from([(
UNION_CASE_FIELD.into(),
Value::String("Pending".into()),
)]))
);
let published = PublishOutcome::Published(Receipt { number: 7 })
.encode()
.unwrap();
let Value::Record(fields) = &published else {
panic!("union must encode as a record")
};
assert_eq!(
fields.get(UNION_CASE_FIELD),
Some(&Value::String("Published".into()))
);
assert!(matches!(
fields.get(UNION_VALUE_FIELD),
Some(Value::Record(_))
));
for outcome in [ for outcome in [
PublishOutcome::Pending,
PublishOutcome::Published(Receipt { number: 7 }), PublishOutcome::Published(Receipt { number: 7 }),
PublishOutcome::Rejected("policy".into()), PublishOutcome::Rejected("policy".into()),
] { ] {
let encoded = outcome.clone().encode().unwrap(); let encoded = outcome.clone().encode().unwrap();
let decoded = PublishOutcome::decode(encoded).unwrap(); assert_eq!(PublishOutcome::decode(encoded).unwrap(), outcome);
assert_eq!(decoded, outcome);
} }
} }
#[test] #[test]
fn derived_declarations_include_docs_optionality_and_dependencies() { fn derived_declarations_use_canonical_named_type_expressions() {
let mut declarations = Vec::new(); let mut declarations = Vec::new();
Request::collect_declarations(&mut declarations); Request::collect_declarations(&mut declarations);
Color::collect_declarations(&mut declarations); Color::collect_declarations(&mut declarations);
PublishOutcome::collect_declarations(&mut declarations); PublishOutcome::collect_declarations(&mut declarations);
let TypeDeclaration::Record(RecordDeclaration { let request = declarations
documentation, .iter()
fields, .find(|declaration| declaration.name == "Request")
.. .unwrap();
}) = &declarations[0] assert_eq!(
else { request.documentation.as_ref().unwrap().summary,
"A request record."
);
let TypeExpr::Record { fields } = &request.definition else {
panic!("Request must be a record declaration") panic!("Request must be a record declaration")
}; };
assert_eq!(documentation.as_ref().unwrap().summary, "A request record.");
assert_eq!( assert_eq!(
fields[0].documentation.as_ref().unwrap().summary, fields[0].documentation.as_ref().unwrap().summary,
"Required title." "Required title."
); );
assert!(!fields[0].optional); assert!(fields[0].required);
assert!(fields[1].optional); assert!(!fields[1].required);
assert_eq!(fields[1].ty, TypeExpr::Integer); assert_eq!(fields[1].r#type, TypeExpr::Integer);
assert_eq!( assert_eq!(
fields[2].ty, fields[2].r#type,
TypeExpr::List(Box::new(TypeExpr::Entry { TypeExpr::List {
key: Box::new(TypeExpr::String), items: Box::new(TypeExpr::Entry),
value: Box::new(TypeExpr::Boolean), }
}))
); );
let color = declarations let color = declarations
.iter() .iter()
.find(|declaration| declaration.name() == "Color") .find(|declaration| declaration.name == "Color")
.unwrap(); .unwrap();
let TypeDeclaration::Enum(EnumDeclaration { variants, .. }) = color else { let TypeExpr::Enum { cases } = &color.definition else {
panic!("Color must be an enum declaration") panic!("Color must be an enum declaration")
}; };
assert_eq!( assert_eq!(
variants[0].documentation.as_ref().unwrap().summary, cases[0].documentation.as_ref().unwrap().summary,
"Warm red." "Warm red."
); );
let outcome = declarations let outcome = declarations
.iter() .iter()
.find(|declaration| declaration.name() == "PublishOutcome") .find(|declaration| declaration.name == "PublishOutcome")
.unwrap(); .unwrap();
let TypeDeclaration::Union(UnionDeclaration { variants, .. }) = outcome else { let TypeExpr::Union { cases } = &outcome.definition else {
panic!("PublishOutcome must be a union declaration") panic!("PublishOutcome must be a union declaration")
}; };
assert_eq!(variants[0].ty, TypeExpr::Named("Receipt".into())); assert_eq!(cases[0].payload, None);
assert_eq!(
cases[1].payload,
Some(TypeExpr::Named {
name: "Receipt".into(),
})
);
assert!( assert!(
declarations declarations
.iter() .iter()
.any(|declaration| declaration.name() == "Receipt") .any(|declaration| declaration.name == "Receipt")
); );
} }
#[test] #[test]
fn codecs_reject_invalid_boundaries() { fn codecs_reject_invalid_boundaries_and_json_bytes() {
assert!(i64::MAX.encode().is_err()); assert!(i64::MAX.encode().is_err());
assert!(f64::NAN.encode().is_err()); assert!(f64::NAN.encode().is_err());
assert!(Request::decode(Value::Record(vec![])).is_err()); assert!(Request::decode(Value::Record(BTreeMap::new())).is_err());
assert!( assert!(
Request::decode(Value::Record(vec![NamedValue { Request::decode(Value::Record(BTreeMap::from([(
name: "unknown".into(), "unknown".into(),
value: Value::String("value".into()), Value::String("value".into()),
}])) )])))
.is_err()
);
assert!(Color::decode(Value::String("Green".into())).is_err());
assert!(Entry::new("articles/1").is_err());
assert!(Entry::decode(Value::String("/articles/../1".into())).is_err());
assert!(JsonValue::decode(Value::Bytes(vec![1])).is_err());
assert!(
JsonValue::decode(Value::Record(BTreeMap::from([(
"nested".into(),
Value::List(vec![Value::Bytes(vec![1])]),
)])))
.is_err() .is_err()
); );
assert!(Color::decode(Value::Enum("Green".into())).is_err());
} }
+47 -51
View File
@@ -1,4 +1,6 @@
use wip_interface::wip_protocol::{NamedValue, TypeDeclaration, TypeExpr, Value}; use std::collections::BTreeMap;
use wip_interface::wip_protocol::{INTERFACE_FORMAT_V1, TypeExpr, Value};
use wip_interface::{DispatchError, HostError, OperationResult, WipType, interface}; use wip_interface::{DispatchError, HostError, OperationResult, WipType, interface};
/// Publication request. /// Publication request.
@@ -36,7 +38,7 @@ pub trait Catalog {
note: Option<String>, note: Option<String>,
) -> OperationResult<PublishOutcome>; ) -> OperationResult<PublishOutcome>;
/// Checks connectivity without returning an application value. /// Checks connectivity without returning application data.
fn ping(&self) -> OperationResult<()>; fn ping(&self) -> OperationResult<()>;
/// Demonstrates a host/runtime failure. /// Demonstrates a host/runtime failure.
@@ -88,36 +90,29 @@ impl Catalog for RejectingCatalog {
} }
} }
fn named(name: &str, value: Value) -> NamedValue { fn publish_arguments(with_note: bool) -> BTreeMap<String, Value> {
NamedValue { let mut arguments = BTreeMap::from([(
name: name.into(), "request".into(),
value,
}
}
fn publish_arguments(with_note: bool) -> Vec<NamedValue> {
let mut arguments = vec![named(
"request",
PublishRequest { PublishRequest {
title: "example".into(), title: "example".into(),
} }
.encode() .encode()
.unwrap(), .unwrap(),
)]; )]);
if with_note { if with_note {
arguments.push(named("note", Value::String("audit".into()))); arguments.insert("note".into(), Value::String("audit".into()));
} }
arguments arguments
} }
#[test] #[test]
fn descriptor_is_generated_only_from_the_trait_declaration() { fn descriptor_is_canonical_and_generated_only_from_the_trait_declaration() {
let first = CatalogDefinition::descriptor(); let first = CatalogDefinition::descriptor();
let second = CatalogDefinition::descriptor(); let second = CatalogDefinition::descriptor();
assert_eq!(first, second); assert_eq!(first, second);
first.validate().unwrap(); first.validate().unwrap();
assert_eq!(first.name, "Catalog"); assert_eq!(first.format, INTERFACE_FORMAT_V1);
assert_eq!( assert_eq!(
first.documentation.as_ref().unwrap().summary, first.documentation.as_ref().unwrap().summary,
"Catalog editing capability." "Catalog editing capability."
@@ -139,81 +134,82 @@ fn descriptor_is_generated_only_from_the_trait_declaration() {
.summary, .summary,
"Item to publish." "Item to publish."
); );
assert!(!publish.parameters[0].optional); assert!(publish.parameters[0].required);
assert!(publish.parameters[1].optional); assert!(!publish.parameters[1].required);
assert_eq!( assert_eq!(
publish publish.returns.documentation.as_ref().unwrap().summary,
.returns
.as_ref()
.unwrap()
.documentation
.as_ref()
.unwrap()
.summary,
"The application publication outcome." "The application publication outcome."
); );
assert_eq!( assert_eq!(
publish.returns.as_ref().unwrap().ty, publish.returns.r#type,
TypeExpr::Named("PublishOutcome".into()) TypeExpr::Named {
name: "PublishOutcome".into(),
}
); );
assert!(first.operations[1].returns.is_none()); assert_eq!(first.operations[1].returns.r#type, TypeExpr::Unit);
assert!( assert!(
first first
.declarations .types
.iter() .iter()
.any(|declaration| matches!(declaration, TypeDeclaration::Record(_))) .any(|declaration| matches!(declaration.definition, TypeExpr::Record { .. }))
); );
assert!( assert!(
first first
.declarations .types
.iter() .iter()
.any(|declaration| matches!(declaration, TypeDeclaration::Union(_))) .any(|declaration| matches!(declaration.definition, TypeExpr::Union { .. }))
); );
// Both implementation types satisfy the same generated dispatch boundary; // Both implementation types satisfy one immutable generated definition;
// neither can affect the descriptor shape. // implementation selection cannot contribute fields to the descriptor.
fn accepts_definition<T: Catalog>(_: &T) {} fn accepts_definition<T: Catalog>(_: &T) {}
accepts_definition(&AcceptingCatalog); accepts_definition(&AcceptingCatalog);
accepts_definition(&RejectingCatalog); accepts_definition(&RejectingCatalog);
assert_eq!(CatalogDefinition::descriptor(), first);
} }
#[test] #[test]
fn generated_argument_decoder_and_result_encoder_round_trip() { fn generated_argument_decoder_and_result_encoder_use_raw_values() {
let decoded = CatalogPublishArguments::decode(publish_arguments(false)).unwrap(); let decoded = CatalogPublishArguments::decode(publish_arguments(false)).unwrap();
assert_eq!(decoded.request.title, "example"); assert_eq!(decoded.request.title, "example");
assert_eq!(decoded.note, None); assert_eq!(decoded.note, None);
let encoded = CatalogPublishArguments::encode_result(PublishOutcome::Rejected("domain".into())) let encoded =
.unwrap() CatalogPublishArguments::encode_result(PublishOutcome::Rejected("domain".into())).unwrap();
.unwrap();
assert_eq!( assert_eq!(
PublishOutcome::decode(encoded).unwrap(), PublishOutcome::decode(encoded).unwrap(),
PublishOutcome::Rejected("domain".into()) PublishOutcome::Rejected("domain".into())
); );
assert_eq!(
CatalogPingArguments::encode_result(()).unwrap(),
Value::Unit
);
} }
#[test] #[test]
fn typed_dispatch_validates_decodes_invokes_encodes_and_validates() { fn typed_dispatch_validates_decodes_invokes_encodes_and_validates() {
let value = CatalogDefinition::dispatch(&AcceptingCatalog, "publish", publish_arguments(true)) let value =
.unwrap() CatalogDefinition::dispatch(&AcceptingCatalog, "publish", publish_arguments(true)).unwrap();
.unwrap();
assert_eq!( assert_eq!(
PublishOutcome::decode(value).unwrap(), PublishOutcome::decode(value).unwrap(),
PublishOutcome::Published(true) PublishOutcome::Published(true)
); );
assert_eq!( assert_eq!(
CatalogDefinition::dispatch(&AcceptingCatalog, "ping", vec![]).unwrap(), CatalogDefinition::dispatch(&AcceptingCatalog, "ping", BTreeMap::new()).unwrap(),
None Value::Unit
); );
let invalid = CatalogDefinition::dispatch(&AcceptingCatalog, "publish", vec![]).unwrap_err(); let invalid =
CatalogDefinition::dispatch(&AcceptingCatalog, "publish", BTreeMap::new()).unwrap_err();
assert!(matches!(invalid, DispatchError::InvalidArguments(_))); assert!(matches!(invalid, DispatchError::InvalidArguments(_)));
let host = CatalogDefinition::dispatch(&AcceptingCatalog, "host_value", vec![]).unwrap_err(); let host =
CatalogDefinition::dispatch(&AcceptingCatalog, "host_value", BTreeMap::new()).unwrap_err();
assert!(matches!(host, DispatchError::Host(error) if error.message() == "backend unavailable")); assert!(matches!(host, DispatchError::Host(error) if error.message() == "backend unavailable"));
let unknown = CatalogDefinition::dispatch(&AcceptingCatalog, "missing", vec![]).unwrap_err(); let unknown =
CatalogDefinition::dispatch(&AcceptingCatalog, "missing", BTreeMap::new()).unwrap_err();
assert!(matches!(unknown, DispatchError::UnknownOperation(name) if name == "missing")); assert!(matches!(unknown, DispatchError::UnknownOperation(name) if name == "missing"));
} }
@@ -245,11 +241,11 @@ fn distinct_operation_names_always_generate_distinct_public_items() {
assert_eq!(descriptor.operations[0].name, "item"); assert_eq!(descriptor.operations[0].name, "item");
assert_eq!(descriptor.operations[1].name, "item_"); assert_eq!(descriptor.operations[1].name, "item_");
let first = DistinctOperationsDefinition::dispatch(&DistinctImplementation, "item", vec![]) let first =
.unwrap() DistinctOperationsDefinition::dispatch(&DistinctImplementation, "item", BTreeMap::new())
.unwrap(); .unwrap();
let second = DistinctOperationsDefinition::dispatch(&DistinctImplementation, "item_", vec![]) let second =
.unwrap() DistinctOperationsDefinition::dispatch(&DistinctImplementation, "item_", BTreeMap::new())
.unwrap(); .unwrap();
assert_eq!(first, Value::String("item".into())); assert_eq!(first, Value::String("item".into()));
assert_eq!(second, Value::String("item_".into())); assert_eq!(second, Value::String("item_".into()));
+2 -2
View File
@@ -1,9 +1,9 @@
use wip_interface::WipType; use wip_interface::WipType;
#[derive(WipType)] #[derive(WipType)]
enum MixedEnum { enum UnsupportedEnum {
Empty, Empty,
Payload(String), Payload { value: String },
} }
fn main() {} fn main() {}
@@ -1,5 +1,5 @@
error: a `WipType` enum must contain either only unit variants or only single-field tuple variants; mixed and named-field variants are unsupported error: a `WipType` enum must contain unit variants and/or single-field tuple variants; named-field and multi-field variants are unsupported
--> tests/ui/mixed_enum.rs:4:1 --> tests/ui/mixed_enum.rs:6:5
| |
4 | enum MixedEnum { 6 | Payload { value: String },
| ^^^^ | ^^^^^^^
@@ -4,16 +4,11 @@ error[E0277]: the trait bound `(std::string::String, std::string::String): WipTy
5 | fn load(&self, pair: (String, String)) -> OperationResult<String>; 5 | fn load(&self, pair: (String, String)) -> OperationResult<String>;
| ^^^^^^^^^^^^^^^^ the trait `WipType` is not implemented for `(std::string::String, std::string::String)` | ^^^^^^^^^^^^^^^^ the trait `WipType` is not implemented for `(std::string::String, std::string::String)`
| |
= help: the following other types implement trait `WipType`: help: the trait `WipType` is implemented for `()`
JsonValue --> src/lib.rs
Vec<T> |
bool | impl WipType for () {
f32 | ^^^^^^^^^^^^^^^^^^^
f64
i128
i16
i32
and $N others
error[E0277]: the trait bound `(std::string::String, std::string::String): WipType` is not satisfied error[E0277]: the trait bound `(std::string::String, std::string::String): WipType` is not satisfied
--> tests/ui/unsupported_type.rs:5:26 --> tests/ui/unsupported_type.rs:5:26
@@ -24,16 +19,11 @@ error[E0277]: the trait bound `(std::string::String, std::string::String): WipTy
5 | fn load(&self, pair: (String, String)) -> OperationResult<String>; 5 | fn load(&self, pair: (String, String)) -> OperationResult<String>;
| ^^^^^^^^^^^^^^^^ the trait `WipType` is not implemented for `(std::string::String, std::string::String)` | ^^^^^^^^^^^^^^^^ the trait `WipType` is not implemented for `(std::string::String, std::string::String)`
| |
= help: the following other types implement trait `WipType`: help: the trait `WipType` is implemented for `()`
JsonValue --> src/lib.rs
Vec<T> |
bool | impl WipType for () {
f32 | ^^^^^^^^^^^^^^^^^^^
f64
i128
i16
i32
and $N others
note: required by a bound in `wip_interface::__private::RecordDecoder::required` note: required by a bound in `wip_interface::__private::RecordDecoder::required`
--> src/lib.rs --> src/lib.rs
| |
+63 -73
View File
@@ -22,18 +22,17 @@
#![deny(missing_docs)] #![deny(missing_docs)]
use std::collections::BTreeMap;
use std::error::Error; use std::error::Error;
use std::io; use std::io;
use wip_host::{ use wip_host::{
CallErrorKind, CallRequest, InterfaceRef, InterfaceRegistry, InterfaceTarget, PublishedObject, CallErrorKind, CallRequest, InterfaceRef, InterfaceRegistry, InterfaceTarget, ObjectId,
Validator, WipObject, PublishedObject, Validator, WipObject,
}; };
use wip_interface as wip; use wip_interface as wip;
use wip_interface::WipType; use wip_interface::WipType;
use wip_protocol::{ use wip_protocol::ProtocolError;
InterfaceObservation, NamedValue, ObjectId, OperationOutcome, OperationResult, OperationTarget,
};
const OBJECT_ID: &str = "article-service"; const OBJECT_ID: &str = "article-service";
const OBJECT_VALIDATOR: &[u8] = b"object-v1"; const OBJECT_VALIDATOR: &[u8] = b"object-v1";
@@ -114,10 +113,10 @@ pub trait Diagnostics {
pub struct DemoReport { pub struct DemoReport {
/// Opaque interface references published by the object, in exposure order. /// Opaque interface references published by the object, in exposure order.
pub published_interfaces: Vec<String>, pub published_interfaces: Vec<String>,
/// Descriptor identity observed for the catalog interface. /// Descriptor format observed for the catalog interface.
pub catalog_descriptor: String, pub catalog_descriptor_format: String,
/// Descriptor identity observed for the audit interface. /// Descriptor format observed for the audit interface.
pub audit_descriptor: String, pub audit_descriptor_format: String,
/// Typed successful application result decoded from a validated `Value`. /// Typed successful application result decoded from a validated `Value`.
pub published: PublishOutcome, pub published: PublishOutcome,
/// Typed application-domain rejection decoded from a validated `Value`. /// Typed application-domain rejection decoded from a validated `Value`.
@@ -128,8 +127,8 @@ pub struct DemoReport {
pub audit_status: String, pub audit_status: String,
/// Stable failure kind produced by the deliberate host/backend failure. /// Stable failure kind produced by the deliberate host/backend failure.
pub host_failure: CallErrorKind, pub host_failure: CallErrorKind,
/// A protocol-level operation result built from the host failure. /// A protocol-level failure built from the host failure.
pub protocol_failure: OperationResult, pub protocol_failure: ProtocolError,
} }
/// Runs the complete end-to-end example. /// Runs the complete end-to-end example.
@@ -179,14 +178,8 @@ pub fn run() -> Result<DemoReport, Box<dyn Error>> {
.observe(&audit_reference) .observe(&audit_reference)
.ok_or_else(|| io::Error::other("registered audit descriptor was not observable"))?; .ok_or_else(|| io::Error::other("registered audit descriptor was not observable"))?;
InterfaceObservation { catalog_observation.descriptor.validate()?;
object: object.clone(), audit_observation.descriptor.validate()?;
descriptors: vec![
catalog_observation.descriptor.clone(),
audit_observation.descriptor.clone(),
],
}
.validate()?;
let published = dispatch_publish( let published = dispatch_publish(
&publication, &publication,
@@ -211,13 +204,18 @@ pub fn run() -> Result<DemoReport, Box<dyn Error>> {
catalog_observation.reference.clone(), catalog_observation.reference.clone(),
Some(Validator::new(CATALOG_VALIDATOR)), Some(Validator::new(CATALOG_VALIDATOR)),
"status", "status",
vec![], BTreeMap::new(),
), ),
"catalog status", "catalog status",
)?; )?;
let audit_status = dispatch_string( let audit_status = dispatch_string(
&publication, &publication,
request(audit_observation.reference.clone(), None, "status", vec![]), request(
audit_observation.reference.clone(),
None,
"status",
BTreeMap::new(),
),
"audit status", "audit status",
)?; )?;
@@ -225,7 +223,7 @@ pub fn run() -> Result<DemoReport, Box<dyn Error>> {
catalog_observation.reference.clone(), catalog_observation.reference.clone(),
Some(Validator::new(CATALOG_VALIDATOR)), Some(Validator::new(CATALOG_VALIDATOR)),
"backend_status", "backend_status",
vec![], BTreeMap::new(),
)) { )) {
Ok(_) => { Ok(_) => {
return Err(io::Error::other( return Err(io::Error::other(
@@ -242,20 +240,12 @@ pub fn run() -> Result<DemoReport, Box<dyn Error>> {
)) ))
.into()); .into());
} }
let protocol_error = host_error.protocol_error(); let protocol_failure = host_error.protocol_error();
let protocol_failure = OperationResult {
target: OperationTarget {
object: ObjectId(OBJECT_ID.into()),
interface: catalog_observation.reference.as_str().into(),
operation: "backend_status".into(),
},
outcome: OperationOutcome::Failed(protocol_error),
};
Ok(DemoReport { Ok(DemoReport {
published_interfaces: object.interfaces, published_interfaces: object.interfaces,
catalog_descriptor: catalog_observation.descriptor.name, catalog_descriptor_format: catalog_observation.descriptor.format,
audit_descriptor: audit_observation.descriptor.name, audit_descriptor_format: audit_observation.descriptor.format,
published, published,
rejected, rejected,
catalog_status, catalog_status,
@@ -275,26 +265,19 @@ fn dispatch_publish(
catalog.clone(), catalog.clone(),
Some(Validator::new(CATALOG_VALIDATOR)), Some(Validator::new(CATALOG_VALIDATOR)),
"publish", "publish",
vec![NamedValue { BTreeMap::from([("request".into(), encoded_request)]),
name: "request".into(),
value: encoded_request,
}],
))?; ))?;
let value = result let value = result.value;
.value
.ok_or_else(|| io::Error::other("publish returned no application value"))?;
Ok(PublishOutcome::decode(value)?) Ok(PublishOutcome::decode(value)?)
} }
fn dispatch_string( fn dispatch_string(
publication: &PublishedObject<ApplicationService>, publication: &PublishedObject<ApplicationService>,
request: CallRequest, request: CallRequest,
context: &str, _context: &str,
) -> Result<String, Box<dyn Error>> { ) -> Result<String, Box<dyn Error>> {
let result = publication.dispatch(request)?; let result = publication.dispatch(request)?;
let value = result let value = result.value;
.value
.ok_or_else(|| io::Error::other(format!("{context} returned no value")))?;
Ok(String::decode(value)?) Ok(String::decode(value)?)
} }
@@ -302,7 +285,7 @@ fn request(
interface: InterfaceRef, interface: InterfaceRef,
interface_validator: Option<Validator>, interface_validator: Option<Validator>,
operation: &str, operation: &str,
arguments: Vec<NamedValue>, arguments: BTreeMap<String, wip_protocol::Value>,
) -> CallRequest { ) -> CallRequest {
CallRequest { CallRequest {
object: ObjectId(OBJECT_ID.into()), object: ObjectId(OBJECT_ID.into()),
@@ -383,7 +366,7 @@ mod tests {
use super::*; use super::*;
use wip_host::CallError; use wip_host::CallError;
use wip_interface::{CodecError, TypeDeclaration, TypeExpr}; use wip_interface::{CodecError, TypeDeclaration, TypeExpr};
use wip_protocol::{ProtocolErrorCategory, Value}; use wip_protocol::{ProtocolErrorCode, Value};
/// A deliberately inconsistent custom codec used to prove result validation. /// A deliberately inconsistent custom codec used to prove result validation.
pub struct InvalidValue; pub struct InvalidValue;
@@ -444,9 +427,9 @@ mod tests {
} }
} }
fn assert_failure(error: CallError, kind: CallErrorKind, category: ProtocolErrorCategory) { fn assert_failure(error: CallError, kind: CallErrorKind, code: ProtocolErrorCode) {
assert_eq!(error.kind(), kind); assert_eq!(error.kind(), kind);
assert_eq!(error.protocol_error().category, category); assert_eq!(error.protocol_error().code, code);
} }
#[test] #[test]
@@ -459,14 +442,14 @@ mod tests {
fixture.catalog.clone(), fixture.catalog.clone(),
Some(Validator::new(CATALOG_VALIDATOR)), Some(Validator::new(CATALOG_VALIDATOR)),
"status", "status",
vec![], BTreeMap::new(),
), ),
"catalog status", "catalog status",
) )
.unwrap(); .unwrap();
let audit = dispatch_string( let audit = dispatch_string(
&fixture.publication, &fixture.publication,
request(fixture.audit.clone(), None, "status", vec![]), request(fixture.audit.clone(), None, "status", BTreeMap::new()),
"audit status", "audit status",
) )
.unwrap(); .unwrap();
@@ -475,12 +458,17 @@ mod tests {
let not_exposed = fixture let not_exposed = fixture
.publication .publication
.dispatch(request(fixture.diagnostics, None, "diagnostics", vec![])) .dispatch(request(
fixture.diagnostics,
None,
"diagnostics",
BTreeMap::new(),
))
.unwrap_err(); .unwrap_err();
assert_failure( assert_failure(
not_exposed, not_exposed,
CallErrorKind::InterfaceMismatch, CallErrorKind::InterfaceMismatch,
ProtocolErrorCategory::InterfaceNotFound, ProtocolErrorCode::InterfaceMismatch,
); );
} }
@@ -492,20 +480,20 @@ mod tests {
fixture.catalog.clone(), fixture.catalog.clone(),
Some(Validator::new(CATALOG_VALIDATOR)), Some(Validator::new(CATALOG_VALIDATOR)),
"status", "status",
vec![], BTreeMap::new(),
); );
wrong_object.object = ObjectId("other-object".into()); wrong_object.object = ObjectId("other-object".into());
assert_failure( assert_failure(
fixture.publication.dispatch(wrong_object).unwrap_err(), fixture.publication.dispatch(wrong_object).unwrap_err(),
CallErrorKind::ObjectNotFound, CallErrorKind::ObjectNotFound,
ProtocolErrorCategory::ObjectNotFound, ProtocolErrorCode::NotFound,
); );
let mut missing_object_validator = request( let mut missing_object_validator = request(
fixture.catalog.clone(), fixture.catalog.clone(),
Some(Validator::new(CATALOG_VALIDATOR)), Some(Validator::new(CATALOG_VALIDATOR)),
"status", "status",
vec![], BTreeMap::new(),
); );
missing_object_validator.object_validator = None; missing_object_validator.object_validator = None;
assert_failure( assert_failure(
@@ -514,14 +502,14 @@ mod tests {
.dispatch(missing_object_validator) .dispatch(missing_object_validator)
.unwrap_err(), .unwrap_err(),
CallErrorKind::ValidatorRequired, CallErrorKind::ValidatorRequired,
ProtocolErrorCategory::InvalidRequest, ProtocolErrorCode::InvalidRequest,
); );
let mut stale_object_validator = request( let mut stale_object_validator = request(
fixture.catalog.clone(), fixture.catalog.clone(),
Some(Validator::new(CATALOG_VALIDATOR)), Some(Validator::new(CATALOG_VALIDATOR)),
"status", "status",
vec![], BTreeMap::new(),
); );
stale_object_validator.object_validator = Some(Validator::new(b"stale-object")); stale_object_validator.object_validator = Some(Validator::new(b"stale-object"));
assert_failure( assert_failure(
@@ -530,16 +518,21 @@ mod tests {
.dispatch(stale_object_validator) .dispatch(stale_object_validator)
.unwrap_err(), .unwrap_err(),
CallErrorKind::ValidatorMismatch, CallErrorKind::ValidatorMismatch,
ProtocolErrorCategory::InvalidRequest, ProtocolErrorCode::InvalidRequest,
); );
assert_failure( assert_failure(
fixture fixture
.publication .publication
.dispatch(request(fixture.catalog.clone(), None, "status", vec![])) .dispatch(request(
fixture.catalog.clone(),
None,
"status",
BTreeMap::new(),
))
.unwrap_err(), .unwrap_err(),
CallErrorKind::InterfaceValidatorRequired, CallErrorKind::InterfaceValidatorRequired,
ProtocolErrorCategory::InvalidRequest, ProtocolErrorCode::InvalidRequest,
); );
assert_failure( assert_failure(
@@ -549,11 +542,11 @@ mod tests {
fixture.catalog.clone(), fixture.catalog.clone(),
Some(Validator::new(b"stale-interface")), Some(Validator::new(b"stale-interface")),
"status", "status",
vec![], BTreeMap::new(),
)) ))
.unwrap_err(), .unwrap_err(),
CallErrorKind::InterfaceValidatorMismatch, CallErrorKind::InterfaceValidatorMismatch,
ProtocolErrorCategory::InvalidRequest, ProtocolErrorCode::InvalidRequest,
); );
assert_failure( assert_failure(
@@ -563,11 +556,11 @@ mod tests {
fixture.catalog.clone(), fixture.catalog.clone(),
Some(Validator::new(CATALOG_VALIDATOR)), Some(Validator::new(CATALOG_VALIDATOR)),
"missing", "missing",
vec![], BTreeMap::new(),
)) ))
.unwrap_err(), .unwrap_err(),
CallErrorKind::OperationNotFound, CallErrorKind::OperationNotFound,
ProtocolErrorCategory::OperationNotFound, ProtocolErrorCode::OperationNotFound,
); );
assert_failure( assert_failure(
@@ -577,14 +570,11 @@ mod tests {
fixture.catalog.clone(), fixture.catalog.clone(),
Some(Validator::new(CATALOG_VALIDATOR)), Some(Validator::new(CATALOG_VALIDATOR)),
"publish", "publish",
vec![NamedValue { BTreeMap::from([("request".into(), Value::String("not a record".into()))]),
name: "request".into(),
value: Value::String("not a record".into()),
}],
)) ))
.unwrap_err(), .unwrap_err(),
CallErrorKind::InvalidArguments, CallErrorKind::InvalidArguments,
ProtocolErrorCategory::InvalidArguments, ProtocolErrorCode::InvalidArguments,
); );
assert_failure( assert_failure(
@@ -594,11 +584,11 @@ mod tests {
fixture.catalog, fixture.catalog,
Some(Validator::new(CATALOG_VALIDATOR)), Some(Validator::new(CATALOG_VALIDATOR)),
"backend_status", "backend_status",
vec![], BTreeMap::new(),
)) ))
.unwrap_err(), .unwrap_err(),
CallErrorKind::Implementation, CallErrorKind::Implementation,
ProtocolErrorCategory::Internal, ProtocolErrorCode::Internal,
); );
} }
@@ -615,13 +605,13 @@ mod tests {
broken.reference().clone(), broken.reference().clone(),
None, None,
"broken_result", "broken_result",
vec![], BTreeMap::new(),
)) ))
.unwrap_err(); .unwrap_err();
assert_failure( assert_failure(
error, error,
CallErrorKind::InvalidResult, CallErrorKind::InvalidResult,
ProtocolErrorCategory::Internal, ProtocolErrorCode::Internal,
); );
} }
} }
+4 -7
View File
@@ -1,5 +1,5 @@
use wip_host::CallErrorKind; use wip_host::CallErrorKind;
use wip_protocol::{OperationOutcome, ProtocolErrorCategory}; use wip_protocol::{INTERFACE_FORMAT_V1, ProtocolErrorCode};
use wip_public_api_example::{PublishOutcome, run}; use wip_public_api_example::{PublishOutcome, run};
#[test] #[test]
@@ -7,8 +7,8 @@ fn public_crates_support_the_complete_end_to_end_scenario() {
let report = run().unwrap(); let report = run().unwrap();
assert_eq!(report.published_interfaces.len(), 2); assert_eq!(report.published_interfaces.len(), 2);
assert_eq!(report.catalog_descriptor, report.published_interfaces[0]); assert_eq!(report.catalog_descriptor_format, INTERFACE_FORMAT_V1);
assert_eq!(report.audit_descriptor, report.published_interfaces[1]); assert_eq!(report.audit_descriptor_format, INTERFACE_FORMAT_V1);
assert_eq!( assert_eq!(
report.published, report.published,
PublishOutcome::Published("article-1".into()) PublishOutcome::Published("article-1".into())
@@ -20,8 +20,5 @@ fn public_crates_support_the_complete_end_to_end_scenario() {
assert_eq!(report.catalog_status, "catalog-ready"); assert_eq!(report.catalog_status, "catalog-ready");
assert_eq!(report.audit_status, "audit-ready"); assert_eq!(report.audit_status, "audit-ready");
assert_eq!(report.host_failure, CallErrorKind::Implementation); assert_eq!(report.host_failure, CallErrorKind::Implementation);
assert!(matches!( assert_eq!(report.protocol_failure.code, ProtocolErrorCode::Internal);
report.protocol_failure.outcome,
OperationOutcome::Failed(error) if error.category == ProtocolErrorCategory::Internal
));
} }