fix: align interface macros with canonical values
This commit is contained in:
+34
-32
@@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user