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:
@@ -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