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.
561 lines
18 KiB
Rust
561 lines
18 KiB
Rust
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);
|
|
}
|