feat: bind Host interfaces to scope Object lifetimes
Register explicit scope/name coordinates independently of Definition type. Require provider-attested current scope snapshots for fetch and dispatch, compare optional scope_ref at membership precedence, and retain the same Descriptor through result validation. Migrate FS pinned-root boundaries and public self-scope example; document provider atomicity/lifecycle and root-only FS support. Add lifecycle, ref, replacement-race, root-lineage and compile-time exposure regressions. Validation: cargo fmt --all --check; git diff --check; cargo check --workspace --all-targets; cargo test --workspace --all-targets and --doc (232 passing test-harness cases including doctests); nix develop --command cargo clippy --workspace --all-targets -- -D warnings; RUSTDOCFLAGS=-Dwarnings cargo doc --workspace --no-deps. Narrow wip-host suite also passed.
This commit is contained in:
+312
-115
@@ -3,7 +3,7 @@
|
||||
//! # Responsibility boundaries
|
||||
//!
|
||||
//! [`InterfaceRegistry::register`] validates and freezes a generated interface
|
||||
//! descriptor, assigns an opaque [`InterfaceRef`], and leaves that reference
|
||||
//! descriptor at an explicit scope and local name, and leaves that reference
|
||||
//! outside the descriptor. [`PublishedObject::expose`] explicitly combines a
|
||||
//! request-scoped Rust object with a registered interface; its
|
||||
//! [`InterfaceImplementation`] bound proves at compile time that the object
|
||||
@@ -11,7 +11,8 @@
|
||||
//! and a publication with no interfaces is valid.
|
||||
//!
|
||||
//! [`ObjectProvider::call_operation`] resolves `target.path` exactly once and
|
||||
//! keeps the resulting object, interface membership, descriptor, implementation,
|
||||
//! keeps the resulting object, current scope Object association/ref, interface
|
||||
//! membership, descriptor, implementation,
|
||||
//! and backend target fixed within the provider's [`ObjectProvider::with_object`]
|
||||
//! callback. Dispatch is qualified by `(interface reference, operation name)`.
|
||||
//! The same frozen operation declaration validates both arguments and results.
|
||||
@@ -29,14 +30,13 @@
|
||||
|
||||
#![deny(missing_docs)]
|
||||
|
||||
use std::any::TypeId;
|
||||
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||
use std::error::Error;
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
use std::marker::PhantomData;
|
||||
use std::panic::{AssertUnwindSafe, catch_unwind};
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::time::Instant;
|
||||
|
||||
use wip_interface::DispatchError;
|
||||
@@ -187,36 +187,104 @@ impl ExecutionContext {
|
||||
}
|
||||
}
|
||||
|
||||
/// Opaque host-assigned reference to one registered interface artifact.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct InterfaceRef(String);
|
||||
/// One publication lifetime of the Object at a canonical scope path.
|
||||
///
|
||||
/// Clone while the same Object remains published, including state updates. Create
|
||||
/// a new handle after deletion, replacement or movement, even with the same ref.
|
||||
/// This private lifetime is not a Protocol identity component or generated ref.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Scope {
|
||||
path: String,
|
||||
lifetime: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl InterfaceRef {
|
||||
/// Returns the legacy opaque registration reference.
|
||||
#[must_use]
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
impl Scope {
|
||||
/// Creates a fresh lifetime without implicitly publishing an Object.
|
||||
pub fn new(path: impl Into<String>) -> Result<Self, ValidationError> {
|
||||
let path = path.into();
|
||||
wip_protocol::validate_path(&path)?;
|
||||
Ok(Self {
|
||||
path,
|
||||
lifetime: Arc::new(AtomicBool::new(true)),
|
||||
})
|
||||
}
|
||||
|
||||
/// Maps the legacy registration reference to a root-scoped protocol reference.
|
||||
///
|
||||
/// The opaque registration string is copied as the name without parsing it.
|
||||
/// Returns the client-facing canonical scope path.
|
||||
#[must_use]
|
||||
pub fn path(&self) -> &str {
|
||||
&self.path
|
||||
}
|
||||
|
||||
fn same_lifetime(&self, other: &Self) -> bool {
|
||||
self.path == other.path && Arc::ptr_eq(&self.lifetime, &other.lifetime)
|
||||
}
|
||||
|
||||
fn active(&self) -> bool {
|
||||
self.lifetime.load(Ordering::Acquire)
|
||||
}
|
||||
}
|
||||
|
||||
/// Provider-attested current scope Object association.
|
||||
///
|
||||
/// Construct only inside a coherent boundary that proves this lifetime belongs
|
||||
/// to the Object at `scope.path()`. A prelookup or matching ref alone is not proof.
|
||||
/// The Object need not expose interfaces. Its validator is not scope identity.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ScopeObservation {
|
||||
scope: Scope,
|
||||
reference: Option<String>,
|
||||
}
|
||||
|
||||
impl ScopeObservation {
|
||||
/// Attests the association and captures the current Object's optional ref.
|
||||
#[must_use]
|
||||
pub fn new(scope: &Scope, object: &impl WipObject) -> Self {
|
||||
Self {
|
||||
scope: scope.clone(),
|
||||
reference: object.reference().map(str::to_owned),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the observed optional ref.
|
||||
#[must_use]
|
||||
pub fn reference(&self) -> Option<&str> {
|
||||
self.reference.as_deref()
|
||||
}
|
||||
|
||||
/// Returns the associated scope publication lifetime.
|
||||
#[must_use]
|
||||
pub fn scope(&self) -> &Scope {
|
||||
&self.scope
|
||||
}
|
||||
}
|
||||
|
||||
/// Structured client-facing reference to a scoped Interface publication.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct InterfaceRef(InterfaceReference);
|
||||
|
||||
impl InterfaceRef {
|
||||
/// Returns the local name without parsing or adding display delimiters.
|
||||
#[must_use]
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0.name
|
||||
}
|
||||
|
||||
/// Returns the structured Protocol reference.
|
||||
#[must_use]
|
||||
pub fn protocol_reference(&self) -> InterfaceReference {
|
||||
InterfaceReference {
|
||||
scope: "/".into(),
|
||||
name: self.0.clone(),
|
||||
}
|
||||
self.0.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// One atomic observation of an interface reference, descriptor, and validator.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct InterfaceObservation {
|
||||
/// Opaque reference selected by published objects and operation calls.
|
||||
/// Structured reference selected by published objects and operation calls.
|
||||
pub reference: InterfaceRef,
|
||||
/// Canonical descriptor frozen when the interface was registered.
|
||||
pub descriptor: InterfaceDescriptor,
|
||||
/// Optional ref of the scope Object from this same observation.
|
||||
pub scope_ref: Option<String>,
|
||||
/// Optional validator. Its presence makes it a required call precondition.
|
||||
pub validator: Option<Validator>,
|
||||
}
|
||||
@@ -242,7 +310,7 @@ impl InterfaceObservation {
|
||||
let response = FetchInterfaceResponse {
|
||||
interface: self.reference.protocol_reference(),
|
||||
descriptor: self.descriptor.clone(),
|
||||
scope_ref: None,
|
||||
scope_ref: self.scope_ref.clone(),
|
||||
validator: self.validator.as_ref().map(|validator| validator.0.clone()),
|
||||
};
|
||||
response
|
||||
@@ -259,6 +327,7 @@ impl InterfaceObservation {
|
||||
|
||||
#[derive(Debug)]
|
||||
struct InterfaceArtifact {
|
||||
scope: Scope,
|
||||
reference: InterfaceRef,
|
||||
descriptor: InterfaceDescriptor,
|
||||
validator: Option<Validator>,
|
||||
@@ -267,7 +336,7 @@ struct InterfaceArtifact {
|
||||
/// A typed registration token for a generated interface definition.
|
||||
///
|
||||
/// Tokens can be reused by any number of request-scoped objects. They expose an
|
||||
/// opaque reference, not the Rust type name or an address.
|
||||
/// scoped reference and a private publication lifetime, not a Rust address.
|
||||
pub struct RegisteredInterface<D> {
|
||||
artifact: Arc<InterfaceArtifact>,
|
||||
definition: PhantomData<fn() -> D>,
|
||||
@@ -283,170 +352,161 @@ impl<D> Clone for RegisteredInterface<D> {
|
||||
}
|
||||
|
||||
impl<D> RegisteredInterface<D> {
|
||||
/// Returns the opaque protocol reference assigned by the registry.
|
||||
/// Returns the explicit scope/name reference.
|
||||
#[must_use]
|
||||
pub fn reference(&self) -> &InterfaceRef {
|
||||
&self.artifact.reference
|
||||
}
|
||||
}
|
||||
|
||||
/// Registry of validated, immutable interface artifacts.
|
||||
/// Registry of frozen definition artifacts and explicitly scoped registrations.
|
||||
///
|
||||
/// Generated descriptors are never rewritten to contain their assigned
|
||||
/// references. A validator-free artifact is immutable for the lifetime of its
|
||||
/// reference. A validator-bearing artifact requires that exact validator on a
|
||||
/// call. Re-registering a definition is rejected rather than silently changing
|
||||
/// the observation associated with an existing reference.
|
||||
#[derive(Debug)]
|
||||
/// Artifact retention is not proof of current publication. Fetch requires a live
|
||||
/// provider-attested ScopeObservation; execution requires the same lifetime in
|
||||
/// the target's atomic boundary. Collision keys are exact (scope, name) pairs,
|
||||
/// not Rust TypeId: the same Definition can be registered at multiple coordinates.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct InterfaceRegistry {
|
||||
namespace: u64,
|
||||
next_reference: u64,
|
||||
definitions: HashSet<TypeId>,
|
||||
artifacts: HashMap<InterfaceRef, Arc<InterfaceArtifact>>,
|
||||
}
|
||||
|
||||
static NEXT_REGISTRY_NAMESPACE: AtomicU64 = AtomicU64::new(1);
|
||||
|
||||
impl Default for InterfaceRegistry {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
namespace: NEXT_REGISTRY_NAMESPACE.fetch_add(1, Ordering::Relaxed),
|
||||
next_reference: 0,
|
||||
definitions: HashSet::new(),
|
||||
artifacts: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl InterfaceRegistry {
|
||||
/// Creates an empty interface registry.
|
||||
/// Creates an empty artifact registry without publishing any Object.
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Registers and freezes one generated interface definition.
|
||||
/// Freezes a Definition at an explicit scope and local name.
|
||||
pub fn register<D>(
|
||||
&mut self,
|
||||
scope: &Scope,
|
||||
name: impl Into<String>,
|
||||
_definition: D,
|
||||
validator: Option<Validator>,
|
||||
) -> Result<RegisteredInterface<D>, RegistrationError>
|
||||
where
|
||||
D: InterfaceDefinition + 'static,
|
||||
{
|
||||
let definition = TypeId::of::<D>();
|
||||
if self.definitions.contains(&definition) {
|
||||
let reference = InterfaceRef(InterfaceReference {
|
||||
scope: scope.path.clone(),
|
||||
name: name.into(),
|
||||
});
|
||||
reference
|
||||
.0
|
||||
.validate()
|
||||
.map_err(RegistrationError::InvalidReference)?;
|
||||
if !scope.active() {
|
||||
return Err(RegistrationError::RetiredScope);
|
||||
}
|
||||
if self.artifacts.contains_key(&reference) {
|
||||
return Err(RegistrationError::AlreadyRegistered);
|
||||
}
|
||||
|
||||
let descriptor = D::descriptor();
|
||||
descriptor
|
||||
.validate()
|
||||
.map_err(RegistrationError::InvalidDescriptor)?;
|
||||
|
||||
let reference = InterfaceRef(format!(
|
||||
"wip-interface:{}:{}",
|
||||
self.namespace, self.next_reference
|
||||
));
|
||||
self.next_reference = self
|
||||
.next_reference
|
||||
.checked_add(1)
|
||||
.ok_or(RegistrationError::ReferenceSpaceExhausted)?;
|
||||
let artifact = Arc::new(InterfaceArtifact {
|
||||
scope: scope.clone(),
|
||||
reference: reference.clone(),
|
||||
descriptor,
|
||||
validator,
|
||||
});
|
||||
self.definitions.insert(definition);
|
||||
self.artifacts.insert(reference, Arc::clone(&artifact));
|
||||
|
||||
Ok(RegisteredInterface {
|
||||
artifact,
|
||||
definition: PhantomData,
|
||||
})
|
||||
}
|
||||
|
||||
/// Resolves a structured protocol reference to a legacy registration token.
|
||||
/// Ends a scope lifetime and releases its registry entries.
|
||||
///
|
||||
/// Legacy artifacts are published only at root scope. The name is looked up
|
||||
/// verbatim; no scope or name parsing is performed.
|
||||
/// Retained tokens cannot start new calls/fetches. Started calls retain their
|
||||
/// frozen Descriptor. Re-publication requires a fresh Scope.
|
||||
pub fn remove_scope(&mut self, scope: &Scope) {
|
||||
scope.lifetime.store(false, Ordering::Release);
|
||||
self.artifacts
|
||||
.retain(|_, artifact| !artifact.scope.same_lifetime(scope));
|
||||
}
|
||||
|
||||
/// Resolves a stored artifact, not evidence of current publication.
|
||||
#[must_use]
|
||||
pub fn resolve(&self, reference: &InterfaceReference) -> Option<InterfaceRef> {
|
||||
if reference.scope != "/" {
|
||||
return None;
|
||||
}
|
||||
let reference = InterfaceRef(reference.name.clone());
|
||||
let reference = InterfaceRef(reference.clone());
|
||||
self.artifacts.contains_key(&reference).then_some(reference)
|
||||
}
|
||||
|
||||
/// Observes a registered interface by its host-side reference.
|
||||
#[must_use]
|
||||
pub fn observe_object(&self, reference: &InterfaceRef) -> Option<InterfaceObservation> {
|
||||
self.artifacts.get(reference).map(Self::observation)
|
||||
/// Iterates registered scope handles, possibly repeated for multiple names.
|
||||
pub fn scopes(&self) -> impl Iterator<Item = &Scope> {
|
||||
self.artifacts.values().map(|artifact| &artifact.scope)
|
||||
}
|
||||
|
||||
/// Implements canonical `fetch_interface` semantics for a structured reference.
|
||||
///
|
||||
/// Malformed references map to `InvalidRequest`. Unknown and currently
|
||||
/// unpublished references (including non-root scopes) map to `InterfaceNotFound`.
|
||||
/// Fetches only with a coherent current scope Object attestation.
|
||||
pub fn fetch_interface(
|
||||
&self,
|
||||
reference: &InterfaceReference,
|
||||
current: &ScopeObservation,
|
||||
) -> Result<InterfaceObservation, ProtocolError> {
|
||||
reference.validate().map_err(|error| ProtocolError {
|
||||
code: ProtocolErrorCode::InvalidRequest,
|
||||
message: error.to_string(),
|
||||
})?;
|
||||
self.resolve(reference)
|
||||
.and_then(|reference| self.artifacts.get(&reference))
|
||||
.map(Self::observation)
|
||||
.ok_or_else(|| ProtocolError {
|
||||
code: ProtocolErrorCode::InterfaceNotFound,
|
||||
message: "interface reference is not currently published".into(),
|
||||
self.artifacts
|
||||
.get(&InterfaceRef(reference.clone()))
|
||||
.filter(|artifact| {
|
||||
artifact.scope.active() && artifact.scope.same_lifetime(¤t.scope)
|
||||
})
|
||||
}
|
||||
|
||||
fn observation(artifact: &Arc<InterfaceArtifact>) -> InterfaceObservation {
|
||||
InterfaceObservation {
|
||||
reference: artifact.reference.clone(),
|
||||
descriptor: artifact.descriptor.clone(),
|
||||
validator: artifact.validator.clone(),
|
||||
}
|
||||
.map(|artifact| InterfaceObservation {
|
||||
reference: artifact.reference.clone(),
|
||||
descriptor: artifact.descriptor.clone(),
|
||||
scope_ref: current.reference.clone(),
|
||||
validator: artifact.validator.clone(),
|
||||
})
|
||||
.ok_or_else(interface_not_found)
|
||||
}
|
||||
}
|
||||
|
||||
/// Failure while registering a generated interface definition.
|
||||
fn interface_not_found() -> ProtocolError {
|
||||
ProtocolError {
|
||||
code: ProtocolErrorCode::InterfaceNotFound,
|
||||
message: "interface is not currently published".into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Failure while registering a generated interface Definition.
|
||||
#[derive(Debug)]
|
||||
pub enum RegistrationError {
|
||||
/// The same generated definition was already assigned a reference.
|
||||
/// The exact scope/name coordinate is already occupied.
|
||||
AlreadyRegistered,
|
||||
/// The generated descriptor is structurally invalid.
|
||||
/// The generated Descriptor is invalid.
|
||||
InvalidDescriptor(ValidationError),
|
||||
/// The registry cannot assign another opaque reference.
|
||||
ReferenceSpaceExhausted,
|
||||
/// The scope or local name is invalid.
|
||||
InvalidReference(ValidationError),
|
||||
/// The lifetime ended; re-publication needs a fresh Scope.
|
||||
RetiredScope,
|
||||
}
|
||||
|
||||
impl Display for RegistrationError {
|
||||
fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::AlreadyRegistered => {
|
||||
formatter.write_str("interface definition already registered")
|
||||
formatter.write_str("interface scope/name already registered")
|
||||
}
|
||||
Self::InvalidDescriptor(error) => {
|
||||
write!(formatter, "invalid interface descriptor: {error}")
|
||||
}
|
||||
Self::ReferenceSpaceExhausted => {
|
||||
formatter.write_str("interface reference space exhausted")
|
||||
Self::InvalidReference(error) => {
|
||||
write!(formatter, "invalid interface reference: {error}")
|
||||
}
|
||||
Self::RetiredScope => formatter.write_str("scope publication lifetime has ended"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for RegistrationError {
|
||||
fn source(&self) -> Option<&(dyn Error + 'static)> {
|
||||
match self {
|
||||
Self::InvalidDescriptor(error) => Some(error),
|
||||
Self::AlreadyRegistered | Self::ReferenceSpaceExhausted => None,
|
||||
Self::InvalidDescriptor(error) | Self::InvalidReference(error) => Some(error),
|
||||
Self::AlreadyRegistered | Self::RetiredScope => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -553,6 +613,7 @@ struct BoundInterface<T> {
|
||||
pub struct PublishedObject<T: WipObject> {
|
||||
implementation: T,
|
||||
interfaces: Vec<BoundInterface<T>>,
|
||||
scopes: Vec<ScopeObservation>,
|
||||
}
|
||||
|
||||
impl<T: WipObject> PublishedObject<T> {
|
||||
@@ -562,13 +623,45 @@ impl<T: WipObject> PublishedObject<T> {
|
||||
Self {
|
||||
implementation,
|
||||
interfaces: Vec::new(),
|
||||
scopes: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Attaches a current scope snapshot inside the provider's atomic boundary.
|
||||
///
|
||||
/// Never populate this from a stale prelookup. Target, scope association,
|
||||
/// Descriptor and backend handles must remain coherent through dispatch and
|
||||
/// commit. A later same-path binding replaces the earlier snapshot.
|
||||
#[must_use]
|
||||
pub fn bind_scope(mut self, current: ScopeObservation) -> Self {
|
||||
self.scopes
|
||||
.retain(|old| old.scope.path != current.scope.path);
|
||||
self.scopes.push(current);
|
||||
self
|
||||
}
|
||||
|
||||
fn current_scope(&self, scope: &Scope) -> Option<&ScopeObservation> {
|
||||
self.scopes
|
||||
.iter()
|
||||
.find(|current| scope.active() && scope.same_lifetime(¤t.scope))
|
||||
}
|
||||
|
||||
fn scopes_valid_at(&self, path: &str) -> bool {
|
||||
self.interfaces.iter().all(|bound| {
|
||||
bound.artifact.reference.0.validate_for_path(path).is_ok()
|
||||
&& self
|
||||
.current_scope(&bound.artifact.scope)
|
||||
.is_some_and(|current| {
|
||||
current.scope.path != path
|
||||
|| current.reference() == self.implementation.reference()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Explicitly exposes a registered generated interface in insertion order.
|
||||
///
|
||||
/// The `InterfaceImplementation<T>` bound is generated only when `T`
|
||||
/// implements the corresponding Rust interface trait. Duplicate opaque
|
||||
/// implements the corresponding Rust interface trait. Duplicate scoped
|
||||
/// references are rejected.
|
||||
pub fn expose<D>(mut self, interface: &RegisteredInterface<D>) -> Result<Self, PublicationError>
|
||||
where
|
||||
@@ -621,6 +714,7 @@ impl<T: WipObject> PublishedObject<T> {
|
||||
let mut descriptor = bound.artifact.descriptor.clone();
|
||||
restrict_descriptor(&mut descriptor, |operation| visible(reference, operation));
|
||||
bound.artifact = Arc::new(InterfaceArtifact {
|
||||
scope: bound.artifact.scope.clone(),
|
||||
reference: reference.clone(),
|
||||
descriptor,
|
||||
validator: bound.artifact.validator.clone(),
|
||||
@@ -637,9 +731,13 @@ impl<T: WipObject> PublishedObject<T> {
|
||||
&self.implementation
|
||||
}
|
||||
|
||||
/// Builds canonical protocol object metadata in interface insertion order.
|
||||
/// Builds metadata from the explicit exposure set in insertion order.
|
||||
///
|
||||
/// The observation contains no path, children, descriptor, backend handle,
|
||||
/// This path-free projection is useful while composing a publication, but is
|
||||
/// not proof that its scopes are currently published. Use
|
||||
/// [`ObjectProvider::observe_object`] for a validated current observation, or
|
||||
/// [`Self::protocol_object_at`] inside a provider's coherent boundary.
|
||||
/// The metadata contains no path, children, descriptor, backend handle,
|
||||
/// credential, or provider resolution state.
|
||||
#[must_use]
|
||||
pub fn protocol_object(&self) -> Object {
|
||||
@@ -659,6 +757,30 @@ impl<T: WipObject> PublishedObject<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Validates a current Object observation inside the provider's boundary.
|
||||
///
|
||||
/// All exposed interfaces must have compatible, live scope associations. The
|
||||
/// provider must still establish those associations atomically; this method
|
||||
/// cannot verify backend lifetime assertions or lock external writers.
|
||||
pub fn protocol_object_at(&self, path: &str) -> Result<Object, CallError> {
|
||||
wip_protocol::validate_path(path)
|
||||
.map_err(|error| CallError::new(CallErrorKind::InvalidRequest, error.to_string()))?;
|
||||
if !self.scopes_valid_at(path) {
|
||||
return Err(CallError::new(
|
||||
CallErrorKind::Internal,
|
||||
"Host published interfaces without current compatible scope Objects",
|
||||
));
|
||||
}
|
||||
let object = self.protocol_object();
|
||||
object.validate_at_path(path).map_err(|_| {
|
||||
CallError::new(
|
||||
CallErrorKind::Internal,
|
||||
"Host produced an invalid object response",
|
||||
)
|
||||
})?;
|
||||
Ok(object)
|
||||
}
|
||||
|
||||
fn dispatch_resolved(
|
||||
&self,
|
||||
context: &ExecutionContext,
|
||||
@@ -720,6 +842,31 @@ impl<T: WipObject> PublishedObject<T> {
|
||||
);
|
||||
};
|
||||
|
||||
let Some(current_scope) = self.current_scope(&bound.artifact.scope) else {
|
||||
return pre_dispatch_failure(
|
||||
CallErrorKind::InterfaceMismatch,
|
||||
"interface scope Object is not currently associated with this publication",
|
||||
);
|
||||
};
|
||||
if current_scope.scope.path == target.path
|
||||
&& current_scope.reference() != self.implementation.reference()
|
||||
{
|
||||
return pre_dispatch_failure(
|
||||
CallErrorKind::Internal,
|
||||
"provider produced inconsistent self-scope metadata",
|
||||
);
|
||||
}
|
||||
if interface
|
||||
.scope_ref
|
||||
.as_deref()
|
||||
.is_some_and(|supplied| current_scope.reference() != Some(supplied))
|
||||
{
|
||||
return pre_dispatch_failure(
|
||||
CallErrorKind::InterfaceMismatch,
|
||||
"scope Object ref does not match the current association",
|
||||
);
|
||||
}
|
||||
|
||||
match (
|
||||
bound.artifact.validator.as_ref(),
|
||||
interface.validator.as_deref(),
|
||||
@@ -958,7 +1105,12 @@ pub enum ProviderResult<T> {
|
||||
///
|
||||
/// `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`. The execution context is the
|
||||
/// backend target and all current scope Object associations for the duration of
|
||||
/// `operation`. Attach them with PublishedObject::bind_scope inside this same
|
||||
/// boundary, never via an independent scope prelookup. A new Object at the same
|
||||
/// path needs a fresh Scope; ordinary state changes retain the handle. Providers
|
||||
/// own lifetime/atomicity enforcement, including external writers and backend
|
||||
/// commit/rollback. The execution context is the
|
||||
/// runtime-neutral carrier for cancellation, deadline, and resource controls.
|
||||
/// The borrowed publication cannot escape the callback.
|
||||
///
|
||||
@@ -1022,7 +1174,9 @@ pub trait ObjectProvider {
|
||||
wip_protocol::validate_path(path)
|
||||
.map_err(|error| CallError::new(CallErrorKind::InvalidRequest, error.to_string()))?;
|
||||
let resolved = catch_unwind(AssertUnwindSafe(|| {
|
||||
self.with_object(context, path, PublishedObject::protocol_object)
|
||||
self.with_object(context, path, |publication| {
|
||||
publication.protocol_object_at(path)
|
||||
})
|
||||
}))
|
||||
.map_err(|_| {
|
||||
CallError::new(
|
||||
@@ -1031,7 +1185,7 @@ pub trait ObjectProvider {
|
||||
)
|
||||
})?;
|
||||
let object = match resolved {
|
||||
ProviderResult::Found(object) => object,
|
||||
ProviderResult::Found(object) => object?,
|
||||
ProviderResult::NotFound => {
|
||||
return Err(CallError::new(
|
||||
CallErrorKind::NotFound,
|
||||
@@ -1040,15 +1194,58 @@ pub trait ObjectProvider {
|
||||
}
|
||||
ProviderResult::Failure(error) => return Err(CallError::from_read_failure(error)),
|
||||
};
|
||||
object.validate_at_path(path).map_err(|_| {
|
||||
CallError::new(
|
||||
CallErrorKind::Internal,
|
||||
"Host produced an invalid object response",
|
||||
)
|
||||
})?;
|
||||
Ok(object)
|
||||
}
|
||||
|
||||
/// Fetches Descriptor, scope Object ref and Interface validator atomically.
|
||||
///
|
||||
/// The scope Object may expose no interfaces. Its publication must still
|
||||
/// bind the live ScopeObservation. Registry retention is not publication.
|
||||
fn fetch_interface(
|
||||
&self,
|
||||
registry: &InterfaceRegistry,
|
||||
reference: &InterfaceReference,
|
||||
) -> Result<InterfaceObservation, ProtocolError> {
|
||||
reference.validate().map_err(|error| ProtocolError {
|
||||
code: ProtocolErrorCode::InvalidRequest,
|
||||
message: error.to_string(),
|
||||
})?;
|
||||
let observed = catch_unwind(AssertUnwindSafe(|| {
|
||||
self.with_object(
|
||||
&ExecutionContext::default(),
|
||||
&reference.scope,
|
||||
|publication| {
|
||||
publication
|
||||
.scopes
|
||||
.iter()
|
||||
.find(|current| current.scope.path == reference.scope)
|
||||
.ok_or_else(interface_not_found)
|
||||
.and_then(|current| {
|
||||
if current.reference() != publication.implementation.reference() {
|
||||
return Err(ProtocolError {
|
||||
code: ProtocolErrorCode::Internal,
|
||||
message: "provider produced inconsistent scope Object metadata"
|
||||
.into(),
|
||||
});
|
||||
}
|
||||
registry.fetch_interface(reference, current)
|
||||
})
|
||||
},
|
||||
)
|
||||
}))
|
||||
.map_err(|_| ProtocolError {
|
||||
code: ProtocolErrorCode::Internal,
|
||||
message: "provider panicked while observing an interface".into(),
|
||||
})?;
|
||||
match observed {
|
||||
ProviderResult::Found(observation) => observation,
|
||||
ProviderResult::NotFound => Err(interface_not_found()),
|
||||
ProviderResult::Failure(error) => {
|
||||
Err(CallError::from_read_failure(error).protocol_error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolves and executes a canonical path-targeted operation request.
|
||||
///
|
||||
/// Structural path validation precedes provider resolution. For a valid
|
||||
@@ -1168,7 +1365,7 @@ pub trait ObjectProvider {
|
||||
/// Failure to construct an explicit object publication.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum PublicationError {
|
||||
/// The same opaque interface reference was exposed more than once.
|
||||
/// The same scope/name Interface reference was exposed more than once.
|
||||
DuplicateInterface(InterfaceRef),
|
||||
/// Projection attempted to add an interface without an implementation.
|
||||
UnexposedInterface(InterfaceRef),
|
||||
|
||||
@@ -6,7 +6,7 @@ use std::time::{Duration, Instant};
|
||||
use wip_host::{
|
||||
CallError, CallErrorKind, CallRequest, CancellationToken, ExecutionContext, InterfaceRef,
|
||||
InterfaceRegistry, InterfaceTarget, ObjectProvider, OperationAttempt, ProviderResult,
|
||||
PublishedObject, RegisteredInterface, Target, Validator, WipObject,
|
||||
PublishedObject, RegisteredInterface, Scope, ScopeObservation, Target, Validator, WipObject,
|
||||
};
|
||||
use wip_interface::{
|
||||
CodecError, EffectGuarantee, HostError, OperationResult, TypeDeclaration, TypeExpr, Value,
|
||||
@@ -17,9 +17,16 @@ use wip_protocol::ProtocolErrorCode;
|
||||
#[test]
|
||||
fn principal_projection_orders_subsets_and_rejects_unimplemented_or_duplicate_tokens() {
|
||||
let mut registry = InterfaceRegistry::new();
|
||||
let read = registry.register(ReaderDefinition, None).unwrap();
|
||||
let write = registry.register(WriterDefinition, None).unwrap();
|
||||
let hidden = registry.register(HiddenDefinition, None).unwrap();
|
||||
let root = Scope::new("/").unwrap();
|
||||
let read = registry
|
||||
.register(&root, "reader", ReaderDefinition, None)
|
||||
.unwrap();
|
||||
let write = registry
|
||||
.register(&root, "writer::#日本語", WriterDefinition, None)
|
||||
.unwrap();
|
||||
let hidden = registry
|
||||
.register(&root, "hidden", HiddenDefinition, None)
|
||||
.unwrap();
|
||||
let publication = || {
|
||||
PublishedObject::new(Document::open("document", None, Rc::new(Cell::new(0))))
|
||||
.expose(&read)
|
||||
@@ -61,8 +68,9 @@ fn principal_projection_orders_subsets_and_rejects_unimplemented_or_duplicate_to
|
||||
.interfaces
|
||||
.is_empty()
|
||||
);
|
||||
let current = ScopeObservation::new(&root, &Document::open("", None, Rc::new(Cell::new(0))));
|
||||
let full = registry
|
||||
.fetch_interface(&read.reference().protocol_reference())
|
||||
.fetch_interface(&read.reference().protocol_reference(), ¤t)
|
||||
.unwrap();
|
||||
let filtered = full
|
||||
.clone()
|
||||
@@ -71,7 +79,7 @@ fn principal_projection_orders_subsets_and_rejects_unimplemented_or_duplicate_to
|
||||
assert_eq!(full.descriptor.operations.len(), 3);
|
||||
assert_eq!(
|
||||
registry
|
||||
.fetch_interface(&read.reference().protocol_reference())
|
||||
.fetch_interface(&read.reference().protocol_reference(), ¤t)
|
||||
.unwrap(),
|
||||
full
|
||||
);
|
||||
@@ -309,6 +317,7 @@ impl Broken for Document {
|
||||
}
|
||||
|
||||
struct TestProvider {
|
||||
root: Scope,
|
||||
reader: RegisteredInterface<ReaderDefinition>,
|
||||
writer: RegisteredInterface<WriterDefinition>,
|
||||
broken: RegisteredInterface<BrokenDefinition>,
|
||||
@@ -326,11 +335,13 @@ struct TestProvider {
|
||||
|
||||
impl TestProvider {
|
||||
fn new(
|
||||
root: Scope,
|
||||
reader: RegisteredInterface<ReaderDefinition>,
|
||||
writer: RegisteredInterface<WriterDefinition>,
|
||||
broken: RegisteredInterface<BrokenDefinition>,
|
||||
) -> Self {
|
||||
Self {
|
||||
root,
|
||||
reader,
|
||||
writer,
|
||||
broken,
|
||||
@@ -362,7 +373,10 @@ impl ObjectProvider for TestProvider {
|
||||
self.observed_cancelled
|
||||
.set(context.cancellation().is_cancelled());
|
||||
self.resolutions.set(self.resolutions.get() + 1);
|
||||
let scope_object = Document::open("", None, Rc::clone(&self.calls));
|
||||
let current_scope = ScopeObservation::new(&self.root, &scope_object);
|
||||
let (document, interfaces) = match path {
|
||||
"/" => (scope_object, 0),
|
||||
"/document" => (
|
||||
Document::strict(
|
||||
"document",
|
||||
@@ -420,6 +434,7 @@ impl ObjectProvider for TestProvider {
|
||||
3 => publication.expose(&self.broken).unwrap(),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let publication = publication.bind_scope(current_scope);
|
||||
ProviderResult::Found(operation(&publication))
|
||||
}
|
||||
|
||||
@@ -461,13 +476,25 @@ fn registry_and_provider() -> (
|
||||
RegisteredInterface<HiddenDefinition>,
|
||||
) {
|
||||
let mut registry = InterfaceRegistry::new();
|
||||
let root = Scope::new("/").unwrap();
|
||||
let reader = registry
|
||||
.register(ReaderDefinition, Some(Validator::new(b"schema-v1")))
|
||||
.register(
|
||||
&root,
|
||||
"reader",
|
||||
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);
|
||||
let writer = registry
|
||||
.register(&root, "writer::#日本語", WriterDefinition, None)
|
||||
.unwrap();
|
||||
let hidden = registry
|
||||
.register(&root, "hidden", HiddenDefinition, None)
|
||||
.unwrap();
|
||||
let broken = registry
|
||||
.register(&root, "broken", BrokenDefinition, None)
|
||||
.unwrap();
|
||||
let provider = TestProvider::new(root, reader, writer, broken);
|
||||
(registry, provider, hidden)
|
||||
}
|
||||
|
||||
@@ -505,8 +532,8 @@ fn registry_returns_atomic_canonical_observation_without_rewriting_descriptor()
|
||||
let reference = provider.reader.reference();
|
||||
|
||||
assert_ne!(reference.as_str(), "Reader");
|
||||
let observation = registry
|
||||
.fetch_interface(&reference.protocol_reference())
|
||||
let observation = provider
|
||||
.fetch_interface(®istry, &reference.protocol_reference())
|
||||
.unwrap();
|
||||
assert_eq!(observation.reference, *reference);
|
||||
assert_eq!(observation.descriptor, ReaderDefinition::descriptor());
|
||||
@@ -518,8 +545,8 @@ fn registry_returns_atomic_canonical_observation_without_rewriting_descriptor()
|
||||
assert_eq!(response.validator.as_deref(), Some(b"schema-v1".as_slice()));
|
||||
response.descriptor.validate().unwrap();
|
||||
|
||||
let mut invalid_observation = registry
|
||||
.fetch_interface(&reference.protocol_reference())
|
||||
let mut invalid_observation = provider
|
||||
.fetch_interface(®istry, &reference.protocol_reference())
|
||||
.unwrap();
|
||||
let duplicate_operation = invalid_observation.descriptor.operations[0].clone();
|
||||
invalid_observation
|
||||
@@ -529,11 +556,14 @@ fn registry_returns_atomic_canonical_observation_without_rewriting_descriptor()
|
||||
let invalid_response = invalid_observation.protocol_response().unwrap_err();
|
||||
assert_eq!(invalid_response.code, ProtocolErrorCode::Internal);
|
||||
|
||||
let error = registry
|
||||
.fetch_interface(&wip_protocol::InterfaceReference {
|
||||
scope: "/".into(),
|
||||
name: "unknown".into(),
|
||||
})
|
||||
let error = provider
|
||||
.fetch_interface(
|
||||
®istry,
|
||||
&wip_protocol::InterfaceReference {
|
||||
scope: "/".into(),
|
||||
name: "unknown".into(),
|
||||
},
|
||||
)
|
||||
.unwrap_err();
|
||||
assert_eq!(error.code, ProtocolErrorCode::InterfaceNotFound);
|
||||
}
|
||||
@@ -1146,14 +1176,13 @@ fn execution_context_is_propagated_and_preflight_prevents_invocation() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_registry_boundaries_copy_names_verbatim_and_match_exact_scope() {
|
||||
fn registry_boundaries_preserve_names_verbatim_and_match_exact_scope() {
|
||||
let (registry, provider, _) = registry_and_provider();
|
||||
let legacy = provider.writer.reference();
|
||||
let reference = legacy.protocol_reference();
|
||||
assert_eq!(reference.scope, "/");
|
||||
assert_eq!(reference.name, legacy.as_str());
|
||||
// The registry's colon-delimited name remains opaque, not parsed.
|
||||
assert!(reference.name.starts_with("wip-interface:"));
|
||||
assert_eq!(reference.name, "writer::#日本語");
|
||||
assert_eq!(registry.resolve(&reference).as_ref(), Some(legacy));
|
||||
let nested = wip_protocol::InterfaceReference {
|
||||
scope: "/open".into(),
|
||||
@@ -1161,7 +1190,10 @@ fn legacy_registry_boundaries_copy_names_verbatim_and_match_exact_scope() {
|
||||
};
|
||||
assert!(registry.resolve(&nested).is_none());
|
||||
assert_eq!(
|
||||
registry.fetch_interface(&nested).unwrap_err().code,
|
||||
provider
|
||||
.fetch_interface(®istry, &nested)
|
||||
.unwrap_err()
|
||||
.code,
|
||||
ProtocolErrorCode::InterfaceNotFound
|
||||
);
|
||||
let malformed = wip_protocol::InterfaceReference {
|
||||
@@ -1169,7 +1201,10 @@ fn legacy_registry_boundaries_copy_names_verbatim_and_match_exact_scope() {
|
||||
name: reference.name,
|
||||
};
|
||||
assert_eq!(
|
||||
registry.fetch_interface(&malformed).unwrap_err().code,
|
||||
provider
|
||||
.fetch_interface(®istry, &malformed)
|
||||
.unwrap_err()
|
||||
.code,
|
||||
ProtocolErrorCode::InvalidRequest
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,560 @@
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::collections::BTreeMap;
|
||||
use std::rc::Rc;
|
||||
|
||||
use wip_host::{
|
||||
CallErrorKind, CallRequest, ExecutionContext, InterfaceRegistry, InterfaceTarget,
|
||||
ObjectProvider, ProviderResult, PublishedObject, RegisteredInterface, RegistrationError, Scope,
|
||||
ScopeObservation, Target, Validator, WipObject,
|
||||
};
|
||||
use wip_interface::{OperationResult, interface};
|
||||
use wip_protocol::{InterfaceReference, ProtocolErrorCode, Value};
|
||||
|
||||
#[interface]
|
||||
pub trait Read {
|
||||
fn read(&self, value: String) -> OperationResult<String>;
|
||||
}
|
||||
|
||||
#[interface]
|
||||
pub trait Count {
|
||||
fn read(&self, value: i64) -> OperationResult<i64>;
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Metadata {
|
||||
name: String,
|
||||
reference: Option<String>,
|
||||
validator: Option<Validator>,
|
||||
}
|
||||
impl WipObject for Metadata {
|
||||
fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
fn reference(&self) -> Option<&str> {
|
||||
self.reference.as_deref()
|
||||
}
|
||||
fn validator(&self) -> Option<&Validator> {
|
||||
self.validator.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
type OnStart = Rc<RefCell<Option<Box<dyn Fn()>>>>;
|
||||
|
||||
struct Implementation {
|
||||
metadata: Metadata,
|
||||
calls: Rc<Cell<usize>>,
|
||||
on_start: OnStart,
|
||||
}
|
||||
impl WipObject for Implementation {
|
||||
fn name(&self) -> &str {
|
||||
self.metadata.name()
|
||||
}
|
||||
fn reference(&self) -> Option<&str> {
|
||||
self.metadata.reference()
|
||||
}
|
||||
fn validator(&self) -> Option<&Validator> {
|
||||
self.metadata.validator()
|
||||
}
|
||||
}
|
||||
impl Read for Implementation {
|
||||
fn read(&self, value: String) -> OperationResult<String> {
|
||||
self.calls.set(self.calls.get() + 1);
|
||||
if let Some(action) = self.on_start.borrow_mut().take() {
|
||||
action();
|
||||
}
|
||||
Ok(value)
|
||||
}
|
||||
}
|
||||
|
||||
struct Provider {
|
||||
interface: RegisteredInterface<ReadDefinition>,
|
||||
current: Rc<RefCell<Option<(Scope, Metadata)>>>,
|
||||
target_path: String,
|
||||
target_exists: Cell<bool>,
|
||||
member: Cell<bool>,
|
||||
calls: Rc<Cell<usize>>,
|
||||
resolutions: Cell<usize>,
|
||||
on_start: OnStart,
|
||||
}
|
||||
impl Provider {
|
||||
fn new(
|
||||
scope: &Scope,
|
||||
interface: RegisteredInterface<ReadDefinition>,
|
||||
target: &str,
|
||||
reference: Option<&str>,
|
||||
) -> Self {
|
||||
Self {
|
||||
interface,
|
||||
current: Rc::new(RefCell::new(Some((
|
||||
scope.clone(),
|
||||
Metadata {
|
||||
name: scope.path().rsplit('/').next().unwrap().into(),
|
||||
reference: reference.map(str::to_owned),
|
||||
validator: Some(Validator::new(b"scope-state-v1")),
|
||||
},
|
||||
)))),
|
||||
target_path: target.into(),
|
||||
target_exists: Cell::new(true),
|
||||
member: Cell::new(true),
|
||||
calls: Rc::new(Cell::new(0)),
|
||||
resolutions: Cell::new(0),
|
||||
on_start: Rc::new(RefCell::new(None)),
|
||||
}
|
||||
}
|
||||
fn request(&self, scope_ref: Option<&str>) -> CallRequest {
|
||||
CallRequest {
|
||||
target: Target {
|
||||
path: self.target_path.clone(),
|
||||
validator: None,
|
||||
},
|
||||
interface: InterfaceTarget {
|
||||
reference: self.interface.reference().protocol_reference(),
|
||||
scope_ref: scope_ref.map(str::to_owned),
|
||||
validator: Some(b"descriptor-v1".to_vec()),
|
||||
},
|
||||
operation: "read".into(),
|
||||
arguments: BTreeMap::from([("value".into(), Value::String("original backend".into()))]),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl ObjectProvider for Provider {
|
||||
type Object = Implementation;
|
||||
fn with_object<R>(
|
||||
&self,
|
||||
_: &ExecutionContext,
|
||||
path: &str,
|
||||
callback: impl FnOnce(&PublishedObject<Self::Object>) -> R,
|
||||
) -> ProviderResult<R> {
|
||||
self.resolutions.set(self.resolutions.get() + 1);
|
||||
// A single boundary captures both the target and the live scope association.
|
||||
let current = self.current.borrow().clone();
|
||||
let target = path == self.target_path;
|
||||
let metadata = if target {
|
||||
if !self.target_exists.get() {
|
||||
return ProviderResult::NotFound;
|
||||
}
|
||||
if let Some((scope, object)) = ¤t
|
||||
&& scope.path() == path
|
||||
{
|
||||
object.clone()
|
||||
} else {
|
||||
Metadata {
|
||||
name: path.rsplit('/').next().unwrap().into(),
|
||||
reference: None,
|
||||
validator: Some(Validator::new(b"target-v1")),
|
||||
}
|
||||
}
|
||||
} else if let Some((scope, object)) = ¤t {
|
||||
if path != scope.path() {
|
||||
return ProviderResult::NotFound;
|
||||
}
|
||||
object.clone()
|
||||
} else {
|
||||
return ProviderResult::NotFound;
|
||||
};
|
||||
let mut publication = PublishedObject::new(Implementation {
|
||||
metadata,
|
||||
calls: self.calls.clone(),
|
||||
on_start: self.on_start.clone(),
|
||||
});
|
||||
if target && self.member.get() {
|
||||
publication = publication.expose(&self.interface).unwrap();
|
||||
}
|
||||
if let Some((scope, object)) = current {
|
||||
publication = publication.bind_scope(ScopeObservation::new(&scope, &object));
|
||||
}
|
||||
ProviderResult::Found(callback(&publication))
|
||||
}
|
||||
}
|
||||
fn setup(
|
||||
scope: &str,
|
||||
target: &str,
|
||||
reference: Option<&str>,
|
||||
) -> (InterfaceRegistry, Scope, Provider) {
|
||||
let scope = Scope::new(scope).unwrap();
|
||||
let mut registry = InterfaceRegistry::new();
|
||||
let interface = registry
|
||||
.register(
|
||||
&scope,
|
||||
"read::#日本語",
|
||||
ReadDefinition,
|
||||
Some(Validator::new(b"descriptor-v1")),
|
||||
)
|
||||
.unwrap();
|
||||
let provider = Provider::new(&scope, interface, target, reference);
|
||||
(registry, scope, provider)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn registrations_use_scope_name_not_definition_type_or_delimiters() {
|
||||
let mut registry = InterfaceRegistry::new();
|
||||
let left = Scope::new("/left").unwrap();
|
||||
let right = Scope::new("/right").unwrap();
|
||||
let a = registry
|
||||
.register(&left, "read::#日本語", ReadDefinition, None)
|
||||
.unwrap();
|
||||
let b = registry
|
||||
.register(&right, "read::#日本語", ReadDefinition, None)
|
||||
.unwrap();
|
||||
assert_ne!(a.reference(), b.reference());
|
||||
assert_eq!(
|
||||
a.reference().protocol_reference(),
|
||||
InterfaceReference {
|
||||
scope: "/left".into(),
|
||||
name: "read::#日本語".into(),
|
||||
}
|
||||
);
|
||||
assert!(matches!(
|
||||
registry.register(&left, "read::#日本語", CountDefinition, None),
|
||||
Err(RegistrationError::AlreadyRegistered)
|
||||
));
|
||||
assert!(matches!(
|
||||
registry.register(&left, "", ReadDefinition, None),
|
||||
Err(RegistrationError::InvalidReference(_))
|
||||
));
|
||||
assert!(Scope::new("/invalid/").is_err());
|
||||
registry
|
||||
.register(&left, "another", ReadDefinition, None)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn self_root_ancestor_and_membership_are_independent() {
|
||||
for (scope, target) in [
|
||||
("/", "/"),
|
||||
("/", "/target"),
|
||||
("/scope", "/scope"),
|
||||
("/scope", "/scope/target"),
|
||||
] {
|
||||
let (registry, _, provider) = setup(scope, target, Some("scope-id"));
|
||||
assert_eq!(
|
||||
provider
|
||||
.call_operation(provider.request(Some("scope-id")))
|
||||
.unwrap()
|
||||
.result,
|
||||
Value::String("original backend".into())
|
||||
);
|
||||
assert_eq!(
|
||||
provider
|
||||
.fetch_interface(®istry, &provider.request(None).interface.reference)
|
||||
.unwrap()
|
||||
.scope_ref
|
||||
.as_deref(),
|
||||
Some("scope-id")
|
||||
);
|
||||
provider.member.set(false);
|
||||
assert_eq!(
|
||||
provider
|
||||
.call_operation(provider.request(None))
|
||||
.unwrap_err()
|
||||
.kind(),
|
||||
CallErrorKind::InterfaceMismatch
|
||||
);
|
||||
}
|
||||
// Non-self scope Objects need not implement or expose any Interface.
|
||||
let (registry, _, provider) = setup("/scope", "/scope/target", None);
|
||||
assert!(
|
||||
provider
|
||||
.observe_object("/scope")
|
||||
.unwrap()
|
||||
.interfaces
|
||||
.is_empty()
|
||||
);
|
||||
provider
|
||||
.fetch_interface(®istry, &provider.request(None).interface.reference)
|
||||
.unwrap();
|
||||
for target in ["/sibling/target", "/scopelong", "/"] {
|
||||
let (_, _, provider) = setup("/scope", target, None);
|
||||
assert_eq!(
|
||||
provider
|
||||
.call_operation(provider.request(None))
|
||||
.unwrap_err()
|
||||
.kind(),
|
||||
CallErrorKind::InterfaceMismatch
|
||||
);
|
||||
assert_eq!(provider.calls.get(), 0);
|
||||
assert_eq!(
|
||||
provider.observe_object(target).unwrap_err().kind(),
|
||||
CallErrorKind::Internal
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn optional_scope_ref_and_state_validator_have_distinct_responsibilities() {
|
||||
for current in [Some("scope-id"), None, Some("")] {
|
||||
let (registry, _, provider) = setup("/scope", "/scope/target", current);
|
||||
let observation = provider
|
||||
.fetch_interface(®istry, &provider.request(None).interface.reference)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
observation
|
||||
.protocol_response()
|
||||
.unwrap()
|
||||
.scope_ref
|
||||
.as_deref(),
|
||||
current
|
||||
);
|
||||
assert_eq!(
|
||||
observation.validator,
|
||||
Some(Validator::new(b"descriptor-v1"))
|
||||
);
|
||||
provider.call_operation(provider.request(None)).unwrap();
|
||||
if let Some(reference) = current {
|
||||
provider
|
||||
.call_operation(provider.request(Some(reference)))
|
||||
.unwrap();
|
||||
}
|
||||
let before = provider.calls.get();
|
||||
for supplied in ["other", "missing"] {
|
||||
let mut call = provider.request(Some(supplied));
|
||||
call.interface.validator = None;
|
||||
call.operation = "missing".into();
|
||||
assert_eq!(
|
||||
provider.call_operation(call).unwrap_err().kind(),
|
||||
CallErrorKind::InterfaceMismatch
|
||||
);
|
||||
}
|
||||
assert_eq!(provider.calls.get(), before);
|
||||
provider.current.borrow_mut().as_mut().unwrap().1.validator =
|
||||
Some(Validator::new(b"scope-state-v2"));
|
||||
provider.call_operation(provider.request(current)).unwrap();
|
||||
assert_eq!(
|
||||
provider
|
||||
.fetch_interface(®istry, &provider.request(None).interface.reference)
|
||||
.unwrap(),
|
||||
observation
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scope_precondition_is_membership_stage_in_both_dispatch_paths() {
|
||||
let (_, _, provider) = setup("/scope", "/scope/target", Some("current"));
|
||||
let mut call = provider.request(Some("stale"));
|
||||
call.target.validator = Some(b"wrong".to_vec());
|
||||
assert_eq!(
|
||||
provider.call_operation(call.clone()).unwrap_err().kind(),
|
||||
CallErrorKind::ValidatorMismatch
|
||||
);
|
||||
provider.target_exists.set(false);
|
||||
assert_eq!(
|
||||
provider.call_operation(call).unwrap_err().kind(),
|
||||
CallErrorKind::NotFound
|
||||
);
|
||||
provider.target_exists.set(true);
|
||||
let call = provider.request(Some("stale"));
|
||||
assert_eq!(
|
||||
provider
|
||||
.call_operation_with_context_and_decoder(
|
||||
&ExecutionContext::new(),
|
||||
call.target,
|
||||
call.interface,
|
||||
call.operation,
|
||||
|_| -> Result<BTreeMap<String, Value>, ()> { panic!("decoder must not run") }
|
||||
)
|
||||
.unwrap_err()
|
||||
.kind(),
|
||||
CallErrorKind::InterfaceMismatch
|
||||
);
|
||||
assert_eq!(provider.calls.get(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deletion_republication_and_movement_never_inherit_retained_artifacts() {
|
||||
let (mut registry, scope, provider) = setup("/scope", "/scope/target", Some("old"));
|
||||
let reference = provider.request(None).interface.reference;
|
||||
provider.fetch_interface(®istry, &reference).unwrap();
|
||||
*provider.current.borrow_mut() = None;
|
||||
provider.target_exists.set(false);
|
||||
assert_eq!(
|
||||
provider
|
||||
.fetch_interface(®istry, &reference)
|
||||
.unwrap_err()
|
||||
.code,
|
||||
ProtocolErrorCode::InterfaceNotFound
|
||||
);
|
||||
assert_eq!(
|
||||
provider
|
||||
.call_operation(provider.request(None))
|
||||
.unwrap_err()
|
||||
.kind(),
|
||||
CallErrorKind::NotFound
|
||||
);
|
||||
provider.target_exists.set(true);
|
||||
// Even the same ref is not proof that a replacement inherits registration.
|
||||
let replacement = Scope::new("/scope").unwrap();
|
||||
*provider.current.borrow_mut() = Some((
|
||||
replacement.clone(),
|
||||
Metadata {
|
||||
name: "scope".into(),
|
||||
reference: Some("old".into()),
|
||||
validator: None,
|
||||
},
|
||||
));
|
||||
assert_eq!(
|
||||
provider
|
||||
.fetch_interface(®istry, &reference)
|
||||
.unwrap_err()
|
||||
.code,
|
||||
ProtocolErrorCode::InterfaceNotFound
|
||||
);
|
||||
assert_eq!(
|
||||
provider
|
||||
.call_operation(provider.request(Some("old")))
|
||||
.unwrap_err()
|
||||
.kind(),
|
||||
CallErrorKind::InterfaceMismatch
|
||||
);
|
||||
registry.remove_scope(&scope);
|
||||
assert!(matches!(
|
||||
registry.register(&scope, "new", ReadDefinition, None),
|
||||
Err(RegistrationError::RetiredScope)
|
||||
));
|
||||
let fresh = registry
|
||||
.register(&replacement, reference.name.clone(), ReadDefinition, None)
|
||||
.unwrap();
|
||||
provider.fetch_interface(®istry, &reference).unwrap();
|
||||
// Old target membership still binds the old lifetime, even after new publication.
|
||||
assert_eq!(
|
||||
provider
|
||||
.call_operation(provider.request(None))
|
||||
.unwrap_err()
|
||||
.kind(),
|
||||
CallErrorKind::InterfaceMismatch
|
||||
);
|
||||
let new_provider = Provider::new(&replacement, fresh, "/scope/target", Some("new"));
|
||||
let mut new_call = new_provider.request(Some("new"));
|
||||
new_call.interface.validator = None;
|
||||
new_provider.call_operation(new_call).unwrap();
|
||||
let moved = Scope::new("/moved").unwrap();
|
||||
new_provider.target_exists.set(false);
|
||||
*new_provider.current.borrow_mut() = Some((
|
||||
moved.clone(),
|
||||
Metadata {
|
||||
name: "moved".into(),
|
||||
reference: Some("new".into()),
|
||||
validator: None,
|
||||
},
|
||||
));
|
||||
assert_eq!(
|
||||
new_provider
|
||||
.fetch_interface(®istry, &reference)
|
||||
.unwrap_err()
|
||||
.code,
|
||||
ProtocolErrorCode::InterfaceNotFound
|
||||
);
|
||||
assert_eq!(
|
||||
new_provider
|
||||
.call_operation(new_provider.request(None))
|
||||
.unwrap_err()
|
||||
.kind(),
|
||||
CallErrorKind::NotFound
|
||||
);
|
||||
let moved_interface = registry
|
||||
.register(&moved, reference.name, ReadDefinition, None)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
moved_interface.reference().protocol_reference().scope,
|
||||
"/moved"
|
||||
);
|
||||
assert_eq!(provider.calls.get(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replacement_after_fetch_rejects_old_ref_but_omission_and_refless_are_allowed() {
|
||||
for previous_ref in [Some("old"), None] {
|
||||
let (mut registry, old_scope, old_provider) =
|
||||
setup("/scope", "/scope/target", previous_ref);
|
||||
let observed = old_provider
|
||||
.fetch_interface(®istry, &old_provider.request(None).interface.reference)
|
||||
.unwrap();
|
||||
registry.remove_scope(&old_scope);
|
||||
let new_scope = Scope::new("/scope").unwrap();
|
||||
let token = registry
|
||||
.register(
|
||||
&new_scope,
|
||||
"read::#日本語",
|
||||
ReadDefinition,
|
||||
Some(Validator::new(b"descriptor-v1")),
|
||||
)
|
||||
.unwrap();
|
||||
let new_provider = Provider::new(&new_scope, token, "/scope/target", None);
|
||||
if let Some(reference) = observed.scope_ref {
|
||||
assert_eq!(
|
||||
new_provider
|
||||
.call_operation(new_provider.request(Some(&reference)))
|
||||
.unwrap_err()
|
||||
.kind(),
|
||||
CallErrorKind::InterfaceMismatch
|
||||
);
|
||||
assert_eq!(new_provider.calls.get(), 0);
|
||||
}
|
||||
new_provider
|
||||
.call_operation(new_provider.request(None))
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn started_execution_retains_target_and_same_descriptor_through_replacement() {
|
||||
let (registry, old_scope, provider) = setup("/scope", "/scope/target", Some("old"));
|
||||
let registry = Rc::new(RefCell::new(registry));
|
||||
let current = provider.current.clone();
|
||||
let replacement = Scope::new("/scope").unwrap();
|
||||
let registry_for_action = registry.clone();
|
||||
let replacement_for_action = replacement.clone();
|
||||
*provider.on_start.borrow_mut() = Some(Box::new(move || {
|
||||
let mut registry = registry_for_action.borrow_mut();
|
||||
registry.remove_scope(&old_scope);
|
||||
registry
|
||||
.register(
|
||||
&replacement_for_action,
|
||||
"read::#日本語",
|
||||
CountDefinition,
|
||||
Some(Validator::new(b"descriptor-v2")),
|
||||
)
|
||||
.unwrap();
|
||||
*current.borrow_mut() = Some((
|
||||
replacement_for_action.clone(),
|
||||
Metadata {
|
||||
name: "scope".into(),
|
||||
reference: Some("new".into()),
|
||||
validator: None,
|
||||
},
|
||||
));
|
||||
}));
|
||||
let call = provider.request(Some("old"));
|
||||
let result = provider
|
||||
.call_operation_with_context_and_decoder(
|
||||
&ExecutionContext::new(),
|
||||
call.target,
|
||||
call.interface,
|
||||
call.operation,
|
||||
|descriptor| {
|
||||
assert_eq!(descriptor, &ReadDefinition::descriptor());
|
||||
Ok::<_, ()>(call.arguments)
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(result.result, Value::String("original backend".into()));
|
||||
assert_eq!(provider.calls.get(), 1);
|
||||
assert_eq!(provider.resolutions.get(), 1);
|
||||
let new = provider
|
||||
.fetch_interface(
|
||||
®istry.borrow(),
|
||||
&provider.request(None).interface.reference,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(new.descriptor, CountDefinition::descriptor());
|
||||
assert_eq!(new.scope_ref.as_deref(), Some("new"));
|
||||
// The old bound target can no longer start a call against the new descriptor.
|
||||
assert_eq!(
|
||||
provider
|
||||
.call_operation(provider.request(None))
|
||||
.unwrap_err()
|
||||
.kind(),
|
||||
CallErrorKind::InterfaceMismatch
|
||||
);
|
||||
assert_eq!(provider.calls.get(), 1);
|
||||
}
|
||||
@@ -16,6 +16,6 @@ impl WipObject for Object {
|
||||
|
||||
fn main() {
|
||||
let mut registry = InterfaceRegistry::new();
|
||||
let search = registry.register(SearchDefinition, None).unwrap();
|
||||
let search = registry.register(&wip_host::Scope::new("/").unwrap(), "search", SearchDefinition, None).unwrap();
|
||||
let _published = PublishedObject::new(Object).expose(&search).unwrap();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user