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
+34 -32
View File
@@ -31,7 +31,7 @@
#![deny(missing_docs)]
use std::any::TypeId;
use std::collections::{HashMap, HashSet};
use std::collections::{BTreeMap, HashMap, HashSet};
use std::error::Error;
use std::fmt::{self, Display, Formatter};
use std::marker::PhantomData;
@@ -42,8 +42,7 @@ use wip_interface::DispatchError;
pub use wip_interface::{InterfaceDefinition, InterfaceImplementation};
pub use wip_protocol;
use wip_protocol::{
InterfaceDescriptor, NamedValue, Object, ObjectId, OperationCall, OperationTarget,
ProtocolError, ProtocolErrorCategory, ValidationError, Value,
InterfaceDescriptor, Object, ProtocolError, ProtocolErrorCode, ValidationError, Value,
};
/// 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.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct InterfaceRef(String);
@@ -161,8 +168,8 @@ impl InterfaceRegistry {
/// `validator` should be present when the host intends the reference to
/// represent a versioned descriptor. Its presence makes the same value a
/// mandatory precondition for dispatch. Registration finalizes the
/// descriptor's protocol `name` to the assigned opaque reference so it
/// corresponds directly to [`Object::interfaces`].
/// descriptor remains independent of both registration and its assigned
/// opaque reference.
pub fn register<D>(
&mut self,
_definition: D,
@@ -176,7 +183,7 @@ impl InterfaceRegistry {
return Err(RegistrationError::AlreadyRegistered);
}
let mut descriptor = D::descriptor();
let descriptor = D::descriptor();
descriptor
.validate()
.map_err(RegistrationError::InvalidDescriptor)?;
@@ -185,11 +192,6 @@ impl InterfaceRegistry {
"wip-interface:{}:{}",
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
.next_reference
.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> {
artifact: Arc<InterfaceArtifact>,
@@ -350,13 +352,20 @@ impl<T: WipObject> PublishedObject<T> {
/// Builds the protocol object metadata with interface insertion order intact.
#[must_use]
pub fn protocol_object(&self) -> Object {
let id = self.implementation.id();
let object = Object {
id: self.implementation.id(),
name: id.0.clone(),
description: None,
interfaces: self
.interfaces
.iter()
.map(|bound| bound.artifact.reference.0.clone())
.collect(),
r#ref: Some(id.0),
validator: self
.implementation
.validator()
.map(|validator| validator.0.clone()),
};
debug_assert!(object.validate().is_ok());
object
@@ -438,14 +447,7 @@ impl<T: WipObject> PublishedObject<T> {
bound
.artifact
.descriptor
.validate_call(&OperationCall {
target: OperationTarget {
object: request.object.clone(),
interface: bound.artifact.descriptor.name.clone(),
operation: request.operation.clone(),
},
arguments: request.arguments.clone(),
})
.validate_arguments(&request.operation, &request.arguments)
.map_err(|error| CallError::new(CallErrorKind::InvalidArguments, error.to_string()))?;
let value = (bound.dispatch)(&self.implementation, &request.operation, request.arguments)
@@ -454,7 +456,7 @@ impl<T: WipObject> PublishedObject<T> {
bound
.artifact
.descriptor
.validate_return(&request.operation, value.as_ref())
.validate_result(&request.operation, &value)
.map_err(|error| CallError::new(CallErrorKind::InvalidResult, error.to_string()))?;
Ok(CallResult {
@@ -512,14 +514,14 @@ pub struct CallRequest {
/// Operation name within the selected interface descriptor.
pub operation: String,
/// Named protocol arguments.
pub arguments: Vec<NamedValue>,
pub arguments: BTreeMap<String, Value>,
}
/// Successful host dispatch result.
#[derive(Debug, Clone, PartialEq)]
pub struct CallResult {
/// Encoded application value, or `None` for a no-return operation.
pub value: Option<Value>,
/// Encoded application result. No-data operations return [`Value::Unit`].
pub value: Value,
/// Object validator observed after the implementation returned.
pub validator: Option<Validator>,
}
@@ -610,20 +612,20 @@ impl CallError {
#[must_use]
pub fn protocol_error(&self) -> ProtocolError {
let category = match self.kind {
CallErrorKind::ObjectNotFound => ProtocolErrorCategory::ObjectNotFound,
CallErrorKind::InterfaceMismatch => ProtocolErrorCategory::InterfaceNotFound,
CallErrorKind::OperationNotFound => ProtocolErrorCategory::OperationNotFound,
CallErrorKind::InvalidArguments => ProtocolErrorCategory::InvalidArguments,
CallErrorKind::ObjectNotFound => ProtocolErrorCode::NotFound,
CallErrorKind::InterfaceMismatch => ProtocolErrorCode::InterfaceMismatch,
CallErrorKind::OperationNotFound => ProtocolErrorCode::OperationNotFound,
CallErrorKind::InvalidArguments => ProtocolErrorCode::InvalidArguments,
CallErrorKind::ValidatorRequired
| CallErrorKind::ValidatorMismatch
| CallErrorKind::InterfaceValidatorRequired
| CallErrorKind::InterfaceValidatorMismatch => ProtocolErrorCategory::InvalidRequest,
| CallErrorKind::InterfaceValidatorMismatch => ProtocolErrorCode::InvalidRequest,
CallErrorKind::InvalidResult | CallErrorKind::Implementation => {
ProtocolErrorCategory::Internal
ProtocolErrorCode::Internal
}
};
ProtocolError {
category,
code: category,
message: self.message.clone(),
}
}
+45 -50
View File
@@ -1,14 +1,14 @@
use std::cell::{Cell, RefCell};
use std::collections::BTreeMap;
use wip_host::{
CallErrorKind, CallRequest, InterfaceRegistry, InterfaceTarget, PublishedObject, Validator,
WipObject,
CallErrorKind, CallRequest, InterfaceRegistry, InterfaceTarget, ObjectId, PublishedObject,
Validator, WipObject,
};
use wip_interface::{
CodecError, HostError, NamedValue, OperationResult, TypeDeclaration, TypeExpr, Value, WipType,
interface,
CodecError, HostError, OperationResult, TypeDeclaration, TypeExpr, Value, WipType, interface,
};
use wip_protocol::{ObjectId, ProtocolErrorCategory};
use wip_protocol::ProtocolErrorCode;
#[interface]
pub trait Reader {
@@ -143,7 +143,7 @@ fn request(
reference: &wip_host::InterfaceRef,
interface_validator: Option<&[u8]>,
operation: &str,
arguments: Vec<NamedValue>,
arguments: BTreeMap<String, Value>,
) -> CallRequest {
CallRequest {
object: ObjectId(object.into()),
@@ -170,7 +170,7 @@ fn registry_exposes_opaque_validated_descriptor_observations() {
assert!(registry.resolve("wip-interface:unknown:0").is_none());
let observation = registry.observe(reader.reference()).unwrap();
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);
observation.descriptor.validate().unwrap();
@@ -207,15 +207,18 @@ fn publication_is_explicit_ordered_qualified_and_request_scoped() {
reader.reference().as_str().to_owned()
]
);
wip_protocol::InterfaceObservation {
object: object.clone(),
descriptors: vec![
registry.observe(writer.reference()).unwrap().descriptor,
registry.observe(reader.reference()).unwrap().descriptor,
],
}
.validate()
.unwrap();
registry
.observe(writer.reference())
.unwrap()
.descriptor
.validate()
.unwrap();
registry
.observe(reader.reference())
.unwrap()
.descriptor
.validate()
.unwrap();
assert!(!format!("{object:?}").contains("private/backend/path"));
let writer_result = publication
@@ -225,7 +228,7 @@ fn publication_is_explicit_ordered_qualified_and_request_scoped() {
writer.reference(),
None,
"label",
vec![],
BTreeMap::new(),
))
.unwrap();
let reader_result = publication
@@ -235,11 +238,11 @@ fn publication_is_explicit_ordered_qualified_and_request_scoped() {
reader.reference(),
None,
"label",
vec![],
BTreeMap::new(),
))
.unwrap();
assert_eq!(writer_result.value, Some(Value::String("writer".into())));
assert_eq!(reader_result.value, Some(Value::String("reader".into())));
assert_eq!(writer_result.value, Value::String("writer".into()));
assert_eq!(reader_result.value, Value::String("reader".into()));
assert_eq!(publication.implementation().calls.get(), 2);
assert_eq!(
publication
@@ -267,13 +270,13 @@ fn implemented_but_unexposed_interface_is_not_a_member() {
hidden.reference(),
None,
"hidden",
vec![],
BTreeMap::new(),
))
.unwrap_err();
assert_eq!(error.kind(), CallErrorKind::InterfaceMismatch);
assert_eq!(
error.protocol_error().category,
ProtocolErrorCategory::InterfaceNotFound
error.protocol_error().code,
ProtocolErrorCode::InterfaceMismatch
);
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() {
let publication = PublishedObject::new(Document::new("empty"));
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());
object.validate().unwrap();
}
@@ -316,13 +320,13 @@ fn operation_and_argument_failures_are_mapped_without_execution() {
reader.reference(),
None,
"missing",
vec![],
BTreeMap::new(),
))
.unwrap_err();
assert_eq!(missing.kind(), CallErrorKind::OperationNotFound);
assert_eq!(
missing.protocol_error().category,
ProtocolErrorCategory::OperationNotFound
missing.protocol_error().code,
ProtocolErrorCode::OperationNotFound
);
let invalid = publication
@@ -332,16 +336,13 @@ fn operation_and_argument_failures_are_mapped_without_execution() {
reader.reference(),
None,
"add",
vec![NamedValue {
name: "left".into(),
value: Value::String("wrong".into()),
}],
BTreeMap::from([("left".into(), Value::String("wrong".into()))]),
))
.unwrap_err();
assert_eq!(invalid.kind(), CallErrorKind::InvalidArguments);
assert_eq!(
invalid.protocol_error().category,
ProtocolErrorCategory::InvalidArguments
invalid.protocol_error().code,
ProtocolErrorCode::InvalidArguments
);
assert_eq!(publication.implementation().calls.get(), 0);
}
@@ -361,14 +362,11 @@ fn invalid_implementation_result_is_internal_and_not_returned() {
broken.reference(),
None,
"broken",
vec![],
BTreeMap::new(),
))
.unwrap_err();
assert_eq!(error.kind(), CallErrorKind::InvalidResult);
assert_eq!(
error.protocol_error().category,
ProtocolErrorCategory::Internal
);
assert_eq!(error.protocol_error().code, ProtocolErrorCode::Internal);
assert_eq!(publication.implementation().calls.get(), 1);
}
@@ -387,7 +385,7 @@ fn object_validator_missing_and_mismatch_are_distinct_and_prevent_execution() {
reader.reference(),
None,
"label",
vec![],
BTreeMap::new(),
))
.unwrap_err();
assert_eq!(missing.kind(), CallErrorKind::ValidatorRequired);
@@ -399,7 +397,7 @@ fn object_validator_missing_and_mismatch_are_distinct_and_prevent_execution() {
reader.reference(),
None,
"label",
vec![],
BTreeMap::new(),
))
.unwrap_err();
assert_eq!(mismatch.kind(), CallErrorKind::ValidatorMismatch);
@@ -412,7 +410,7 @@ fn object_validator_missing_and_mismatch_are_distinct_and_prevent_execution() {
reader.reference(),
None,
"label",
vec![],
BTreeMap::new(),
))
.unwrap();
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(),
None,
"label",
vec![],
BTreeMap::new(),
))
.unwrap_err();
assert_eq!(missing.kind(), CallErrorKind::InterfaceValidatorRequired);
@@ -451,7 +449,7 @@ fn interface_validator_missing_and_mismatch_are_distinct_and_prevent_execution()
writer.reference(),
Some(b"schema-v0"),
"label",
vec![],
BTreeMap::new(),
))
.unwrap_err();
assert_eq!(mismatch.kind(), CallErrorKind::InterfaceValidatorMismatch);
@@ -464,10 +462,10 @@ fn interface_validator_missing_and_mismatch_are_distinct_and_prevent_execution()
writer.reference(),
Some(b"schema-v1"),
"label",
vec![],
BTreeMap::new(),
))
.unwrap();
assert_eq!(result.value, Some(Value::String("writer".into())));
assert_eq!(result.value, Value::String("writer".into()));
}
#[test]
@@ -496,12 +494,9 @@ fn host_failures_map_to_internal_without_becoming_domain_values() {
failing.reference(),
None,
"fail",
vec![],
BTreeMap::new(),
))
.unwrap_err();
assert_eq!(error.kind(), CallErrorKind::Implementation);
assert_eq!(
error.protocol_error().category,
ProtocolErrorCategory::Internal
);
assert_eq!(error.protocol_error().code, ProtocolErrorCode::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_protocol::ObjectId;
#[interface]
pub trait Search {
@@ -1,27 +1,27 @@
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
| |
| required by a bound introduced by this call
|
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
--> 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
|
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`
--> src/lib.rs