fix: align host dispatch with canonical paths
This commit is contained in:
@@ -174,22 +174,29 @@ references are rejected. Registry references are opaque and remain outside the
|
|||||||
descriptor; generated descriptors contain neither those references nor Rust
|
descriptor; generated descriptors contain neither those references nor Rust
|
||||||
trait identity.
|
trait identity.
|
||||||
|
|
||||||
Dispatch is qualified by interface reference and operation name. The host
|
Dispatch is qualified by interface reference and operation name and targets a
|
||||||
checks current object identity and validator policy, current interface
|
canonical Worldspace path. An `ObjectProvider` resolves that path exactly once
|
||||||
membership, interface validator, descriptor operation, arguments, and result
|
inside `with_object`; the host then checks the supplied object validator,
|
||||||
while retaining the selected object and registered artifact. Host-private
|
current interface membership, interface validator, descriptor operation,
|
||||||
fields and backend handles are never projected by `WipObject`.
|
operation-specific object-validator policy, arguments, and authorization in
|
||||||
|
canonical precedence. Path and children remain provider resolution context and
|
||||||
|
are never projected by `WipObject`; host-private fields and backend handles are
|
||||||
|
also excluded.
|
||||||
|
|
||||||
Application domain failures belong in a derived result union and are returned
|
Application domain failures belong in a derived result union and are returned
|
||||||
through `Ok`. `wip::HostError` is reserved for runtime/backend failures. Invalid
|
through `Ok`. `wip::HostError` is reserved for runtime/backend failures. Invalid
|
||||||
arguments are rejected before invocation; malformed encoded results are
|
arguments are rejected before invocation; malformed encoded results are
|
||||||
rejected as internal host failures and never returned as application data.
|
reported as `InvalidResult` and never returned as application data. Validator,
|
||||||
|
membership, authorization, and result failures retain their distinct canonical
|
||||||
|
protocol error codes.
|
||||||
|
|
||||||
Providers remain responsible for backend atomicity. Enter a transaction, lock,
|
Providers remain responsible for backend atomicity. Enter a transaction, lock,
|
||||||
stable-handle scope, or conditional-update primitive before constructing and
|
stable-handle scope, or conditional-update primitive before invoking the
|
||||||
dispatching the request-scoped `PublishedObject`, and retain it until dispatch
|
`with_object` callback, and retain it until the callback returns. Mutable
|
||||||
returns. The runtime never validates one object and then resolves a second
|
operations must make validator comparison and backend commit atomic and return a
|
||||||
snapshot, but it cannot make an arbitrary external backend atomic by itself.
|
validator for the committed state. The runtime never validates one object and
|
||||||
|
then resolves a second snapshot, but it cannot make an arbitrary external
|
||||||
|
backend atomic by itself.
|
||||||
|
|
||||||
## Validation
|
## Validation
|
||||||
|
|
||||||
|
|||||||
+219
-156
@@ -3,30 +3,26 @@
|
|||||||
//! # Responsibility boundaries
|
//! # Responsibility boundaries
|
||||||
//!
|
//!
|
||||||
//! [`InterfaceRegistry::register`] validates and freezes a generated interface
|
//! [`InterfaceRegistry::register`] validates and freezes a generated interface
|
||||||
//! descriptor, assigns an opaque [`InterfaceRef`], and makes the same descriptor
|
//! descriptor, assigns an opaque [`InterfaceRef`], and leaves that reference
|
||||||
//! observation available to clients and dispatch. [`PublishedObject::expose`]
|
//! outside the descriptor. [`PublishedObject::expose`] explicitly combines a
|
||||||
//! explicitly combines a request-scoped Rust object with a registered interface;
|
//! request-scoped Rust object with a registered interface; its
|
||||||
//! its [`InterfaceImplementation`] bound proves at compile time that the object
|
//! [`InterfaceImplementation`] bound proves at compile time that the object
|
||||||
//! implements that interface. Implementing a Rust trait alone never publishes it,
|
//! implements the interface. Implementing a Rust trait alone never publishes it,
|
||||||
//! and a publication with no interfaces is valid.
|
//! and a publication with no interfaces is valid.
|
||||||
//!
|
//!
|
||||||
//! [`PublishedObject::dispatch`] selects by `(InterfaceRef, operation name)`,
|
//! [`ObjectProvider::call_operation`] resolves `target.path` exactly once and
|
||||||
//! checks object and interface validators, current membership, the frozen
|
//! keeps the resulting object, interface membership, descriptor, implementation,
|
||||||
//! descriptor, and then invokes the matching generated typed dispatcher on the
|
//! and backend target fixed within the provider's [`ObjectProvider::with_object`]
|
||||||
//! same object instance. Arguments and results cross the protocol [`Value`]
|
//! callback. Dispatch is qualified by `(interface reference, operation name)`.
|
||||||
//! boundary only through generated validation and codecs. Rust fields that are
|
//! The same frozen operation declaration validates both arguments and results.
|
||||||
//! not returned by [`WipObject`] are host-private and are never serialized by
|
|
||||||
//! this crate.
|
|
||||||
//!
|
//!
|
||||||
//! The call method is deliberately an execution boundary rather than a global
|
//! A provider is responsible for entering a backend transaction, lock,
|
||||||
//! object registry: a provider may resolve and publish an object per request and
|
//! stable-handle scope, or conditional-update primitive before invoking the
|
||||||
//! drop it afterward. The provider must enter any backend-specific transaction,
|
//! callback and retaining it until the callback returns. The runtime does not
|
||||||
//! lock, stable-handle scope, or conditional-update primitive *before* creating
|
//! reopen the path or select another implementation after checking validators,
|
||||||
//! or dispatching the publication and retain it until dispatch returns. This
|
//! but cannot make an arbitrary external backend atomic on its own. Mutable
|
||||||
//! crate keeps validator checks, descriptor and implementation selection, and
|
//! operations must make object-validator comparison and backend commit atomic;
|
||||||
//! invocation in one borrow of that publication; it does not claim that an
|
//! the validator returned after dispatch must describe the committed state.
|
||||||
//! arbitrary backend stays stable, and it never reopens a path or resolves a
|
|
||||||
//! second implementation snapshot after validation.
|
|
||||||
|
|
||||||
#![deny(missing_docs)]
|
#![deny(missing_docs)]
|
||||||
|
|
||||||
@@ -41,8 +37,13 @@ use std::sync::atomic::{AtomicU64, Ordering};
|
|||||||
use wip_interface::DispatchError;
|
use wip_interface::DispatchError;
|
||||||
pub use wip_interface::{InterfaceDefinition, InterfaceImplementation};
|
pub use wip_interface::{InterfaceDefinition, InterfaceImplementation};
|
||||||
pub use wip_protocol;
|
pub use wip_protocol;
|
||||||
|
pub use wip_protocol::{
|
||||||
|
CallOperationRequest as CallRequest, CallOperationResponse as CallResult, InterfaceTarget,
|
||||||
|
Target,
|
||||||
|
};
|
||||||
use wip_protocol::{
|
use wip_protocol::{
|
||||||
InterfaceDescriptor, Object, ProtocolError, ProtocolErrorCode, ValidationError, Value,
|
FetchInterfaceResponse, InterfaceDescriptor, Object, ProtocolError, ProtocolErrorCode,
|
||||||
|
ValidationError, Value,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Opaque bytes used as a conditional execution precondition.
|
/// Opaque bytes used as a conditional execution precondition.
|
||||||
@@ -63,14 +64,6 @@ 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);
|
||||||
@@ -83,22 +76,34 @@ impl InterfaceRef {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An immutable observation returned for an opaque interface reference.
|
/// One atomic observation of an interface reference, descriptor, and validator.
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub struct InterfaceObservation {
|
pub struct InterfaceObservation {
|
||||||
/// Opaque reference selected by published objects and operation calls.
|
/// Opaque reference selected by published objects and operation calls.
|
||||||
pub reference: InterfaceRef,
|
pub reference: InterfaceRef,
|
||||||
|
/// Canonical descriptor frozen when the interface was registered.
|
||||||
|
pub descriptor: InterfaceDescriptor,
|
||||||
/// Optional validator. Its presence makes it a required call precondition.
|
/// Optional validator. Its presence makes it a required call precondition.
|
||||||
pub validator: Option<Validator>,
|
pub validator: Option<Validator>,
|
||||||
/// Validated descriptor frozen when the interface was registered.
|
}
|
||||||
pub descriptor: InterfaceDescriptor,
|
|
||||||
|
impl InterfaceObservation {
|
||||||
|
/// Converts this host observation to the canonical protocol response.
|
||||||
|
#[must_use]
|
||||||
|
pub fn protocol_response(&self) -> FetchInterfaceResponse {
|
||||||
|
FetchInterfaceResponse {
|
||||||
|
interface: self.reference.0.clone(),
|
||||||
|
descriptor: self.descriptor.clone(),
|
||||||
|
validator: self.validator.as_ref().map(|validator| validator.0.clone()),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct InterfaceArtifact {
|
struct InterfaceArtifact {
|
||||||
reference: InterfaceRef,
|
reference: InterfaceRef,
|
||||||
validator: Option<Validator>,
|
|
||||||
descriptor: InterfaceDescriptor,
|
descriptor: InterfaceDescriptor,
|
||||||
|
validator: Option<Validator>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A typed registration token for a generated interface definition.
|
/// A typed registration token for a generated interface definition.
|
||||||
@@ -129,12 +134,11 @@ impl<D> RegisteredInterface<D> {
|
|||||||
|
|
||||||
/// Registry of validated, immutable interface artifacts.
|
/// Registry of validated, immutable interface artifacts.
|
||||||
///
|
///
|
||||||
/// This registry stores interface definitions, not object instances. Generated
|
/// Generated descriptors are never rewritten to contain their assigned
|
||||||
/// definitions are registered explicitly and receive process-local opaque
|
|
||||||
/// references. A validator-free artifact is immutable for the lifetime of its
|
/// references. A validator-free artifact is immutable for the lifetime of its
|
||||||
/// reference; a validator-bearing artifact requires that exact validator on a
|
/// reference. A validator-bearing artifact requires that exact validator on a
|
||||||
/// call. Re-registering a definition is rejected instead of silently changing
|
/// call. Re-registering a definition is rejected rather than silently changing
|
||||||
/// the descriptor associated with an existing reference.
|
/// the observation associated with an existing reference.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct InterfaceRegistry {
|
pub struct InterfaceRegistry {
|
||||||
namespace: u64,
|
namespace: u64,
|
||||||
@@ -164,12 +168,6 @@ impl InterfaceRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Registers and freezes one generated interface definition.
|
/// Registers and freezes one generated interface definition.
|
||||||
///
|
|
||||||
/// `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 remains independent of both registration and its assigned
|
|
||||||
/// opaque reference.
|
|
||||||
pub fn register<D>(
|
pub fn register<D>(
|
||||||
&mut self,
|
&mut self,
|
||||||
_definition: D,
|
_definition: D,
|
||||||
@@ -198,8 +196,8 @@ impl InterfaceRegistry {
|
|||||||
.ok_or(RegistrationError::ReferenceSpaceExhausted)?;
|
.ok_or(RegistrationError::ReferenceSpaceExhausted)?;
|
||||||
let artifact = Arc::new(InterfaceArtifact {
|
let artifact = Arc::new(InterfaceArtifact {
|
||||||
reference: reference.clone(),
|
reference: reference.clone(),
|
||||||
validator,
|
|
||||||
descriptor,
|
descriptor,
|
||||||
|
validator,
|
||||||
});
|
});
|
||||||
self.definitions.insert(definition);
|
self.definitions.insert(definition);
|
||||||
self.artifacts.insert(reference, Arc::clone(&artifact));
|
self.artifacts.insert(reference, Arc::clone(&artifact));
|
||||||
@@ -211,28 +209,40 @@ impl InterfaceRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Resolves a transport-provided opaque reference string in this registry.
|
/// Resolves a transport-provided opaque reference string in this registry.
|
||||||
///
|
|
||||||
/// The returned value is registry-scoped and suitable for observation or a
|
|
||||||
/// qualified [`CallRequest`]. Unknown or foreign references return `None`;
|
|
||||||
/// constructing arbitrary `InterfaceRef` values is intentionally not part of
|
|
||||||
/// the public API.
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn resolve(&self, reference: &str) -> Option<InterfaceRef> {
|
pub fn resolve(&self, reference: &str) -> Option<InterfaceRef> {
|
||||||
let reference = InterfaceRef(reference.to_owned());
|
let reference = InterfaceRef(reference.to_owned());
|
||||||
self.artifacts.contains_key(&reference).then_some(reference)
|
self.artifacts.contains_key(&reference).then_some(reference)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Observes the descriptor and optional validator for an opaque reference.
|
/// Observes a registered interface by its host-side reference.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn observe(&self, reference: &InterfaceRef) -> Option<InterfaceObservation> {
|
pub fn observe(&self, reference: &InterfaceRef) -> Option<InterfaceObservation> {
|
||||||
|
self.artifacts.get(reference).map(Self::observation)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Implements canonical `fetch_interface` semantics for an opaque reference.
|
||||||
|
///
|
||||||
|
/// Unknown and currently unpublished references map to `InterfaceNotFound`,
|
||||||
|
/// not object membership errors or authorization failures.
|
||||||
|
pub fn fetch_interface(&self, reference: &str) -> Result<InterfaceObservation, ProtocolError> {
|
||||||
|
let reference = InterfaceRef(reference.to_owned());
|
||||||
self.artifacts
|
self.artifacts
|
||||||
.get(reference)
|
.get(&reference)
|
||||||
.map(|artifact| InterfaceObservation {
|
.map(Self::observation)
|
||||||
reference: artifact.reference.clone(),
|
.ok_or_else(|| ProtocolError {
|
||||||
validator: artifact.validator.clone(),
|
code: ProtocolErrorCode::InterfaceNotFound,
|
||||||
descriptor: artifact.descriptor.clone(),
|
message: "interface reference is not currently published".into(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn observation(artifact: &Arc<InterfaceArtifact>) -> InterfaceObservation {
|
||||||
|
InterfaceObservation {
|
||||||
|
reference: artifact.reference.clone(),
|
||||||
|
descriptor: artifact.descriptor.clone(),
|
||||||
|
validator: artifact.validator.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Failure while registering a generated interface definition.
|
/// Failure while registering a generated interface definition.
|
||||||
@@ -274,19 +284,32 @@ impl Error for RegistrationError {
|
|||||||
/// Public metadata and conditional-execution policy for a host-private object.
|
/// Public metadata and conditional-execution policy for a host-private object.
|
||||||
///
|
///
|
||||||
/// Only values returned by this trait are projected into protocol metadata.
|
/// Only values returned by this trait are projected into protocol metadata.
|
||||||
/// Backend handles, filesystem paths, credentials, and other private fields on
|
/// Paths, children, backend handles, credentials, and resolution state are not
|
||||||
/// the implementing type are neither inspected nor serialized.
|
/// part of this API and therefore cannot be serialized into [`Object`].
|
||||||
pub trait WipObject {
|
pub trait WipObject {
|
||||||
/// Returns the current logical identity of this object.
|
/// Returns the public path-segment name of this object.
|
||||||
fn id(&self) -> ObjectId;
|
fn name(&self) -> &str;
|
||||||
|
|
||||||
|
/// Returns a short public description, if one is available.
|
||||||
|
fn description(&self) -> Option<&str> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns optional opaque identity within the client-facing Worldspace.
|
||||||
|
fn reference(&self) -> Option<&str> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the current opaque object validator, if any.
|
/// Returns the current opaque object validator, if any.
|
||||||
fn validator(&self) -> Option<&Validator> {
|
fn validator(&self) -> Option<&Validator> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether a call must include the current object validator.
|
/// Whether this specific qualified operation requires an object validator.
|
||||||
fn requires_validator(&self) -> bool {
|
///
|
||||||
|
/// This hook is evaluated only after membership, interface validators, and
|
||||||
|
/// operation existence have been checked, preserving canonical precedence.
|
||||||
|
fn requires_validator(&self, _interface: &InterfaceRef, _operation: &str) -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -301,7 +324,8 @@ struct BoundInterface<T> {
|
|||||||
/// A host-private object instance combined with an explicit ordered interface set.
|
/// A host-private object instance combined with an explicit ordered interface set.
|
||||||
///
|
///
|
||||||
/// The instance is owned by this value and can therefore be request-scoped. No
|
/// The instance is owned by this value and can therefore be request-scoped. No
|
||||||
/// global or pre-populated object registry is required.
|
/// global object registry is required. Worldspace paths remain provider context
|
||||||
|
/// and are deliberately absent from the publication.
|
||||||
pub struct PublishedObject<T: WipObject> {
|
pub struct PublishedObject<T: WipObject> {
|
||||||
implementation: T,
|
implementation: T,
|
||||||
interfaces: Vec<BoundInterface<T>>,
|
interfaces: Vec<BoundInterface<T>>,
|
||||||
@@ -349,63 +373,46 @@ impl<T: WipObject> PublishedObject<T> {
|
|||||||
&self.implementation
|
&self.implementation
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Builds the protocol object metadata with interface insertion order intact.
|
/// Builds canonical protocol object metadata in interface insertion order.
|
||||||
|
///
|
||||||
|
/// The observation contains no path, children, descriptor, backend handle,
|
||||||
|
/// credential, or provider resolution state.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn protocol_object(&self) -> Object {
|
pub fn protocol_object(&self) -> Object {
|
||||||
let id = self.implementation.id();
|
Object {
|
||||||
let object = Object {
|
name: self.implementation.name().to_owned(),
|
||||||
name: id.0.clone(),
|
description: self.implementation.description().map(str::to_owned),
|
||||||
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),
|
r#ref: self.implementation.reference().map(str::to_owned),
|
||||||
validator: self
|
validator: self
|
||||||
.implementation
|
.implementation
|
||||||
.validator()
|
.validator()
|
||||||
.map(|validator| validator.0.clone()),
|
.map(|validator| validator.0.clone()),
|
||||||
};
|
|
||||||
debug_assert!(object.validate().is_ok());
|
|
||||||
object
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Dispatches one qualified operation against this current publication.
|
|
||||||
///
|
|
||||||
/// Preconditions are checked before generated operation code runs. The
|
|
||||||
/// selected artifact and function pointer are borrowed once and are not
|
|
||||||
/// resolved again after validator checks.
|
|
||||||
pub fn dispatch(&self, request: CallRequest) -> Result<CallResult, CallError> {
|
|
||||||
if request.object != self.implementation.id() {
|
|
||||||
return Err(CallError::new(
|
|
||||||
CallErrorKind::ObjectNotFound,
|
|
||||||
"call target does not identify this published object",
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn dispatch_resolved(
|
||||||
|
&self,
|
||||||
|
request: CallRequest,
|
||||||
|
authorize: impl FnOnce(&T, &InterfaceRef, &str) -> bool,
|
||||||
|
) -> Result<CallResult, CallError> {
|
||||||
let current_object_validator = self.implementation.validator();
|
let current_object_validator = self.implementation.validator();
|
||||||
match request.object_validator.as_ref() {
|
if let Some(supplied) = request.target.validator.as_deref()
|
||||||
Some(supplied) if Some(supplied) != current_object_validator => {
|
&& current_object_validator.map(Validator::as_bytes) != Some(supplied)
|
||||||
|
{
|
||||||
return Err(CallError::new(
|
return Err(CallError::new(
|
||||||
CallErrorKind::ValidatorMismatch,
|
CallErrorKind::ValidatorMismatch,
|
||||||
"object validator does not match current object state",
|
"object validator does not match current object state",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
None if self.implementation.requires_validator() => {
|
|
||||||
return Err(CallError::new(
|
|
||||||
CallErrorKind::ValidatorRequired,
|
|
||||||
"this object requires a validator",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
let Some(bound) = self
|
let Some(bound) = self.interfaces.iter().find(|bound| {
|
||||||
.interfaces
|
bound.artifact.reference.as_str() == request.interface.reference.as_str()
|
||||||
.iter()
|
}) else {
|
||||||
.find(|bound| bound.artifact.reference == request.interface.reference)
|
|
||||||
else {
|
|
||||||
return Err(CallError::new(
|
return Err(CallError::new(
|
||||||
CallErrorKind::InterfaceMismatch,
|
CallErrorKind::InterfaceMismatch,
|
||||||
"interface is not a member of the current object publication",
|
"interface is not a member of the current object publication",
|
||||||
@@ -414,7 +421,7 @@ impl<T: WipObject> PublishedObject<T> {
|
|||||||
|
|
||||||
match (
|
match (
|
||||||
bound.artifact.validator.as_ref(),
|
bound.artifact.validator.as_ref(),
|
||||||
request.interface.validator.as_ref(),
|
request.interface.validator.as_deref(),
|
||||||
) {
|
) {
|
||||||
(Some(_), None) => {
|
(Some(_), None) => {
|
||||||
return Err(CallError::new(
|
return Err(CallError::new(
|
||||||
@@ -422,7 +429,7 @@ impl<T: WipObject> PublishedObject<T> {
|
|||||||
"registered interface requires a validator",
|
"registered interface requires a validator",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
(expected, Some(actual)) if expected != Some(actual) => {
|
(expected, Some(actual)) if expected.map(Validator::as_bytes) != Some(actual) => {
|
||||||
return Err(CallError::new(
|
return Err(CallError::new(
|
||||||
CallErrorKind::InterfaceValidatorMismatch,
|
CallErrorKind::InterfaceValidatorMismatch,
|
||||||
"interface validator does not match the registered descriptor",
|
"interface validator does not match the registered descriptor",
|
||||||
@@ -444,12 +451,34 @@ impl<T: WipObject> PublishedObject<T> {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if request.target.validator.is_none()
|
||||||
|
&& self
|
||||||
|
.implementation
|
||||||
|
.requires_validator(&bound.artifact.reference, &request.operation)
|
||||||
|
{
|
||||||
|
return Err(CallError::new(
|
||||||
|
CallErrorKind::ValidatorRequired,
|
||||||
|
"this operation requires an object validator",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
bound
|
bound
|
||||||
.artifact
|
.artifact
|
||||||
.descriptor
|
.descriptor
|
||||||
.validate_arguments(&request.operation, &request.arguments)
|
.validate_arguments(&request.operation, &request.arguments)
|
||||||
.map_err(|error| CallError::new(CallErrorKind::InvalidArguments, error.to_string()))?;
|
.map_err(|error| CallError::new(CallErrorKind::InvalidArguments, error.to_string()))?;
|
||||||
|
|
||||||
|
if !authorize(
|
||||||
|
&self.implementation,
|
||||||
|
&bound.artifact.reference,
|
||||||
|
&request.operation,
|
||||||
|
) {
|
||||||
|
return Err(CallError::new(
|
||||||
|
CallErrorKind::PermissionDenied,
|
||||||
|
"operation is not permitted for this caller",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
let value = (bound.dispatch)(&self.implementation, &request.operation, request.arguments)
|
let value = (bound.dispatch)(&self.implementation, &request.operation, request.arguments)
|
||||||
.map_err(CallError::from_dispatch)?;
|
.map_err(CallError::from_dispatch)?;
|
||||||
|
|
||||||
@@ -460,8 +489,11 @@ impl<T: WipObject> PublishedObject<T> {
|
|||||||
.map_err(|error| CallError::new(CallErrorKind::InvalidResult, error.to_string()))?;
|
.map_err(|error| CallError::new(CallErrorKind::InvalidResult, error.to_string()))?;
|
||||||
|
|
||||||
Ok(CallResult {
|
Ok(CallResult {
|
||||||
value,
|
result: value,
|
||||||
validator: self.implementation.validator().cloned(),
|
validator: self
|
||||||
|
.implementation
|
||||||
|
.validator()
|
||||||
|
.map(|validator| validator.0.clone()),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -472,6 +504,70 @@ impl<T: WipObject> PublishedObject<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A request-scoped Worldspace resolver and execution boundary.
|
||||||
|
///
|
||||||
|
/// `with_object` must resolve the current object at `path` exactly once and pass
|
||||||
|
/// a publication that pins its current interface set, implementations, and
|
||||||
|
/// backend target for the duration of `operation`. Implementations may hold a
|
||||||
|
/// transaction or lock around the callback. Returning `None` means the path is
|
||||||
|
/// not published to the caller. The borrowed publication cannot escape the
|
||||||
|
/// callback, so the default call path cannot outlive that provider boundary.
|
||||||
|
pub trait ObjectProvider {
|
||||||
|
/// Host-private object type produced by this provider.
|
||||||
|
type Object: WipObject;
|
||||||
|
|
||||||
|
/// Runs an operation against the single object snapshot resolved at `path`.
|
||||||
|
fn with_object<R>(
|
||||||
|
&self,
|
||||||
|
path: &str,
|
||||||
|
operation: impl FnOnce(&PublishedObject<Self::Object>) -> R,
|
||||||
|
) -> Option<R>;
|
||||||
|
|
||||||
|
/// Decides whether a declared operation may execute for the current caller.
|
||||||
|
///
|
||||||
|
/// The hook runs after all validators, membership, operation lookup, and
|
||||||
|
/// argument validation, but before implementation code. Returning `false`
|
||||||
|
/// exposes the operation while producing `PermissionDenied`.
|
||||||
|
fn authorize(
|
||||||
|
&self,
|
||||||
|
_path: &str,
|
||||||
|
_object: &Self::Object,
|
||||||
|
_interface: &InterfaceRef,
|
||||||
|
_operation: &str,
|
||||||
|
) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetches the canonical object projection currently published at `path`.
|
||||||
|
fn fetch(&self, path: &str) -> Result<Object, CallError> {
|
||||||
|
self.with_object(path, PublishedObject::protocol_object)
|
||||||
|
.ok_or_else(|| CallError::new(CallErrorKind::NotFound, "target path is not published"))
|
||||||
|
.and_then(|object| {
|
||||||
|
object.validate_at_path(path).map_err(|error| {
|
||||||
|
CallError::new(CallErrorKind::InvalidResult, error.to_string())
|
||||||
|
})?;
|
||||||
|
Ok(object)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolves and executes a canonical path-targeted operation request.
|
||||||
|
///
|
||||||
|
/// Failure checks follow the exact canonical precedence: path, supplied
|
||||||
|
/// object validator mismatch, membership, interface validator requirement
|
||||||
|
/// and mismatch, operation existence, object validator requirement,
|
||||||
|
/// arguments, then authorization. No failed precondition invokes the typed
|
||||||
|
/// implementation.
|
||||||
|
fn call_operation(&self, request: CallRequest) -> Result<CallResult, CallError> {
|
||||||
|
let path = request.target.path.clone();
|
||||||
|
self.with_object(&path, |publication| {
|
||||||
|
publication.dispatch_resolved(request, |object, interface, operation| {
|
||||||
|
self.authorize(&path, object, interface, operation)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.ok_or_else(|| CallError::new(CallErrorKind::NotFound, "target path is not published"))?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Failure to construct an explicit object publication.
|
/// Failure to construct an explicit object publication.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum PublicationError {
|
pub enum PublicationError {
|
||||||
@@ -493,44 +589,11 @@ impl Display for PublicationError {
|
|||||||
|
|
||||||
impl Error for PublicationError {}
|
impl Error for PublicationError {}
|
||||||
|
|
||||||
/// Interface portion of a qualified operation target.
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
||||||
pub struct InterfaceTarget {
|
|
||||||
/// Opaque reference selected from the current object's interface set.
|
|
||||||
pub reference: InterfaceRef,
|
|
||||||
/// Validator copied from the descriptor observation, when one was present.
|
|
||||||
pub validator: Option<Validator>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// One transport-independent request to a published object operation.
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
|
||||||
pub struct CallRequest {
|
|
||||||
/// Current logical object identity.
|
|
||||||
pub object: ObjectId,
|
|
||||||
/// Optional current object-state precondition.
|
|
||||||
pub object_validator: Option<Validator>,
|
|
||||||
/// Qualified interface reference and descriptor precondition.
|
|
||||||
pub interface: InterfaceTarget,
|
|
||||||
/// Operation name within the selected interface descriptor.
|
|
||||||
pub operation: String,
|
|
||||||
/// Named protocol arguments.
|
|
||||||
pub arguments: BTreeMap<String, Value>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Successful host dispatch result.
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
|
||||||
pub struct CallResult {
|
|
||||||
/// Encoded application result. No-data operations return [`Value::Unit`].
|
|
||||||
pub value: Value,
|
|
||||||
/// Object validator observed after the implementation returned.
|
|
||||||
pub validator: Option<Validator>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Stable host dispatch failure kind.
|
/// Stable host dispatch failure kind.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum CallErrorKind {
|
pub enum CallErrorKind {
|
||||||
/// The call names a different or unavailable object.
|
/// The target path is not currently published.
|
||||||
ObjectNotFound,
|
NotFound,
|
||||||
/// The selected interface is not exposed by the current object.
|
/// The selected interface is not exposed by the current object.
|
||||||
InterfaceMismatch,
|
InterfaceMismatch,
|
||||||
/// The operation is absent from the selected descriptor.
|
/// The operation is absent from the selected descriptor.
|
||||||
@@ -539,7 +602,7 @@ pub enum CallErrorKind {
|
|||||||
InvalidArguments,
|
InvalidArguments,
|
||||||
/// The encoded implementation result violates its declaration.
|
/// The encoded implementation result violates its declaration.
|
||||||
InvalidResult,
|
InvalidResult,
|
||||||
/// This object requires a validator and the call omitted it.
|
/// This operation requires a validator and the call omitted it.
|
||||||
ValidatorRequired,
|
ValidatorRequired,
|
||||||
/// The supplied object validator is stale or belongs to another object.
|
/// The supplied object validator is stale or belongs to another object.
|
||||||
ValidatorMismatch,
|
ValidatorMismatch,
|
||||||
@@ -547,6 +610,8 @@ pub enum CallErrorKind {
|
|||||||
InterfaceValidatorRequired,
|
InterfaceValidatorRequired,
|
||||||
/// The supplied interface validator does not identify the current descriptor.
|
/// The supplied interface validator does not identify the current descriptor.
|
||||||
InterfaceValidatorMismatch,
|
InterfaceValidatorMismatch,
|
||||||
|
/// The operation is declared but not authorized for the caller.
|
||||||
|
PermissionDenied,
|
||||||
/// The typed implementation reported a host/backend failure.
|
/// The typed implementation reported a host/backend failure.
|
||||||
Implementation,
|
Implementation,
|
||||||
}
|
}
|
||||||
@@ -602,30 +667,28 @@ impl CallError {
|
|||||||
&self.message
|
&self.message
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Maps this host failure to the existing protocol error taxonomy.
|
/// Maps this host failure to the canonical protocol error taxonomy.
|
||||||
///
|
|
||||||
/// Validator details remain available through [`CallError::kind`], while
|
|
||||||
/// the current protocol model represents conditional-request failures as
|
|
||||||
/// `InvalidRequest`. Result/codec and implementation failures are internal,
|
|
||||||
/// because exposing malformed host output as an application failure would
|
|
||||||
/// be unsafe.
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn protocol_error(&self) -> ProtocolError {
|
pub fn protocol_error(&self) -> ProtocolError {
|
||||||
let category = match self.kind {
|
let code = match self.kind {
|
||||||
CallErrorKind::ObjectNotFound => ProtocolErrorCode::NotFound,
|
CallErrorKind::NotFound => ProtocolErrorCode::NotFound,
|
||||||
CallErrorKind::InterfaceMismatch => ProtocolErrorCode::InterfaceMismatch,
|
CallErrorKind::InterfaceMismatch => ProtocolErrorCode::InterfaceMismatch,
|
||||||
CallErrorKind::OperationNotFound => ProtocolErrorCode::OperationNotFound,
|
CallErrorKind::OperationNotFound => ProtocolErrorCode::OperationNotFound,
|
||||||
CallErrorKind::InvalidArguments => ProtocolErrorCode::InvalidArguments,
|
CallErrorKind::InvalidArguments => ProtocolErrorCode::InvalidArguments,
|
||||||
CallErrorKind::ValidatorRequired
|
CallErrorKind::InvalidResult => ProtocolErrorCode::InvalidResult,
|
||||||
| CallErrorKind::ValidatorMismatch
|
CallErrorKind::ValidatorRequired => ProtocolErrorCode::ValidatorRequired,
|
||||||
| CallErrorKind::InterfaceValidatorRequired
|
CallErrorKind::ValidatorMismatch => ProtocolErrorCode::ValidatorMismatch,
|
||||||
| CallErrorKind::InterfaceValidatorMismatch => ProtocolErrorCode::InvalidRequest,
|
CallErrorKind::InterfaceValidatorRequired => {
|
||||||
CallErrorKind::InvalidResult | CallErrorKind::Implementation => {
|
ProtocolErrorCode::InterfaceValidatorRequired
|
||||||
ProtocolErrorCode::Internal
|
|
||||||
}
|
}
|
||||||
|
CallErrorKind::InterfaceValidatorMismatch => {
|
||||||
|
ProtocolErrorCode::InterfaceValidatorMismatch
|
||||||
|
}
|
||||||
|
CallErrorKind::PermissionDenied => ProtocolErrorCode::PermissionDenied,
|
||||||
|
CallErrorKind::Implementation => ProtocolErrorCode::Internal,
|
||||||
};
|
};
|
||||||
ProtocolError {
|
ProtocolError {
|
||||||
code: category,
|
code,
|
||||||
message: self.message.clone(),
|
message: self.message.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+439
-314
@@ -1,9 +1,10 @@
|
|||||||
use std::cell::{Cell, RefCell};
|
use std::cell::Cell;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
use wip_host::{
|
use wip_host::{
|
||||||
CallErrorKind, CallRequest, InterfaceRegistry, InterfaceTarget, ObjectId, PublishedObject,
|
CallError, CallErrorKind, CallRequest, InterfaceRef, InterfaceRegistry, InterfaceTarget,
|
||||||
Validator, WipObject,
|
ObjectProvider, PublishedObject, RegisteredInterface, Target, Validator, WipObject,
|
||||||
};
|
};
|
||||||
use wip_interface::{
|
use wip_interface::{
|
||||||
CodecError, HostError, OperationResult, TypeDeclaration, TypeExpr, Value, WipType, interface,
|
CodecError, HostError, OperationResult, TypeDeclaration, TypeExpr, Value, WipType, interface,
|
||||||
@@ -17,6 +18,9 @@ pub trait Reader {
|
|||||||
|
|
||||||
/// Adds two integers.
|
/// Adds two integers.
|
||||||
fn add(&self, left: i64, right: i64) -> OperationResult<i64>;
|
fn add(&self, left: i64, right: i64) -> OperationResult<i64>;
|
||||||
|
|
||||||
|
/// Reports a backend failure.
|
||||||
|
fn fail(&self) -> OperationResult<String>;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[interface]
|
#[interface]
|
||||||
@@ -57,31 +61,37 @@ impl WipType for BadValue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct Document {
|
struct Document {
|
||||||
id: String,
|
name: String,
|
||||||
|
description: Option<String>,
|
||||||
|
reference: Option<String>,
|
||||||
validator: Option<Validator>,
|
validator: Option<Validator>,
|
||||||
require_validator: bool,
|
require_validator: bool,
|
||||||
calls: Cell<usize>,
|
calls: Rc<Cell<usize>>,
|
||||||
private_backend_handle: RefCell<String>,
|
private_backend_handle: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Document {
|
impl Document {
|
||||||
fn new(id: &str) -> Self {
|
fn open(name: &str, reference: Option<&str>, calls: Rc<Cell<usize>>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id: id.into(),
|
name: name.into(),
|
||||||
|
description: Some("public document".into()),
|
||||||
|
reference: reference.map(str::to_owned),
|
||||||
validator: None,
|
validator: None,
|
||||||
require_validator: false,
|
require_validator: false,
|
||||||
calls: Cell::new(0),
|
calls,
|
||||||
private_backend_handle: RefCell::new("/private/backend/path".into()),
|
private_backend_handle: "/private/backend/path".into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn strict(id: &str, validator: &[u8]) -> Self {
|
fn strict(name: &str, reference: &str, validator: &[u8], calls: Rc<Cell<usize>>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id: id.into(),
|
name: name.into(),
|
||||||
|
description: Some("public document".into()),
|
||||||
|
reference: Some(reference.into()),
|
||||||
validator: Some(Validator::new(validator)),
|
validator: Some(Validator::new(validator)),
|
||||||
require_validator: true,
|
require_validator: true,
|
||||||
calls: Cell::new(0),
|
calls,
|
||||||
private_backend_handle: RefCell::new("secret-handle".into()),
|
private_backend_handle: "secret-handle".into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,15 +101,23 @@ impl Document {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl WipObject for Document {
|
impl WipObject for Document {
|
||||||
fn id(&self) -> ObjectId {
|
fn name(&self) -> &str {
|
||||||
ObjectId(self.id.clone())
|
&self.name
|
||||||
|
}
|
||||||
|
|
||||||
|
fn description(&self) -> Option<&str> {
|
||||||
|
self.description.as_deref()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reference(&self) -> Option<&str> {
|
||||||
|
self.reference.as_deref()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validator(&self) -> Option<&Validator> {
|
fn validator(&self) -> Option<&Validator> {
|
||||||
self.validator.as_ref()
|
self.validator.as_ref()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn requires_validator(&self) -> bool {
|
fn requires_validator(&self, _: &InterfaceRef, _: &str) -> bool {
|
||||||
self.require_validator
|
self.require_validator
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -114,6 +132,11 @@ impl Reader for Document {
|
|||||||
self.record_call();
|
self.record_call();
|
||||||
Ok(left + right)
|
Ok(left + right)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn fail(&self) -> OperationResult<String> {
|
||||||
|
self.record_call();
|
||||||
|
Err(HostError::new("backend unavailable"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Writer for Document {
|
impl Writer for Document {
|
||||||
@@ -137,366 +160,468 @@ impl Broken for Document {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct TestProvider {
|
||||||
|
reader: RegisteredInterface<ReaderDefinition>,
|
||||||
|
writer: RegisteredInterface<WriterDefinition>,
|
||||||
|
broken: RegisteredInterface<BrokenDefinition>,
|
||||||
|
calls: Rc<Cell<usize>>,
|
||||||
|
resolutions: Cell<usize>,
|
||||||
|
authorization_checks: Cell<usize>,
|
||||||
|
deny: Cell<bool>,
|
||||||
|
switched: Cell<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TestProvider {
|
||||||
|
fn new(
|
||||||
|
reader: RegisteredInterface<ReaderDefinition>,
|
||||||
|
writer: RegisteredInterface<WriterDefinition>,
|
||||||
|
broken: RegisteredInterface<BrokenDefinition>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
reader,
|
||||||
|
writer,
|
||||||
|
broken,
|
||||||
|
calls: Rc::new(Cell::new(0)),
|
||||||
|
resolutions: Cell::new(0),
|
||||||
|
authorization_checks: Cell::new(0),
|
||||||
|
deny: Cell::new(false),
|
||||||
|
switched: Cell::new(false),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ObjectProvider for TestProvider {
|
||||||
|
type Object = Document;
|
||||||
|
|
||||||
|
fn with_object<R>(
|
||||||
|
&self,
|
||||||
|
path: &str,
|
||||||
|
operation: impl FnOnce(&PublishedObject<Self::Object>) -> R,
|
||||||
|
) -> Option<R> {
|
||||||
|
self.resolutions.set(self.resolutions.get() + 1);
|
||||||
|
let (document, interfaces) = match path {
|
||||||
|
"/document" => (
|
||||||
|
Document::strict(
|
||||||
|
"document",
|
||||||
|
"document-ref",
|
||||||
|
b"object-v1",
|
||||||
|
Rc::clone(&self.calls),
|
||||||
|
),
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
"/open" => (
|
||||||
|
Document::open("open", Some("open-ref"), Rc::clone(&self.calls)),
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
"/left/document" | "/right/document" => (
|
||||||
|
Document::strict(
|
||||||
|
"document",
|
||||||
|
"shared-ref",
|
||||||
|
b"shared-v1",
|
||||||
|
Rc::clone(&self.calls),
|
||||||
|
),
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
"/switch" => {
|
||||||
|
let switched = self.switched.replace(true);
|
||||||
|
let (reference, validator) = if switched {
|
||||||
|
("second-ref", b"second".as_slice())
|
||||||
|
} else {
|
||||||
|
("first-ref", b"first".as_slice())
|
||||||
|
};
|
||||||
|
(
|
||||||
|
Document::strict("switch", reference, validator, Rc::clone(&self.calls)),
|
||||||
|
2,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
"/empty" => (Document::open("empty", None, Rc::clone(&self.calls)), 0),
|
||||||
|
"/broken" => (
|
||||||
|
Document::open("broken", Some("broken-ref"), Rc::clone(&self.calls)),
|
||||||
|
3,
|
||||||
|
),
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let publication = PublishedObject::new(document);
|
||||||
|
let publication = match interfaces {
|
||||||
|
0 => publication,
|
||||||
|
2 => publication
|
||||||
|
.expose(&self.writer)
|
||||||
|
.unwrap()
|
||||||
|
.expose(&self.reader)
|
||||||
|
.unwrap(),
|
||||||
|
3 => publication.expose(&self.broken).unwrap(),
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
Some(operation(&publication))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn authorize(&self, _: &str, _: &Document, _: &InterfaceRef, _: &str) -> bool {
|
||||||
|
self.authorization_checks
|
||||||
|
.set(self.authorization_checks.get() + 1);
|
||||||
|
!self.deny.get()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn registry_and_provider() -> (
|
||||||
|
InterfaceRegistry,
|
||||||
|
TestProvider,
|
||||||
|
RegisteredInterface<HiddenDefinition>,
|
||||||
|
) {
|
||||||
|
let mut registry = InterfaceRegistry::new();
|
||||||
|
let reader = registry
|
||||||
|
.register(ReaderDefinition, Some(Validator::new(b"schema-v1")))
|
||||||
|
.unwrap();
|
||||||
|
let writer = registry.register(WriterDefinition, None).unwrap();
|
||||||
|
let hidden = registry.register(HiddenDefinition, None).unwrap();
|
||||||
|
let broken = registry.register(BrokenDefinition, None).unwrap();
|
||||||
|
let provider = TestProvider::new(reader, writer, broken);
|
||||||
|
(registry, provider, hidden)
|
||||||
|
}
|
||||||
|
|
||||||
fn request(
|
fn request(
|
||||||
object: &str,
|
path: &str,
|
||||||
object_validator: Option<&[u8]>,
|
object_validator: Option<&[u8]>,
|
||||||
reference: &wip_host::InterfaceRef,
|
reference: &InterfaceRef,
|
||||||
interface_validator: Option<&[u8]>,
|
interface_validator: Option<&[u8]>,
|
||||||
operation: &str,
|
operation: &str,
|
||||||
arguments: BTreeMap<String, Value>,
|
arguments: BTreeMap<String, Value>,
|
||||||
) -> CallRequest {
|
) -> CallRequest {
|
||||||
CallRequest {
|
CallRequest {
|
||||||
object: ObjectId(object.into()),
|
target: Target {
|
||||||
object_validator: object_validator.map(Validator::new),
|
path: path.into(),
|
||||||
|
validator: object_validator.map(<[u8]>::to_vec),
|
||||||
|
},
|
||||||
interface: InterfaceTarget {
|
interface: InterfaceTarget {
|
||||||
reference: reference.clone(),
|
reference: reference.as_str().to_owned(),
|
||||||
validator: interface_validator.map(Validator::new),
|
validator: interface_validator.map(<[u8]>::to_vec),
|
||||||
},
|
},
|
||||||
operation: operation.into(),
|
operation: operation.into(),
|
||||||
arguments,
|
arguments,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
fn assert_failure(error: CallError, kind: CallErrorKind, code: ProtocolErrorCode) {
|
||||||
fn registry_exposes_opaque_validated_descriptor_observations() {
|
assert_eq!(error.kind(), kind);
|
||||||
let mut registry = InterfaceRegistry::new();
|
assert_eq!(error.protocol_error().code, code);
|
||||||
let reader = registry.register(ReaderDefinition, None).unwrap();
|
|
||||||
|
|
||||||
assert_ne!(reader.reference().as_str(), "Reader");
|
|
||||||
assert_eq!(
|
|
||||||
registry.resolve(reader.reference().as_str()).as_ref(),
|
|
||||||
Some(reader.reference())
|
|
||||||
);
|
|
||||||
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, ReaderDefinition::descriptor());
|
|
||||||
assert_eq!(observation.validator, None);
|
|
||||||
observation.descriptor.validate().unwrap();
|
|
||||||
|
|
||||||
assert!(registry.register(ReaderDefinition, None).is_err());
|
|
||||||
|
|
||||||
let mut other_registry = InterfaceRegistry::new();
|
|
||||||
let other_reader = other_registry.register(ReaderDefinition, None).unwrap();
|
|
||||||
assert_ne!(reader.reference(), other_reader.reference());
|
|
||||||
assert!(
|
|
||||||
registry
|
|
||||||
.resolve(other_reader.reference().as_str())
|
|
||||||
.is_none()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn publication_is_explicit_ordered_qualified_and_request_scoped() {
|
fn registry_returns_atomic_canonical_observation_without_rewriting_descriptor() {
|
||||||
let mut registry = InterfaceRegistry::new();
|
let (registry, provider, _) = registry_and_provider();
|
||||||
let reader = registry.register(ReaderDefinition, None).unwrap();
|
let reference = provider.reader.reference();
|
||||||
let writer = registry.register(WriterDefinition, None).unwrap();
|
|
||||||
let _hidden = registry.register(HiddenDefinition, None).unwrap();
|
|
||||||
|
|
||||||
let publication = PublishedObject::new(Document::new("document-1"))
|
assert_ne!(reference.as_str(), "Reader");
|
||||||
.expose(&writer)
|
let observation = registry.fetch_interface(reference.as_str()).unwrap();
|
||||||
.unwrap()
|
assert_eq!(observation.reference, *reference);
|
||||||
.expose(&reader)
|
assert_eq!(observation.descriptor, ReaderDefinition::descriptor());
|
||||||
.unwrap();
|
assert_eq!(observation.validator, Some(Validator::new(b"schema-v1")));
|
||||||
|
let response = observation.protocol_response();
|
||||||
|
assert_eq!(response.interface, reference.as_str());
|
||||||
|
assert_eq!(response.descriptor, ReaderDefinition::descriptor());
|
||||||
|
assert_eq!(response.validator.as_deref(), Some(b"schema-v1".as_slice()));
|
||||||
|
response.descriptor.validate().unwrap();
|
||||||
|
|
||||||
let object = publication.protocol_object();
|
let error = registry.fetch_interface("unknown").unwrap_err();
|
||||||
|
assert_eq!(error.code, ProtocolErrorCode::InterfaceNotFound);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn canonical_object_projection_supports_optional_metadata_and_ordered_sets() {
|
||||||
|
let (_, provider, _) = registry_and_provider();
|
||||||
|
let object = provider.fetch("/document").unwrap();
|
||||||
|
|
||||||
|
assert_eq!(object.name, "document");
|
||||||
|
assert_eq!(object.description.as_deref(), Some("public document"));
|
||||||
|
assert_eq!(object.r#ref.as_deref(), Some("document-ref"));
|
||||||
|
assert_eq!(object.validator.as_deref(), Some(b"object-v1".as_slice()));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
object.interfaces,
|
object.interfaces,
|
||||||
vec![
|
vec![
|
||||||
writer.reference().as_str().to_owned(),
|
provider.writer.reference().as_str().to_owned(),
|
||||||
reader.reference().as_str().to_owned()
|
provider.reader.reference().as_str().to_owned(),
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
registry
|
assert!(!format!("{object:?}").contains("secret-handle"));
|
||||||
.observe(writer.reference())
|
assert!(!format!("{object:?}").contains("/document"));
|
||||||
.unwrap()
|
|
||||||
.descriptor
|
|
||||||
.validate()
|
|
||||||
.unwrap();
|
|
||||||
registry
|
|
||||||
.observe(reader.reference())
|
|
||||||
.unwrap()
|
|
||||||
.descriptor
|
|
||||||
.validate()
|
|
||||||
.unwrap();
|
|
||||||
assert!(!format!("{object:?}").contains("private/backend/path"));
|
|
||||||
|
|
||||||
let writer_result = publication
|
let empty = provider.fetch("/empty").unwrap();
|
||||||
.dispatch(request(
|
assert!(empty.interfaces.is_empty());
|
||||||
"document-1",
|
assert_eq!(empty.r#ref, None);
|
||||||
|
assert_eq!(empty.validator, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn duplicate_exposure_is_rejected_and_private_state_is_not_projected() {
|
||||||
|
let (_, provider, _) = registry_and_provider();
|
||||||
|
let publication = PublishedObject::new(Document::open(
|
||||||
|
"duplicate",
|
||||||
None,
|
None,
|
||||||
writer.reference(),
|
Rc::clone(&provider.calls),
|
||||||
None,
|
|
||||||
"label",
|
|
||||||
BTreeMap::new(),
|
|
||||||
))
|
))
|
||||||
|
.expose(&provider.reader)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let reader_result = publication
|
|
||||||
.dispatch(request(
|
|
||||||
"document-1",
|
|
||||||
None,
|
|
||||||
reader.reference(),
|
|
||||||
None,
|
|
||||||
"label",
|
|
||||||
BTreeMap::new(),
|
|
||||||
))
|
|
||||||
.unwrap();
|
|
||||||
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!(
|
assert_eq!(
|
||||||
publication
|
publication.implementation().private_backend_handle,
|
||||||
.implementation()
|
|
||||||
.private_backend_handle
|
|
||||||
.borrow()
|
|
||||||
.as_str(),
|
|
||||||
"/private/backend/path"
|
"/private/backend/path"
|
||||||
);
|
);
|
||||||
}
|
let error = match publication.expose(&provider.reader) {
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn implemented_but_unexposed_interface_is_not_a_member() {
|
|
||||||
let mut registry = InterfaceRegistry::new();
|
|
||||||
let reader = registry.register(ReaderDefinition, None).unwrap();
|
|
||||||
let hidden = registry.register(HiddenDefinition, None).unwrap();
|
|
||||||
let publication = PublishedObject::new(Document::new("document-2"))
|
|
||||||
.expose(&reader)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let error = publication
|
|
||||||
.dispatch(request(
|
|
||||||
"document-2",
|
|
||||||
None,
|
|
||||||
hidden.reference(),
|
|
||||||
None,
|
|
||||||
"hidden",
|
|
||||||
BTreeMap::new(),
|
|
||||||
))
|
|
||||||
.unwrap_err();
|
|
||||||
assert_eq!(error.kind(), CallErrorKind::InterfaceMismatch);
|
|
||||||
assert_eq!(
|
|
||||||
error.protocol_error().code,
|
|
||||||
ProtocolErrorCode::InterfaceMismatch
|
|
||||||
);
|
|
||||||
assert_eq!(publication.implementation().calls.get(), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn zero_interface_objects_are_valid() {
|
|
||||||
let publication = PublishedObject::new(Document::new("empty"));
|
|
||||||
let object = publication.protocol_object();
|
|
||||||
assert_eq!(object.name, "empty");
|
|
||||||
assert_eq!(object.r#ref.as_deref(), Some("empty"));
|
|
||||||
assert!(object.interfaces.is_empty());
|
|
||||||
object.validate().unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn duplicate_exposure_is_rejected_without_reordering() {
|
|
||||||
let mut registry = InterfaceRegistry::new();
|
|
||||||
let reader = registry.register(ReaderDefinition, None).unwrap();
|
|
||||||
let publication = PublishedObject::new(Document::new("duplicate"))
|
|
||||||
.expose(&reader)
|
|
||||||
.unwrap();
|
|
||||||
let error = match publication.expose(&reader) {
|
|
||||||
Ok(_) => panic!("duplicate exposure unexpectedly succeeded"),
|
Ok(_) => panic!("duplicate exposure unexpectedly succeeded"),
|
||||||
Err(error) => error,
|
Err(error) => error,
|
||||||
};
|
};
|
||||||
assert!(error.to_string().contains(reader.reference().as_str()));
|
assert!(
|
||||||
}
|
error
|
||||||
|
.to_string()
|
||||||
#[test]
|
.contains(provider.reader.reference().as_str())
|
||||||
fn operation_and_argument_failures_are_mapped_without_execution() {
|
|
||||||
let mut registry = InterfaceRegistry::new();
|
|
||||||
let reader = registry.register(ReaderDefinition, None).unwrap();
|
|
||||||
let publication = PublishedObject::new(Document::new("validation"))
|
|
||||||
.expose(&reader)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let missing = publication
|
|
||||||
.dispatch(request(
|
|
||||||
"validation",
|
|
||||||
None,
|
|
||||||
reader.reference(),
|
|
||||||
None,
|
|
||||||
"missing",
|
|
||||||
BTreeMap::new(),
|
|
||||||
))
|
|
||||||
.unwrap_err();
|
|
||||||
assert_eq!(missing.kind(), CallErrorKind::OperationNotFound);
|
|
||||||
assert_eq!(
|
|
||||||
missing.protocol_error().code,
|
|
||||||
ProtocolErrorCode::OperationNotFound
|
|
||||||
);
|
);
|
||||||
|
|
||||||
let invalid = publication
|
|
||||||
.dispatch(request(
|
|
||||||
"validation",
|
|
||||||
None,
|
|
||||||
reader.reference(),
|
|
||||||
None,
|
|
||||||
"add",
|
|
||||||
BTreeMap::from([("left".into(), Value::String("wrong".into()))]),
|
|
||||||
))
|
|
||||||
.unwrap_err();
|
|
||||||
assert_eq!(invalid.kind(), CallErrorKind::InvalidArguments);
|
|
||||||
assert_eq!(
|
|
||||||
invalid.protocol_error().code,
|
|
||||||
ProtocolErrorCode::InvalidArguments
|
|
||||||
);
|
|
||||||
assert_eq!(publication.implementation().calls.get(), 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn invalid_implementation_result_is_internal_and_not_returned() {
|
fn paths_are_resolution_context_not_object_identity() {
|
||||||
let mut registry = InterfaceRegistry::new();
|
let (_, provider, _) = registry_and_provider();
|
||||||
let broken = registry.register(BrokenDefinition, None).unwrap();
|
|
||||||
let publication = PublishedObject::new(Document::new("broken"))
|
|
||||||
.expose(&broken)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let error = publication
|
let left = provider.fetch("/left/document").unwrap();
|
||||||
.dispatch(request(
|
let right = provider.fetch("/right/document").unwrap();
|
||||||
"broken",
|
assert_eq!(left, right);
|
||||||
None,
|
assert_eq!(left.r#ref.as_deref(), Some("shared-ref"));
|
||||||
broken.reference(),
|
|
||||||
None,
|
let first = provider.fetch("/switch").unwrap();
|
||||||
"broken",
|
let second = provider.fetch("/switch").unwrap();
|
||||||
BTreeMap::new(),
|
assert_ne!(first.r#ref, second.r#ref);
|
||||||
))
|
assert_ne!(first.validator, second.validator);
|
||||||
.unwrap_err();
|
|
||||||
assert_eq!(error.kind(), CallErrorKind::InvalidResult);
|
|
||||||
assert_eq!(error.protocol_error().code, ProtocolErrorCode::Internal);
|
|
||||||
assert_eq!(publication.implementation().calls.get(), 1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn object_validator_missing_and_mismatch_are_distinct_and_prevent_execution() {
|
fn path_dispatch_is_interface_qualified_and_resolves_once_per_call() {
|
||||||
let mut registry = InterfaceRegistry::new();
|
let (_, provider, _) = registry_and_provider();
|
||||||
let reader = registry.register(ReaderDefinition, None).unwrap();
|
let before = provider.resolutions.get();
|
||||||
let publication = PublishedObject::new(Document::strict("strict", b"object-v1"))
|
|
||||||
.expose(&reader)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let missing = publication
|
let writer = provider
|
||||||
.dispatch(request(
|
.call_operation(request(
|
||||||
"strict",
|
"/open",
|
||||||
None,
|
None,
|
||||||
reader.reference(),
|
provider.writer.reference(),
|
||||||
None,
|
|
||||||
"label",
|
|
||||||
BTreeMap::new(),
|
|
||||||
))
|
|
||||||
.unwrap_err();
|
|
||||||
assert_eq!(missing.kind(), CallErrorKind::ValidatorRequired);
|
|
||||||
|
|
||||||
let mismatch = publication
|
|
||||||
.dispatch(request(
|
|
||||||
"strict",
|
|
||||||
Some(b"stale"),
|
|
||||||
reader.reference(),
|
|
||||||
None,
|
|
||||||
"label",
|
|
||||||
BTreeMap::new(),
|
|
||||||
))
|
|
||||||
.unwrap_err();
|
|
||||||
assert_eq!(mismatch.kind(), CallErrorKind::ValidatorMismatch);
|
|
||||||
assert_eq!(publication.implementation().calls.get(), 0);
|
|
||||||
|
|
||||||
let result = publication
|
|
||||||
.dispatch(request(
|
|
||||||
"strict",
|
|
||||||
Some(b"object-v1"),
|
|
||||||
reader.reference(),
|
|
||||||
None,
|
None,
|
||||||
"label",
|
"label",
|
||||||
BTreeMap::new(),
|
BTreeMap::new(),
|
||||||
))
|
))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(result.validator, Some(Validator::new(b"object-v1")));
|
let reader = provider
|
||||||
assert_eq!(publication.implementation().calls.get(), 1);
|
.call_operation(request(
|
||||||
}
|
"/open",
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn interface_validator_missing_and_mismatch_are_distinct_and_prevent_execution() {
|
|
||||||
let mut registry = InterfaceRegistry::new();
|
|
||||||
let writer = registry
|
|
||||||
.register(WriterDefinition, Some(Validator::new(b"schema-v1")))
|
|
||||||
.unwrap();
|
|
||||||
let publication = PublishedObject::new(Document::new("mutable-interface"))
|
|
||||||
.expose(&writer)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let observation = registry.observe(writer.reference()).unwrap();
|
|
||||||
assert_eq!(observation.validator, Some(Validator::new(b"schema-v1")));
|
|
||||||
|
|
||||||
let missing = publication
|
|
||||||
.dispatch(request(
|
|
||||||
"mutable-interface",
|
|
||||||
None,
|
None,
|
||||||
writer.reference(),
|
provider.reader.reference(),
|
||||||
None,
|
|
||||||
"label",
|
|
||||||
BTreeMap::new(),
|
|
||||||
))
|
|
||||||
.unwrap_err();
|
|
||||||
assert_eq!(missing.kind(), CallErrorKind::InterfaceValidatorRequired);
|
|
||||||
|
|
||||||
let mismatch = publication
|
|
||||||
.dispatch(request(
|
|
||||||
"mutable-interface",
|
|
||||||
None,
|
|
||||||
writer.reference(),
|
|
||||||
Some(b"schema-v0"),
|
|
||||||
"label",
|
|
||||||
BTreeMap::new(),
|
|
||||||
))
|
|
||||||
.unwrap_err();
|
|
||||||
assert_eq!(mismatch.kind(), CallErrorKind::InterfaceValidatorMismatch);
|
|
||||||
assert_eq!(publication.implementation().calls.get(), 0);
|
|
||||||
|
|
||||||
let result = publication
|
|
||||||
.dispatch(request(
|
|
||||||
"mutable-interface",
|
|
||||||
None,
|
|
||||||
writer.reference(),
|
|
||||||
Some(b"schema-v1"),
|
Some(b"schema-v1"),
|
||||||
"label",
|
"label",
|
||||||
BTreeMap::new(),
|
BTreeMap::new(),
|
||||||
))
|
))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(result.value, Value::String("writer".into()));
|
|
||||||
|
let strict = provider
|
||||||
|
.call_operation(request(
|
||||||
|
"/document",
|
||||||
|
Some(b"object-v1"),
|
||||||
|
provider.reader.reference(),
|
||||||
|
Some(b"schema-v1"),
|
||||||
|
"label",
|
||||||
|
BTreeMap::new(),
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(writer.result, Value::String("writer".into()));
|
||||||
|
assert_eq!(reader.result, Value::String("reader".into()));
|
||||||
|
assert_eq!(strict.result, Value::String("reader".into()));
|
||||||
|
assert_eq!(strict.validator.as_deref(), Some(b"object-v1".as_slice()));
|
||||||
|
assert_eq!(provider.resolutions.get() - before, 3);
|
||||||
|
assert_eq!(provider.calls.get(), 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn host_failures_map_to_internal_without_becoming_domain_values() {
|
fn call_failures_follow_canonical_precedence_without_execution() {
|
||||||
#[interface]
|
let (_, provider, hidden) = registry_and_provider();
|
||||||
pub trait Failing {
|
|
||||||
/// Always fails in the host implementation.
|
|
||||||
fn fail(&self) -> OperationResult<String>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Failing for Document {
|
assert_failure(
|
||||||
fn fail(&self) -> OperationResult<String> {
|
provider
|
||||||
Err(HostError::new("backend unavailable"))
|
.call_operation(request(
|
||||||
}
|
"/missing",
|
||||||
}
|
Some(b"stale"),
|
||||||
|
hidden.reference(),
|
||||||
|
Some(b"stale"),
|
||||||
|
"missing",
|
||||||
|
BTreeMap::new(),
|
||||||
|
))
|
||||||
|
.unwrap_err(),
|
||||||
|
CallErrorKind::NotFound,
|
||||||
|
ProtocolErrorCode::NotFound,
|
||||||
|
);
|
||||||
|
assert_failure(
|
||||||
|
provider
|
||||||
|
.call_operation(request(
|
||||||
|
"/document",
|
||||||
|
Some(b"stale"),
|
||||||
|
hidden.reference(),
|
||||||
|
None,
|
||||||
|
"missing",
|
||||||
|
BTreeMap::new(),
|
||||||
|
))
|
||||||
|
.unwrap_err(),
|
||||||
|
CallErrorKind::ValidatorMismatch,
|
||||||
|
ProtocolErrorCode::ValidatorMismatch,
|
||||||
|
);
|
||||||
|
assert_failure(
|
||||||
|
provider
|
||||||
|
.call_operation(request(
|
||||||
|
"/document",
|
||||||
|
Some(b"object-v1"),
|
||||||
|
hidden.reference(),
|
||||||
|
None,
|
||||||
|
"hidden",
|
||||||
|
BTreeMap::new(),
|
||||||
|
))
|
||||||
|
.unwrap_err(),
|
||||||
|
CallErrorKind::InterfaceMismatch,
|
||||||
|
ProtocolErrorCode::InterfaceMismatch,
|
||||||
|
);
|
||||||
|
assert_failure(
|
||||||
|
provider
|
||||||
|
.call_operation(request(
|
||||||
|
"/document",
|
||||||
|
Some(b"object-v1"),
|
||||||
|
provider.reader.reference(),
|
||||||
|
None,
|
||||||
|
"missing",
|
||||||
|
BTreeMap::new(),
|
||||||
|
))
|
||||||
|
.unwrap_err(),
|
||||||
|
CallErrorKind::InterfaceValidatorRequired,
|
||||||
|
ProtocolErrorCode::InterfaceValidatorRequired,
|
||||||
|
);
|
||||||
|
assert_failure(
|
||||||
|
provider
|
||||||
|
.call_operation(request(
|
||||||
|
"/document",
|
||||||
|
Some(b"object-v1"),
|
||||||
|
provider.reader.reference(),
|
||||||
|
Some(b"stale-schema"),
|
||||||
|
"missing",
|
||||||
|
BTreeMap::new(),
|
||||||
|
))
|
||||||
|
.unwrap_err(),
|
||||||
|
CallErrorKind::InterfaceValidatorMismatch,
|
||||||
|
ProtocolErrorCode::InterfaceValidatorMismatch,
|
||||||
|
);
|
||||||
|
assert_failure(
|
||||||
|
provider
|
||||||
|
.call_operation(request(
|
||||||
|
"/document",
|
||||||
|
None,
|
||||||
|
provider.reader.reference(),
|
||||||
|
Some(b"schema-v1"),
|
||||||
|
"missing",
|
||||||
|
BTreeMap::new(),
|
||||||
|
))
|
||||||
|
.unwrap_err(),
|
||||||
|
CallErrorKind::OperationNotFound,
|
||||||
|
ProtocolErrorCode::OperationNotFound,
|
||||||
|
);
|
||||||
|
assert_failure(
|
||||||
|
provider
|
||||||
|
.call_operation(request(
|
||||||
|
"/document",
|
||||||
|
None,
|
||||||
|
provider.reader.reference(),
|
||||||
|
Some(b"schema-v1"),
|
||||||
|
"add",
|
||||||
|
BTreeMap::new(),
|
||||||
|
))
|
||||||
|
.unwrap_err(),
|
||||||
|
CallErrorKind::ValidatorRequired,
|
||||||
|
ProtocolErrorCode::ValidatorRequired,
|
||||||
|
);
|
||||||
|
assert_failure(
|
||||||
|
provider
|
||||||
|
.call_operation(request(
|
||||||
|
"/document",
|
||||||
|
Some(b"object-v1"),
|
||||||
|
provider.reader.reference(),
|
||||||
|
Some(b"schema-v1"),
|
||||||
|
"add",
|
||||||
|
BTreeMap::from([("left".into(), Value::String("wrong".into()))]),
|
||||||
|
))
|
||||||
|
.unwrap_err(),
|
||||||
|
CallErrorKind::InvalidArguments,
|
||||||
|
ProtocolErrorCode::InvalidArguments,
|
||||||
|
);
|
||||||
|
|
||||||
let mut registry = InterfaceRegistry::new();
|
provider.deny.set(true);
|
||||||
let failing = registry.register(FailingDefinition, None).unwrap();
|
assert_failure(
|
||||||
let publication = PublishedObject::new(Document::new("failure"))
|
provider
|
||||||
.expose(&failing)
|
.call_operation(request(
|
||||||
.unwrap();
|
"/document",
|
||||||
let error = publication
|
Some(b"object-v1"),
|
||||||
.dispatch(request(
|
provider.reader.reference(),
|
||||||
"failure",
|
Some(b"schema-v1"),
|
||||||
|
"label",
|
||||||
|
BTreeMap::new(),
|
||||||
|
))
|
||||||
|
.unwrap_err(),
|
||||||
|
CallErrorKind::PermissionDenied,
|
||||||
|
ProtocolErrorCode::PermissionDenied,
|
||||||
|
);
|
||||||
|
assert_eq!(provider.authorization_checks.get(), 1);
|
||||||
|
assert_eq!(provider.calls.get(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn immutable_interface_rejects_a_supplied_validator() {
|
||||||
|
let (_, provider, _) = registry_and_provider();
|
||||||
|
assert_failure(
|
||||||
|
provider
|
||||||
|
.call_operation(request(
|
||||||
|
"/open",
|
||||||
None,
|
None,
|
||||||
failing.reference(),
|
provider.writer.reference(),
|
||||||
|
Some(b"unexpected"),
|
||||||
|
"label",
|
||||||
|
BTreeMap::new(),
|
||||||
|
))
|
||||||
|
.unwrap_err(),
|
||||||
|
CallErrorKind::InterfaceValidatorMismatch,
|
||||||
|
ProtocolErrorCode::InterfaceValidatorMismatch,
|
||||||
|
);
|
||||||
|
assert_eq!(provider.calls.get(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn invalid_results_and_backend_failures_keep_distinct_protocol_codes() {
|
||||||
|
let (_, provider, _) = registry_and_provider();
|
||||||
|
|
||||||
|
assert_failure(
|
||||||
|
provider
|
||||||
|
.call_operation(request(
|
||||||
|
"/broken",
|
||||||
None,
|
None,
|
||||||
|
provider.broken.reference(),
|
||||||
|
None,
|
||||||
|
"broken",
|
||||||
|
BTreeMap::new(),
|
||||||
|
))
|
||||||
|
.unwrap_err(),
|
||||||
|
CallErrorKind::InvalidResult,
|
||||||
|
ProtocolErrorCode::InvalidResult,
|
||||||
|
);
|
||||||
|
assert_failure(
|
||||||
|
provider
|
||||||
|
.call_operation(request(
|
||||||
|
"/open",
|
||||||
|
None,
|
||||||
|
provider.reader.reference(),
|
||||||
|
Some(b"schema-v1"),
|
||||||
"fail",
|
"fail",
|
||||||
BTreeMap::new(),
|
BTreeMap::new(),
|
||||||
))
|
))
|
||||||
.unwrap_err();
|
.unwrap_err(),
|
||||||
assert_eq!(error.kind(), CallErrorKind::Implementation);
|
CallErrorKind::Implementation,
|
||||||
assert_eq!(error.protocol_error().code, ProtocolErrorCode::Internal);
|
ProtocolErrorCode::Internal,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use wip_host::{InterfaceRegistry, ObjectId, PublishedObject, WipObject};
|
use wip_host::{InterfaceRegistry, PublishedObject, WipObject};
|
||||||
use wip_interface::{OperationResult, interface};
|
use wip_interface::{OperationResult, interface};
|
||||||
|
|
||||||
#[interface]
|
#[interface]
|
||||||
@@ -9,8 +9,8 @@ pub trait Search {
|
|||||||
struct Object;
|
struct Object;
|
||||||
|
|
||||||
impl WipObject for Object {
|
impl WipObject for Object {
|
||||||
fn id(&self) -> ObjectId {
|
fn name(&self) -> &str {
|
||||||
ObjectId("object".into())
|
"object"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -264,3 +264,15 @@ pub struct ProtocolError {
|
|||||||
/// Bounded human-readable diagnostic. Consumers must not branch on it.
|
/// Bounded human-readable diagnostic. Consumers must not branch on it.
|
||||||
pub message: String,
|
pub message: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Display for ProtocolError {
|
||||||
|
fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(
|
||||||
|
formatter,
|
||||||
|
"protocol failure ({:?}): {}",
|
||||||
|
self.code, self.message
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Error for ProtocolError {}
|
||||||
|
|||||||
+108
-365
@@ -1,24 +1,9 @@
|
|||||||
//! End-to-end use of the public WIP protocol, interface, and host APIs.
|
//! End-to-end use of the public WIP protocol, interface, and host APIs.
|
||||||
//!
|
//!
|
||||||
//! This workspace package is deliberately separate from the three library
|
//! This workspace package defines application DTOs and interfaces with
|
||||||
//! crates. It defines application DTOs and interfaces with `wip-interface`,
|
//! `wip-interface`, keeps its implementation private, publishes it through a
|
||||||
//! keeps its implementation private, publishes that implementation with
|
//! request-scoped `wip-host` provider, and crosses the call boundary only with
|
||||||
//! `wip-host`, and crosses the call boundary only with `wip-protocol` values.
|
//! canonical `wip-protocol` values and path targets.
|
||||||
//!
|
|
||||||
//! The complete scenario can be run as a normal binary or called directly:
|
|
||||||
//!
|
|
||||||
//! ```
|
|
||||||
//! let report = wip_public_api_example::run()?;
|
|
||||||
//!
|
|
||||||
//! assert_eq!(report.published_interfaces.len(), 2);
|
|
||||||
//! assert_eq!(report.catalog_status, "catalog-ready");
|
|
||||||
//! assert_eq!(report.audit_status, "audit-ready");
|
|
||||||
//! assert_eq!(
|
|
||||||
//! report.host_failure,
|
|
||||||
//! wip_host::CallErrorKind::Implementation,
|
|
||||||
//! );
|
|
||||||
//! # Ok::<(), Box<dyn std::error::Error>>(())
|
|
||||||
//! ```
|
|
||||||
|
|
||||||
#![deny(missing_docs)]
|
#![deny(missing_docs)]
|
||||||
|
|
||||||
@@ -27,14 +12,14 @@ use std::error::Error;
|
|||||||
use std::io;
|
use std::io;
|
||||||
|
|
||||||
use wip_host::{
|
use wip_host::{
|
||||||
CallErrorKind, CallRequest, InterfaceRef, InterfaceRegistry, InterfaceTarget, ObjectId,
|
CallErrorKind, CallRequest, InterfaceRef, InterfaceRegistry, InterfaceTarget, ObjectProvider,
|
||||||
PublishedObject, Validator, WipObject,
|
PublishedObject, RegisteredInterface, Target, Validator, WipObject,
|
||||||
};
|
};
|
||||||
use wip_interface as wip;
|
use wip_interface as wip;
|
||||||
use wip_interface::WipType;
|
use wip_interface::WipType;
|
||||||
use wip_protocol::ProtocolError;
|
use wip_protocol::ProtocolError;
|
||||||
|
|
||||||
const OBJECT_ID: &str = "article-service";
|
const OBJECT_PATH: &str = "/article-service";
|
||||||
const OBJECT_VALIDATOR: &[u8] = b"object-v1";
|
const OBJECT_VALIDATOR: &[u8] = b"object-v1";
|
||||||
const CATALOG_VALIDATOR: &[u8] = b"catalog-v1";
|
const CATALOG_VALIDATOR: &[u8] = b"catalog-v1";
|
||||||
|
|
||||||
@@ -48,9 +33,6 @@ pub struct PublishRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The application domain result of a publication attempt.
|
/// The application domain result of a publication attempt.
|
||||||
///
|
|
||||||
/// Rejection is an ordinary typed result. It is intentionally distinct from a
|
|
||||||
/// host or backend failure, which uses [`wip::HostError`].
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, wip::WipType)]
|
#[derive(Debug, Clone, PartialEq, Eq, wip::WipType)]
|
||||||
pub enum PublishOutcome {
|
pub enum PublishOutcome {
|
||||||
/// Publication succeeded and returned its application identifier.
|
/// Publication succeeded and returned its application identifier.
|
||||||
@@ -73,15 +55,9 @@ pub trait Catalog {
|
|||||||
) -> wip::OperationResult<PublishOutcome>;
|
) -> wip::OperationResult<PublishOutcome>;
|
||||||
|
|
||||||
/// Returns the catalog-qualified status.
|
/// Returns the catalog-qualified status.
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
/// A catalog status label.
|
|
||||||
fn status(&self) -> wip::OperationResult<String>;
|
fn status(&self) -> wip::OperationResult<String>;
|
||||||
|
|
||||||
/// Demonstrates a host/backend failure rather than a domain result.
|
/// Demonstrates a host/backend failure rather than a domain result.
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
/// A backend status only when the host can reach it.
|
|
||||||
fn backend_status(&self) -> wip::OperationResult<String>;
|
fn backend_status(&self) -> wip::OperationResult<String>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,22 +65,13 @@ pub trait Catalog {
|
|||||||
#[wip::interface]
|
#[wip::interface]
|
||||||
pub trait Audit {
|
pub trait Audit {
|
||||||
/// Returns the audit-qualified status.
|
/// Returns the audit-qualified status.
|
||||||
///
|
|
||||||
/// This deliberately has the same operation name as [`Catalog::status`].
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
/// An audit status label.
|
|
||||||
fn status(&self) -> wip::OperationResult<String>;
|
fn status(&self) -> wip::OperationResult<String>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A registered but deliberately unexposed interface used to demonstrate that
|
/// A registered but deliberately unexposed interface.
|
||||||
/// Rust trait implementation does not imply protocol membership.
|
|
||||||
#[wip::interface]
|
#[wip::interface]
|
||||||
pub trait Diagnostics {
|
pub trait Diagnostics {
|
||||||
/// Returns host-private diagnostics when the interface is explicitly exposed.
|
/// Returns host-private diagnostics when explicitly exposed.
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
/// A diagnostic label.
|
|
||||||
fn diagnostics(&self) -> wip::OperationResult<String>;
|
fn diagnostics(&self) -> wip::OperationResult<String>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,106 +100,76 @@ pub struct DemoReport {
|
|||||||
|
|
||||||
/// Runs the complete end-to-end example.
|
/// Runs the complete end-to-end example.
|
||||||
///
|
///
|
||||||
/// The function registers and observes generated descriptors, explicitly
|
/// The provider resolves the current object from a Worldspace path for each
|
||||||
/// exposes two interfaces on one host-private object, validates the resulting
|
/// call. Interface observations remain separate from object projection, and all
|
||||||
/// protocol observation, dispatches interface-qualified calls using named
|
/// calls carry independent object and interface validators.
|
||||||
/// [`wip_protocol::Value`] arguments, decodes validated values back to
|
|
||||||
/// application types, and keeps a domain rejection distinct from a host
|
|
||||||
/// failure.
|
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// Returns an error if registration, publication, observation validation,
|
/// Returns an error if registration, observation, path resolution, dispatch, or
|
||||||
/// dispatch, or application-value decoding fails unexpectedly.
|
/// application-value decoding fails unexpectedly.
|
||||||
pub fn run() -> Result<DemoReport, Box<dyn Error>> {
|
pub fn run() -> Result<DemoReport, Box<dyn Error>> {
|
||||||
let mut registry = InterfaceRegistry::new();
|
let mut registry = InterfaceRegistry::new();
|
||||||
let catalog = registry.register(CatalogDefinition, Some(Validator::new(CATALOG_VALIDATOR)))?;
|
let catalog = registry.register(CatalogDefinition, Some(Validator::new(CATALOG_VALIDATOR)))?;
|
||||||
let audit = registry.register(AuditDefinition, None)?;
|
let audit = registry.register(AuditDefinition, None)?;
|
||||||
let _diagnostics = registry.register(DiagnosticsDefinition, None)?;
|
let _diagnostics = registry.register(DiagnosticsDefinition, None)?;
|
||||||
|
let provider = ApplicationProvider {
|
||||||
|
catalog: catalog.clone(),
|
||||||
|
audit: audit.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
let publication = PublishedObject::new(ApplicationService::new())
|
let object = provider.fetch(OBJECT_PATH)?;
|
||||||
.expose(&catalog)?
|
let catalog_observation = registry.fetch_interface(
|
||||||
.expose(&audit)?;
|
object
|
||||||
let object = publication.protocol_object();
|
|
||||||
|
|
||||||
// Model a transport/provider that retained only the protocol object's
|
|
||||||
// opaque strings, then resolved them back through the registry's public API.
|
|
||||||
let catalog_wire_reference = object
|
|
||||||
.interfaces
|
.interfaces
|
||||||
.first()
|
.first()
|
||||||
.ok_or_else(|| io::Error::other("catalog interface was not published"))?;
|
.ok_or_else(|| io::Error::other("catalog interface was not published"))?,
|
||||||
let audit_wire_reference = object
|
)?;
|
||||||
|
let audit_observation = registry.fetch_interface(
|
||||||
|
object
|
||||||
.interfaces
|
.interfaces
|
||||||
.get(1)
|
.get(1)
|
||||||
.ok_or_else(|| io::Error::other("audit interface was not published"))?;
|
.ok_or_else(|| io::Error::other("audit interface was not published"))?,
|
||||||
let catalog_reference = registry
|
)?;
|
||||||
.resolve(catalog_wire_reference)
|
|
||||||
.ok_or_else(|| io::Error::other("published catalog reference did not resolve"))?;
|
|
||||||
let audit_reference = registry
|
|
||||||
.resolve(audit_wire_reference)
|
|
||||||
.ok_or_else(|| io::Error::other("published audit reference did not resolve"))?;
|
|
||||||
let catalog_observation = registry
|
|
||||||
.observe(&catalog_reference)
|
|
||||||
.ok_or_else(|| io::Error::other("registered catalog descriptor was not observable"))?;
|
|
||||||
let audit_observation = registry
|
|
||||||
.observe(&audit_reference)
|
|
||||||
.ok_or_else(|| io::Error::other("registered audit descriptor was not observable"))?;
|
|
||||||
|
|
||||||
catalog_observation.descriptor.validate()?;
|
|
||||||
audit_observation.descriptor.validate()?;
|
|
||||||
|
|
||||||
let published = dispatch_publish(
|
let published = dispatch_publish(
|
||||||
&publication,
|
&provider,
|
||||||
&catalog_observation.reference,
|
catalog.reference(),
|
||||||
PublishRequest {
|
PublishRequest {
|
||||||
title: "Public APIs".into(),
|
title: "Public APIs".into(),
|
||||||
body: Some("protocol values in, validated values out".into()),
|
body: Some("protocol values in, validated values out".into()),
|
||||||
},
|
},
|
||||||
)?;
|
)?;
|
||||||
let rejected = dispatch_publish(
|
let rejected = dispatch_publish(
|
||||||
&publication,
|
&provider,
|
||||||
&catalog_observation.reference,
|
catalog.reference(),
|
||||||
PublishRequest {
|
PublishRequest {
|
||||||
title: String::new(),
|
title: String::new(),
|
||||||
body: None,
|
body: None,
|
||||||
},
|
},
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let catalog_status = dispatch_string(
|
let catalog_status = dispatch_string(
|
||||||
&publication,
|
&provider,
|
||||||
request(
|
request(
|
||||||
catalog_observation.reference.clone(),
|
catalog.reference(),
|
||||||
Some(Validator::new(CATALOG_VALIDATOR)),
|
Some(CATALOG_VALIDATOR),
|
||||||
"status",
|
"status",
|
||||||
BTreeMap::new(),
|
BTreeMap::new(),
|
||||||
),
|
),
|
||||||
"catalog status",
|
|
||||||
)?;
|
)?;
|
||||||
let audit_status = dispatch_string(
|
let audit_status = dispatch_string(
|
||||||
&publication,
|
&provider,
|
||||||
request(
|
request(audit.reference(), None, "status", BTreeMap::new()),
|
||||||
audit_observation.reference.clone(),
|
|
||||||
None,
|
|
||||||
"status",
|
|
||||||
BTreeMap::new(),
|
|
||||||
),
|
|
||||||
"audit status",
|
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let host_error = match publication.dispatch(request(
|
let host_error = provider
|
||||||
catalog_observation.reference.clone(),
|
.call_operation(request(
|
||||||
Some(Validator::new(CATALOG_VALIDATOR)),
|
catalog.reference(),
|
||||||
|
Some(CATALOG_VALIDATOR),
|
||||||
"backend_status",
|
"backend_status",
|
||||||
BTreeMap::new(),
|
BTreeMap::new(),
|
||||||
)) {
|
))
|
||||||
Ok(_) => {
|
.unwrap_err();
|
||||||
return Err(io::Error::other(
|
|
||||||
"the deliberately unavailable example backend unexpectedly succeeded",
|
|
||||||
)
|
|
||||||
.into());
|
|
||||||
}
|
|
||||||
Err(error) => error,
|
|
||||||
};
|
|
||||||
if host_error.kind() != CallErrorKind::Implementation {
|
if host_error.kind() != CallErrorKind::Implementation {
|
||||||
return Err(io::Error::other(format!(
|
return Err(io::Error::other(format!(
|
||||||
"unexpected host failure kind: {:?}",
|
"unexpected host failure kind: {:?}",
|
||||||
@@ -256,49 +193,73 @@ pub fn run() -> Result<DemoReport, Box<dyn Error>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn dispatch_publish(
|
fn dispatch_publish(
|
||||||
publication: &PublishedObject<ApplicationService>,
|
provider: &ApplicationProvider,
|
||||||
catalog: &InterfaceRef,
|
catalog: &InterfaceRef,
|
||||||
request_value: PublishRequest,
|
request_value: PublishRequest,
|
||||||
) -> Result<PublishOutcome, Box<dyn Error>> {
|
) -> Result<PublishOutcome, Box<dyn Error>> {
|
||||||
let encoded_request = request_value.encode()?;
|
let encoded_request = request_value.encode()?;
|
||||||
let result = publication.dispatch(request(
|
let result = provider.call_operation(request(
|
||||||
catalog.clone(),
|
catalog,
|
||||||
Some(Validator::new(CATALOG_VALIDATOR)),
|
Some(CATALOG_VALIDATOR),
|
||||||
"publish",
|
"publish",
|
||||||
BTreeMap::from([("request".into(), encoded_request)]),
|
BTreeMap::from([("request".into(), encoded_request)]),
|
||||||
))?;
|
))?;
|
||||||
let value = result.value;
|
Ok(PublishOutcome::decode(result.result)?)
|
||||||
Ok(PublishOutcome::decode(value)?)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dispatch_string(
|
fn dispatch_string(
|
||||||
publication: &PublishedObject<ApplicationService>,
|
provider: &ApplicationProvider,
|
||||||
request: CallRequest,
|
request: CallRequest,
|
||||||
_context: &str,
|
|
||||||
) -> Result<String, Box<dyn Error>> {
|
) -> Result<String, Box<dyn Error>> {
|
||||||
let result = publication.dispatch(request)?;
|
let result = provider.call_operation(request)?;
|
||||||
let value = result.value;
|
Ok(String::decode(result.result)?)
|
||||||
Ok(String::decode(value)?)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn request(
|
fn request(
|
||||||
interface: InterfaceRef,
|
interface: &InterfaceRef,
|
||||||
interface_validator: Option<Validator>,
|
interface_validator: Option<&[u8]>,
|
||||||
operation: &str,
|
operation: &str,
|
||||||
arguments: BTreeMap<String, wip_protocol::Value>,
|
arguments: BTreeMap<String, wip_protocol::Value>,
|
||||||
) -> CallRequest {
|
) -> CallRequest {
|
||||||
CallRequest {
|
CallRequest {
|
||||||
object: ObjectId(OBJECT_ID.into()),
|
target: Target {
|
||||||
object_validator: Some(Validator::new(OBJECT_VALIDATOR)),
|
path: OBJECT_PATH.into(),
|
||||||
|
validator: Some(OBJECT_VALIDATOR.to_vec()),
|
||||||
|
},
|
||||||
interface: InterfaceTarget {
|
interface: InterfaceTarget {
|
||||||
reference: interface,
|
reference: interface.as_str().to_owned(),
|
||||||
validator: interface_validator,
|
validator: interface_validator.map(<[u8]>::to_vec),
|
||||||
},
|
},
|
||||||
operation: operation.into(),
|
operation: operation.into(),
|
||||||
arguments,
|
arguments,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct ApplicationProvider {
|
||||||
|
catalog: RegisteredInterface<CatalogDefinition>,
|
||||||
|
audit: RegisteredInterface<AuditDefinition>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ObjectProvider for ApplicationProvider {
|
||||||
|
type Object = ApplicationService;
|
||||||
|
|
||||||
|
fn with_object<R>(
|
||||||
|
&self,
|
||||||
|
path: &str,
|
||||||
|
operation: impl FnOnce(&PublishedObject<Self::Object>) -> R,
|
||||||
|
) -> Option<R> {
|
||||||
|
if path != OBJECT_PATH {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let publication = PublishedObject::new(ApplicationService::new())
|
||||||
|
.expose(&self.catalog)
|
||||||
|
.expect("ApplicationService implements Catalog")
|
||||||
|
.expose(&self.audit)
|
||||||
|
.expect("ApplicationService implements Audit");
|
||||||
|
Some(operation(&publication))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct ApplicationService {
|
struct ApplicationService {
|
||||||
object_validator: Validator,
|
object_validator: Validator,
|
||||||
backend_connection: String,
|
backend_connection: String,
|
||||||
@@ -314,15 +275,23 @@ impl ApplicationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl WipObject for ApplicationService {
|
impl WipObject for ApplicationService {
|
||||||
fn id(&self) -> ObjectId {
|
fn name(&self) -> &str {
|
||||||
ObjectId(OBJECT_ID.into())
|
"article-service"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn description(&self) -> Option<&str> {
|
||||||
|
Some("Article publication service")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reference(&self) -> Option<&str> {
|
||||||
|
Some("article-service-instance")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validator(&self) -> Option<&Validator> {
|
fn validator(&self) -> Option<&Validator> {
|
||||||
Some(&self.object_validator)
|
Some(&self.object_validator)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn requires_validator(&self) -> bool {
|
fn requires_validator(&self, _: &InterfaceRef, _: &str) -> bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -364,254 +333,28 @@ impl Diagnostics for ApplicationService {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use wip_host::CallError;
|
use wip_protocol::ProtocolErrorCode;
|
||||||
use wip_interface::{CodecError, TypeDeclaration, TypeExpr};
|
|
||||||
use wip_protocol::{ProtocolErrorCode, Value};
|
|
||||||
|
|
||||||
/// A deliberately inconsistent custom codec used to prove result validation.
|
|
||||||
pub struct InvalidValue;
|
|
||||||
|
|
||||||
impl WipType for InvalidValue {
|
|
||||||
fn type_expr() -> TypeExpr {
|
|
||||||
TypeExpr::String
|
|
||||||
}
|
|
||||||
|
|
||||||
fn collect_declarations(_: &mut Vec<TypeDeclaration>) {}
|
|
||||||
|
|
||||||
fn encode(self) -> Result<Value, CodecError> {
|
|
||||||
Ok(Value::Integer(7))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn decode(_: Value) -> Result<Self, CodecError> {
|
|
||||||
Ok(Self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Test-only interface whose custom codec emits an invalid result.
|
|
||||||
#[wip::interface]
|
|
||||||
pub trait BrokenResult {
|
|
||||||
/// Returns a value that violates its declared protocol type.
|
|
||||||
fn broken_result(&self) -> wip::OperationResult<InvalidValue>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BrokenResult for ApplicationService {
|
|
||||||
fn broken_result(&self) -> wip::OperationResult<InvalidValue> {
|
|
||||||
Ok(InvalidValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Fixture {
|
|
||||||
publication: PublishedObject<ApplicationService>,
|
|
||||||
catalog: InterfaceRef,
|
|
||||||
audit: InterfaceRef,
|
|
||||||
diagnostics: InterfaceRef,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn fixture() -> Fixture {
|
|
||||||
let mut registry = InterfaceRegistry::new();
|
|
||||||
let catalog = registry
|
|
||||||
.register(CatalogDefinition, Some(Validator::new(CATALOG_VALIDATOR)))
|
|
||||||
.unwrap();
|
|
||||||
let audit = registry.register(AuditDefinition, None).unwrap();
|
|
||||||
let diagnostics = registry.register(DiagnosticsDefinition, None).unwrap();
|
|
||||||
let publication = PublishedObject::new(ApplicationService::new())
|
|
||||||
.expose(&catalog)
|
|
||||||
.unwrap()
|
|
||||||
.expose(&audit)
|
|
||||||
.unwrap();
|
|
||||||
Fixture {
|
|
||||||
publication,
|
|
||||||
catalog: catalog.reference().clone(),
|
|
||||||
audit: audit.reference().clone(),
|
|
||||||
diagnostics: diagnostics.reference().clone(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn assert_failure(error: CallError, kind: CallErrorKind, code: ProtocolErrorCode) {
|
|
||||||
assert_eq!(error.kind(), kind);
|
|
||||||
assert_eq!(error.protocol_error().code, code);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn interface_membership_and_same_name_operations_are_qualified() {
|
fn unexposed_interfaces_are_membership_failures() {
|
||||||
let fixture = fixture();
|
let mut registry = InterfaceRegistry::new();
|
||||||
|
let catalog = registry.register(CatalogDefinition, None).unwrap();
|
||||||
|
let audit = registry.register(AuditDefinition, None).unwrap();
|
||||||
|
let diagnostics = registry.register(DiagnosticsDefinition, None).unwrap();
|
||||||
|
let provider = ApplicationProvider { catalog, audit };
|
||||||
|
|
||||||
let catalog = dispatch_string(
|
let error = provider
|
||||||
&fixture.publication,
|
.call_operation(request(
|
||||||
request(
|
diagnostics.reference(),
|
||||||
fixture.catalog.clone(),
|
|
||||||
Some(Validator::new(CATALOG_VALIDATOR)),
|
|
||||||
"status",
|
|
||||||
BTreeMap::new(),
|
|
||||||
),
|
|
||||||
"catalog status",
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
let audit = dispatch_string(
|
|
||||||
&fixture.publication,
|
|
||||||
request(fixture.audit.clone(), None, "status", BTreeMap::new()),
|
|
||||||
"audit status",
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(catalog, "catalog-ready");
|
|
||||||
assert_eq!(audit, "audit-ready");
|
|
||||||
|
|
||||||
let not_exposed = fixture
|
|
||||||
.publication
|
|
||||||
.dispatch(request(
|
|
||||||
fixture.diagnostics,
|
|
||||||
None,
|
None,
|
||||||
"diagnostics",
|
"diagnostics",
|
||||||
BTreeMap::new(),
|
BTreeMap::new(),
|
||||||
))
|
))
|
||||||
.unwrap_err();
|
.unwrap_err();
|
||||||
assert_failure(
|
assert_eq!(error.kind(), CallErrorKind::InterfaceMismatch);
|
||||||
not_exposed,
|
assert_eq!(
|
||||||
CallErrorKind::InterfaceMismatch,
|
error.protocol_error().code,
|
||||||
ProtocolErrorCode::InterfaceMismatch,
|
ProtocolErrorCode::InterfaceMismatch
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn every_host_precondition_and_dispatch_failure_is_typed() {
|
|
||||||
let fixture = fixture();
|
|
||||||
|
|
||||||
let mut wrong_object = request(
|
|
||||||
fixture.catalog.clone(),
|
|
||||||
Some(Validator::new(CATALOG_VALIDATOR)),
|
|
||||||
"status",
|
|
||||||
BTreeMap::new(),
|
|
||||||
);
|
|
||||||
wrong_object.object = ObjectId("other-object".into());
|
|
||||||
assert_failure(
|
|
||||||
fixture.publication.dispatch(wrong_object).unwrap_err(),
|
|
||||||
CallErrorKind::ObjectNotFound,
|
|
||||||
ProtocolErrorCode::NotFound,
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut missing_object_validator = request(
|
|
||||||
fixture.catalog.clone(),
|
|
||||||
Some(Validator::new(CATALOG_VALIDATOR)),
|
|
||||||
"status",
|
|
||||||
BTreeMap::new(),
|
|
||||||
);
|
|
||||||
missing_object_validator.object_validator = None;
|
|
||||||
assert_failure(
|
|
||||||
fixture
|
|
||||||
.publication
|
|
||||||
.dispatch(missing_object_validator)
|
|
||||||
.unwrap_err(),
|
|
||||||
CallErrorKind::ValidatorRequired,
|
|
||||||
ProtocolErrorCode::InvalidRequest,
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut stale_object_validator = request(
|
|
||||||
fixture.catalog.clone(),
|
|
||||||
Some(Validator::new(CATALOG_VALIDATOR)),
|
|
||||||
"status",
|
|
||||||
BTreeMap::new(),
|
|
||||||
);
|
|
||||||
stale_object_validator.object_validator = Some(Validator::new(b"stale-object"));
|
|
||||||
assert_failure(
|
|
||||||
fixture
|
|
||||||
.publication
|
|
||||||
.dispatch(stale_object_validator)
|
|
||||||
.unwrap_err(),
|
|
||||||
CallErrorKind::ValidatorMismatch,
|
|
||||||
ProtocolErrorCode::InvalidRequest,
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_failure(
|
|
||||||
fixture
|
|
||||||
.publication
|
|
||||||
.dispatch(request(
|
|
||||||
fixture.catalog.clone(),
|
|
||||||
None,
|
|
||||||
"status",
|
|
||||||
BTreeMap::new(),
|
|
||||||
))
|
|
||||||
.unwrap_err(),
|
|
||||||
CallErrorKind::InterfaceValidatorRequired,
|
|
||||||
ProtocolErrorCode::InvalidRequest,
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_failure(
|
|
||||||
fixture
|
|
||||||
.publication
|
|
||||||
.dispatch(request(
|
|
||||||
fixture.catalog.clone(),
|
|
||||||
Some(Validator::new(b"stale-interface")),
|
|
||||||
"status",
|
|
||||||
BTreeMap::new(),
|
|
||||||
))
|
|
||||||
.unwrap_err(),
|
|
||||||
CallErrorKind::InterfaceValidatorMismatch,
|
|
||||||
ProtocolErrorCode::InvalidRequest,
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_failure(
|
|
||||||
fixture
|
|
||||||
.publication
|
|
||||||
.dispatch(request(
|
|
||||||
fixture.catalog.clone(),
|
|
||||||
Some(Validator::new(CATALOG_VALIDATOR)),
|
|
||||||
"missing",
|
|
||||||
BTreeMap::new(),
|
|
||||||
))
|
|
||||||
.unwrap_err(),
|
|
||||||
CallErrorKind::OperationNotFound,
|
|
||||||
ProtocolErrorCode::OperationNotFound,
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_failure(
|
|
||||||
fixture
|
|
||||||
.publication
|
|
||||||
.dispatch(request(
|
|
||||||
fixture.catalog.clone(),
|
|
||||||
Some(Validator::new(CATALOG_VALIDATOR)),
|
|
||||||
"publish",
|
|
||||||
BTreeMap::from([("request".into(), Value::String("not a record".into()))]),
|
|
||||||
))
|
|
||||||
.unwrap_err(),
|
|
||||||
CallErrorKind::InvalidArguments,
|
|
||||||
ProtocolErrorCode::InvalidArguments,
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_failure(
|
|
||||||
fixture
|
|
||||||
.publication
|
|
||||||
.dispatch(request(
|
|
||||||
fixture.catalog,
|
|
||||||
Some(Validator::new(CATALOG_VALIDATOR)),
|
|
||||||
"backend_status",
|
|
||||||
BTreeMap::new(),
|
|
||||||
))
|
|
||||||
.unwrap_err(),
|
|
||||||
CallErrorKind::Implementation,
|
|
||||||
ProtocolErrorCode::Internal,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn invalid_implementation_result_never_crosses_the_host_boundary() {
|
|
||||||
let mut registry = InterfaceRegistry::new();
|
|
||||||
let broken = registry.register(BrokenResultDefinition, None).unwrap();
|
|
||||||
let publication = PublishedObject::new(ApplicationService::new())
|
|
||||||
.expose(&broken)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let error = publication
|
|
||||||
.dispatch(request(
|
|
||||||
broken.reference().clone(),
|
|
||||||
None,
|
|
||||||
"broken_result",
|
|
||||||
BTreeMap::new(),
|
|
||||||
))
|
|
||||||
.unwrap_err();
|
|
||||||
assert_failure(
|
|
||||||
error,
|
|
||||||
CallErrorKind::InvalidResult,
|
|
||||||
ProtocolErrorCode::Internal,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user