fix: align host dispatch with canonical paths
This commit is contained in:
+447
-322
@@ -1,9 +1,10 @@
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::cell::Cell;
|
||||
use std::collections::BTreeMap;
|
||||
use std::rc::Rc;
|
||||
|
||||
use wip_host::{
|
||||
CallErrorKind, CallRequest, InterfaceRegistry, InterfaceTarget, ObjectId, PublishedObject,
|
||||
Validator, WipObject,
|
||||
CallError, CallErrorKind, CallRequest, InterfaceRef, InterfaceRegistry, InterfaceTarget,
|
||||
ObjectProvider, PublishedObject, RegisteredInterface, Target, Validator, WipObject,
|
||||
};
|
||||
use wip_interface::{
|
||||
CodecError, HostError, OperationResult, TypeDeclaration, TypeExpr, Value, WipType, interface,
|
||||
@@ -17,6 +18,9 @@ pub trait Reader {
|
||||
|
||||
/// Adds two integers.
|
||||
fn add(&self, left: i64, right: i64) -> OperationResult<i64>;
|
||||
|
||||
/// Reports a backend failure.
|
||||
fn fail(&self) -> OperationResult<String>;
|
||||
}
|
||||
|
||||
#[interface]
|
||||
@@ -57,31 +61,37 @@ impl WipType for BadValue {
|
||||
}
|
||||
|
||||
struct Document {
|
||||
id: String,
|
||||
name: String,
|
||||
description: Option<String>,
|
||||
reference: Option<String>,
|
||||
validator: Option<Validator>,
|
||||
require_validator: bool,
|
||||
calls: Cell<usize>,
|
||||
private_backend_handle: RefCell<String>,
|
||||
calls: Rc<Cell<usize>>,
|
||||
private_backend_handle: String,
|
||||
}
|
||||
|
||||
impl Document {
|
||||
fn new(id: &str) -> Self {
|
||||
fn open(name: &str, reference: Option<&str>, calls: Rc<Cell<usize>>) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
name: name.into(),
|
||||
description: Some("public document".into()),
|
||||
reference: reference.map(str::to_owned),
|
||||
validator: None,
|
||||
require_validator: false,
|
||||
calls: Cell::new(0),
|
||||
private_backend_handle: RefCell::new("/private/backend/path".into()),
|
||||
calls,
|
||||
private_backend_handle: "/private/backend/path".into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn strict(id: &str, validator: &[u8]) -> Self {
|
||||
fn strict(name: &str, reference: &str, validator: &[u8], calls: Rc<Cell<usize>>) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
name: name.into(),
|
||||
description: Some("public document".into()),
|
||||
reference: Some(reference.into()),
|
||||
validator: Some(Validator::new(validator)),
|
||||
require_validator: true,
|
||||
calls: Cell::new(0),
|
||||
private_backend_handle: RefCell::new("secret-handle".into()),
|
||||
calls,
|
||||
private_backend_handle: "secret-handle".into(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,15 +101,23 @@ impl Document {
|
||||
}
|
||||
|
||||
impl WipObject for Document {
|
||||
fn id(&self) -> ObjectId {
|
||||
ObjectId(self.id.clone())
|
||||
fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
fn description(&self) -> Option<&str> {
|
||||
self.description.as_deref()
|
||||
}
|
||||
|
||||
fn reference(&self) -> Option<&str> {
|
||||
self.reference.as_deref()
|
||||
}
|
||||
|
||||
fn validator(&self) -> Option<&Validator> {
|
||||
self.validator.as_ref()
|
||||
}
|
||||
|
||||
fn requires_validator(&self) -> bool {
|
||||
fn requires_validator(&self, _: &InterfaceRef, _: &str) -> bool {
|
||||
self.require_validator
|
||||
}
|
||||
}
|
||||
@@ -114,6 +132,11 @@ impl Reader for Document {
|
||||
self.record_call();
|
||||
Ok(left + right)
|
||||
}
|
||||
|
||||
fn fail(&self) -> OperationResult<String> {
|
||||
self.record_call();
|
||||
Err(HostError::new("backend unavailable"))
|
||||
}
|
||||
}
|
||||
|
||||
impl Writer for Document {
|
||||
@@ -137,366 +160,468 @@ impl Broken for Document {
|
||||
}
|
||||
}
|
||||
|
||||
struct TestProvider {
|
||||
reader: RegisteredInterface<ReaderDefinition>,
|
||||
writer: RegisteredInterface<WriterDefinition>,
|
||||
broken: RegisteredInterface<BrokenDefinition>,
|
||||
calls: Rc<Cell<usize>>,
|
||||
resolutions: Cell<usize>,
|
||||
authorization_checks: Cell<usize>,
|
||||
deny: Cell<bool>,
|
||||
switched: Cell<bool>,
|
||||
}
|
||||
|
||||
impl TestProvider {
|
||||
fn new(
|
||||
reader: RegisteredInterface<ReaderDefinition>,
|
||||
writer: RegisteredInterface<WriterDefinition>,
|
||||
broken: RegisteredInterface<BrokenDefinition>,
|
||||
) -> Self {
|
||||
Self {
|
||||
reader,
|
||||
writer,
|
||||
broken,
|
||||
calls: Rc::new(Cell::new(0)),
|
||||
resolutions: Cell::new(0),
|
||||
authorization_checks: Cell::new(0),
|
||||
deny: Cell::new(false),
|
||||
switched: Cell::new(false),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjectProvider for TestProvider {
|
||||
type Object = Document;
|
||||
|
||||
fn with_object<R>(
|
||||
&self,
|
||||
path: &str,
|
||||
operation: impl FnOnce(&PublishedObject<Self::Object>) -> R,
|
||||
) -> Option<R> {
|
||||
self.resolutions.set(self.resolutions.get() + 1);
|
||||
let (document, interfaces) = match path {
|
||||
"/document" => (
|
||||
Document::strict(
|
||||
"document",
|
||||
"document-ref",
|
||||
b"object-v1",
|
||||
Rc::clone(&self.calls),
|
||||
),
|
||||
2,
|
||||
),
|
||||
"/open" => (
|
||||
Document::open("open", Some("open-ref"), Rc::clone(&self.calls)),
|
||||
2,
|
||||
),
|
||||
"/left/document" | "/right/document" => (
|
||||
Document::strict(
|
||||
"document",
|
||||
"shared-ref",
|
||||
b"shared-v1",
|
||||
Rc::clone(&self.calls),
|
||||
),
|
||||
2,
|
||||
),
|
||||
"/switch" => {
|
||||
let switched = self.switched.replace(true);
|
||||
let (reference, validator) = if switched {
|
||||
("second-ref", b"second".as_slice())
|
||||
} else {
|
||||
("first-ref", b"first".as_slice())
|
||||
};
|
||||
(
|
||||
Document::strict("switch", reference, validator, Rc::clone(&self.calls)),
|
||||
2,
|
||||
)
|
||||
}
|
||||
"/empty" => (Document::open("empty", None, Rc::clone(&self.calls)), 0),
|
||||
"/broken" => (
|
||||
Document::open("broken", Some("broken-ref"), Rc::clone(&self.calls)),
|
||||
3,
|
||||
),
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
let publication = PublishedObject::new(document);
|
||||
let publication = match interfaces {
|
||||
0 => publication,
|
||||
2 => publication
|
||||
.expose(&self.writer)
|
||||
.unwrap()
|
||||
.expose(&self.reader)
|
||||
.unwrap(),
|
||||
3 => publication.expose(&self.broken).unwrap(),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
Some(operation(&publication))
|
||||
}
|
||||
|
||||
fn authorize(&self, _: &str, _: &Document, _: &InterfaceRef, _: &str) -> bool {
|
||||
self.authorization_checks
|
||||
.set(self.authorization_checks.get() + 1);
|
||||
!self.deny.get()
|
||||
}
|
||||
}
|
||||
|
||||
fn registry_and_provider() -> (
|
||||
InterfaceRegistry,
|
||||
TestProvider,
|
||||
RegisteredInterface<HiddenDefinition>,
|
||||
) {
|
||||
let mut registry = InterfaceRegistry::new();
|
||||
let reader = registry
|
||||
.register(ReaderDefinition, Some(Validator::new(b"schema-v1")))
|
||||
.unwrap();
|
||||
let writer = registry.register(WriterDefinition, None).unwrap();
|
||||
let hidden = registry.register(HiddenDefinition, None).unwrap();
|
||||
let broken = registry.register(BrokenDefinition, None).unwrap();
|
||||
let provider = TestProvider::new(reader, writer, broken);
|
||||
(registry, provider, hidden)
|
||||
}
|
||||
|
||||
fn request(
|
||||
object: &str,
|
||||
path: &str,
|
||||
object_validator: Option<&[u8]>,
|
||||
reference: &wip_host::InterfaceRef,
|
||||
reference: &InterfaceRef,
|
||||
interface_validator: Option<&[u8]>,
|
||||
operation: &str,
|
||||
arguments: BTreeMap<String, Value>,
|
||||
) -> CallRequest {
|
||||
CallRequest {
|
||||
object: ObjectId(object.into()),
|
||||
object_validator: object_validator.map(Validator::new),
|
||||
target: Target {
|
||||
path: path.into(),
|
||||
validator: object_validator.map(<[u8]>::to_vec),
|
||||
},
|
||||
interface: InterfaceTarget {
|
||||
reference: reference.clone(),
|
||||
validator: interface_validator.map(Validator::new),
|
||||
reference: reference.as_str().to_owned(),
|
||||
validator: interface_validator.map(<[u8]>::to_vec),
|
||||
},
|
||||
operation: operation.into(),
|
||||
arguments,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn registry_exposes_opaque_validated_descriptor_observations() {
|
||||
let mut registry = InterfaceRegistry::new();
|
||||
let reader = registry.register(ReaderDefinition, None).unwrap();
|
||||
|
||||
assert_ne!(reader.reference().as_str(), "Reader");
|
||||
assert_eq!(
|
||||
registry.resolve(reader.reference().as_str()).as_ref(),
|
||||
Some(reader.reference())
|
||||
);
|
||||
assert!(registry.resolve("wip-interface:unknown:0").is_none());
|
||||
let observation = registry.observe(reader.reference()).unwrap();
|
||||
assert_eq!(observation.reference, *reader.reference());
|
||||
assert_eq!(observation.descriptor, ReaderDefinition::descriptor());
|
||||
assert_eq!(observation.validator, None);
|
||||
observation.descriptor.validate().unwrap();
|
||||
|
||||
assert!(registry.register(ReaderDefinition, None).is_err());
|
||||
|
||||
let mut other_registry = InterfaceRegistry::new();
|
||||
let other_reader = other_registry.register(ReaderDefinition, None).unwrap();
|
||||
assert_ne!(reader.reference(), other_reader.reference());
|
||||
assert!(
|
||||
registry
|
||||
.resolve(other_reader.reference().as_str())
|
||||
.is_none()
|
||||
);
|
||||
fn assert_failure(error: CallError, kind: CallErrorKind, code: ProtocolErrorCode) {
|
||||
assert_eq!(error.kind(), kind);
|
||||
assert_eq!(error.protocol_error().code, code);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn publication_is_explicit_ordered_qualified_and_request_scoped() {
|
||||
let mut registry = InterfaceRegistry::new();
|
||||
let reader = registry.register(ReaderDefinition, None).unwrap();
|
||||
let writer = registry.register(WriterDefinition, None).unwrap();
|
||||
let _hidden = registry.register(HiddenDefinition, None).unwrap();
|
||||
fn registry_returns_atomic_canonical_observation_without_rewriting_descriptor() {
|
||||
let (registry, provider, _) = registry_and_provider();
|
||||
let reference = provider.reader.reference();
|
||||
|
||||
let publication = PublishedObject::new(Document::new("document-1"))
|
||||
.expose(&writer)
|
||||
.unwrap()
|
||||
.expose(&reader)
|
||||
.unwrap();
|
||||
assert_ne!(reference.as_str(), "Reader");
|
||||
let observation = registry.fetch_interface(reference.as_str()).unwrap();
|
||||
assert_eq!(observation.reference, *reference);
|
||||
assert_eq!(observation.descriptor, ReaderDefinition::descriptor());
|
||||
assert_eq!(observation.validator, Some(Validator::new(b"schema-v1")));
|
||||
let response = observation.protocol_response();
|
||||
assert_eq!(response.interface, reference.as_str());
|
||||
assert_eq!(response.descriptor, ReaderDefinition::descriptor());
|
||||
assert_eq!(response.validator.as_deref(), Some(b"schema-v1".as_slice()));
|
||||
response.descriptor.validate().unwrap();
|
||||
|
||||
let object = publication.protocol_object();
|
||||
let error = registry.fetch_interface("unknown").unwrap_err();
|
||||
assert_eq!(error.code, ProtocolErrorCode::InterfaceNotFound);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonical_object_projection_supports_optional_metadata_and_ordered_sets() {
|
||||
let (_, provider, _) = registry_and_provider();
|
||||
let object = provider.fetch("/document").unwrap();
|
||||
|
||||
assert_eq!(object.name, "document");
|
||||
assert_eq!(object.description.as_deref(), Some("public document"));
|
||||
assert_eq!(object.r#ref.as_deref(), Some("document-ref"));
|
||||
assert_eq!(object.validator.as_deref(), Some(b"object-v1".as_slice()));
|
||||
assert_eq!(
|
||||
object.interfaces,
|
||||
vec![
|
||||
writer.reference().as_str().to_owned(),
|
||||
reader.reference().as_str().to_owned()
|
||||
provider.writer.reference().as_str().to_owned(),
|
||||
provider.reader.reference().as_str().to_owned(),
|
||||
]
|
||||
);
|
||||
registry
|
||||
.observe(writer.reference())
|
||||
.unwrap()
|
||||
.descriptor
|
||||
.validate()
|
||||
.unwrap();
|
||||
registry
|
||||
.observe(reader.reference())
|
||||
.unwrap()
|
||||
.descriptor
|
||||
.validate()
|
||||
.unwrap();
|
||||
assert!(!format!("{object:?}").contains("private/backend/path"));
|
||||
assert!(!format!("{object:?}").contains("secret-handle"));
|
||||
assert!(!format!("{object:?}").contains("/document"));
|
||||
|
||||
let writer_result = publication
|
||||
.dispatch(request(
|
||||
"document-1",
|
||||
None,
|
||||
writer.reference(),
|
||||
None,
|
||||
"label",
|
||||
BTreeMap::new(),
|
||||
))
|
||||
.unwrap();
|
||||
let reader_result = publication
|
||||
.dispatch(request(
|
||||
"document-1",
|
||||
None,
|
||||
reader.reference(),
|
||||
None,
|
||||
"label",
|
||||
BTreeMap::new(),
|
||||
))
|
||||
.unwrap();
|
||||
assert_eq!(writer_result.value, Value::String("writer".into()));
|
||||
assert_eq!(reader_result.value, Value::String("reader".into()));
|
||||
assert_eq!(publication.implementation().calls.get(), 2);
|
||||
let empty = provider.fetch("/empty").unwrap();
|
||||
assert!(empty.interfaces.is_empty());
|
||||
assert_eq!(empty.r#ref, None);
|
||||
assert_eq!(empty.validator, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duplicate_exposure_is_rejected_and_private_state_is_not_projected() {
|
||||
let (_, provider, _) = registry_and_provider();
|
||||
let publication = PublishedObject::new(Document::open(
|
||||
"duplicate",
|
||||
None,
|
||||
Rc::clone(&provider.calls),
|
||||
))
|
||||
.expose(&provider.reader)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
publication
|
||||
.implementation()
|
||||
.private_backend_handle
|
||||
.borrow()
|
||||
.as_str(),
|
||||
publication.implementation().private_backend_handle,
|
||||
"/private/backend/path"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn implemented_but_unexposed_interface_is_not_a_member() {
|
||||
let mut registry = InterfaceRegistry::new();
|
||||
let reader = registry.register(ReaderDefinition, None).unwrap();
|
||||
let hidden = registry.register(HiddenDefinition, None).unwrap();
|
||||
let publication = PublishedObject::new(Document::new("document-2"))
|
||||
.expose(&reader)
|
||||
.unwrap();
|
||||
|
||||
let error = publication
|
||||
.dispatch(request(
|
||||
"document-2",
|
||||
None,
|
||||
hidden.reference(),
|
||||
None,
|
||||
"hidden",
|
||||
BTreeMap::new(),
|
||||
))
|
||||
.unwrap_err();
|
||||
assert_eq!(error.kind(), CallErrorKind::InterfaceMismatch);
|
||||
assert_eq!(
|
||||
error.protocol_error().code,
|
||||
ProtocolErrorCode::InterfaceMismatch
|
||||
);
|
||||
assert_eq!(publication.implementation().calls.get(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_interface_objects_are_valid() {
|
||||
let publication = PublishedObject::new(Document::new("empty"));
|
||||
let object = publication.protocol_object();
|
||||
assert_eq!(object.name, "empty");
|
||||
assert_eq!(object.r#ref.as_deref(), Some("empty"));
|
||||
assert!(object.interfaces.is_empty());
|
||||
object.validate().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duplicate_exposure_is_rejected_without_reordering() {
|
||||
let mut registry = InterfaceRegistry::new();
|
||||
let reader = registry.register(ReaderDefinition, None).unwrap();
|
||||
let publication = PublishedObject::new(Document::new("duplicate"))
|
||||
.expose(&reader)
|
||||
.unwrap();
|
||||
let error = match publication.expose(&reader) {
|
||||
let error = match publication.expose(&provider.reader) {
|
||||
Ok(_) => panic!("duplicate exposure unexpectedly succeeded"),
|
||||
Err(error) => error,
|
||||
};
|
||||
assert!(error.to_string().contains(reader.reference().as_str()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn operation_and_argument_failures_are_mapped_without_execution() {
|
||||
let mut registry = InterfaceRegistry::new();
|
||||
let reader = registry.register(ReaderDefinition, None).unwrap();
|
||||
let publication = PublishedObject::new(Document::new("validation"))
|
||||
.expose(&reader)
|
||||
.unwrap();
|
||||
|
||||
let missing = publication
|
||||
.dispatch(request(
|
||||
"validation",
|
||||
None,
|
||||
reader.reference(),
|
||||
None,
|
||||
"missing",
|
||||
BTreeMap::new(),
|
||||
))
|
||||
.unwrap_err();
|
||||
assert_eq!(missing.kind(), CallErrorKind::OperationNotFound);
|
||||
assert_eq!(
|
||||
missing.protocol_error().code,
|
||||
ProtocolErrorCode::OperationNotFound
|
||||
assert!(
|
||||
error
|
||||
.to_string()
|
||||
.contains(provider.reader.reference().as_str())
|
||||
);
|
||||
|
||||
let invalid = publication
|
||||
.dispatch(request(
|
||||
"validation",
|
||||
None,
|
||||
reader.reference(),
|
||||
None,
|
||||
"add",
|
||||
BTreeMap::from([("left".into(), Value::String("wrong".into()))]),
|
||||
))
|
||||
.unwrap_err();
|
||||
assert_eq!(invalid.kind(), CallErrorKind::InvalidArguments);
|
||||
assert_eq!(
|
||||
invalid.protocol_error().code,
|
||||
ProtocolErrorCode::InvalidArguments
|
||||
);
|
||||
assert_eq!(publication.implementation().calls.get(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_implementation_result_is_internal_and_not_returned() {
|
||||
let mut registry = InterfaceRegistry::new();
|
||||
let broken = registry.register(BrokenDefinition, None).unwrap();
|
||||
let publication = PublishedObject::new(Document::new("broken"))
|
||||
.expose(&broken)
|
||||
.unwrap();
|
||||
fn paths_are_resolution_context_not_object_identity() {
|
||||
let (_, provider, _) = registry_and_provider();
|
||||
|
||||
let error = publication
|
||||
.dispatch(request(
|
||||
"broken",
|
||||
None,
|
||||
broken.reference(),
|
||||
None,
|
||||
"broken",
|
||||
BTreeMap::new(),
|
||||
))
|
||||
.unwrap_err();
|
||||
assert_eq!(error.kind(), CallErrorKind::InvalidResult);
|
||||
assert_eq!(error.protocol_error().code, ProtocolErrorCode::Internal);
|
||||
assert_eq!(publication.implementation().calls.get(), 1);
|
||||
let left = provider.fetch("/left/document").unwrap();
|
||||
let right = provider.fetch("/right/document").unwrap();
|
||||
assert_eq!(left, right);
|
||||
assert_eq!(left.r#ref.as_deref(), Some("shared-ref"));
|
||||
|
||||
let first = provider.fetch("/switch").unwrap();
|
||||
let second = provider.fetch("/switch").unwrap();
|
||||
assert_ne!(first.r#ref, second.r#ref);
|
||||
assert_ne!(first.validator, second.validator);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn object_validator_missing_and_mismatch_are_distinct_and_prevent_execution() {
|
||||
let mut registry = InterfaceRegistry::new();
|
||||
let reader = registry.register(ReaderDefinition, None).unwrap();
|
||||
let publication = PublishedObject::new(Document::strict("strict", b"object-v1"))
|
||||
.expose(&reader)
|
||||
.unwrap();
|
||||
fn path_dispatch_is_interface_qualified_and_resolves_once_per_call() {
|
||||
let (_, provider, _) = registry_and_provider();
|
||||
let before = provider.resolutions.get();
|
||||
|
||||
let missing = publication
|
||||
.dispatch(request(
|
||||
"strict",
|
||||
let writer = provider
|
||||
.call_operation(request(
|
||||
"/open",
|
||||
None,
|
||||
reader.reference(),
|
||||
None,
|
||||
"label",
|
||||
BTreeMap::new(),
|
||||
))
|
||||
.unwrap_err();
|
||||
assert_eq!(missing.kind(), CallErrorKind::ValidatorRequired);
|
||||
|
||||
let mismatch = publication
|
||||
.dispatch(request(
|
||||
"strict",
|
||||
Some(b"stale"),
|
||||
reader.reference(),
|
||||
None,
|
||||
"label",
|
||||
BTreeMap::new(),
|
||||
))
|
||||
.unwrap_err();
|
||||
assert_eq!(mismatch.kind(), CallErrorKind::ValidatorMismatch);
|
||||
assert_eq!(publication.implementation().calls.get(), 0);
|
||||
|
||||
let result = publication
|
||||
.dispatch(request(
|
||||
"strict",
|
||||
Some(b"object-v1"),
|
||||
reader.reference(),
|
||||
provider.writer.reference(),
|
||||
None,
|
||||
"label",
|
||||
BTreeMap::new(),
|
||||
))
|
||||
.unwrap();
|
||||
assert_eq!(result.validator, Some(Validator::new(b"object-v1")));
|
||||
assert_eq!(publication.implementation().calls.get(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interface_validator_missing_and_mismatch_are_distinct_and_prevent_execution() {
|
||||
let mut registry = InterfaceRegistry::new();
|
||||
let writer = registry
|
||||
.register(WriterDefinition, Some(Validator::new(b"schema-v1")))
|
||||
.unwrap();
|
||||
let publication = PublishedObject::new(Document::new("mutable-interface"))
|
||||
.expose(&writer)
|
||||
.unwrap();
|
||||
|
||||
let observation = registry.observe(writer.reference()).unwrap();
|
||||
assert_eq!(observation.validator, Some(Validator::new(b"schema-v1")));
|
||||
|
||||
let missing = publication
|
||||
.dispatch(request(
|
||||
"mutable-interface",
|
||||
let reader = provider
|
||||
.call_operation(request(
|
||||
"/open",
|
||||
None,
|
||||
writer.reference(),
|
||||
None,
|
||||
"label",
|
||||
BTreeMap::new(),
|
||||
))
|
||||
.unwrap_err();
|
||||
assert_eq!(missing.kind(), CallErrorKind::InterfaceValidatorRequired);
|
||||
|
||||
let mismatch = publication
|
||||
.dispatch(request(
|
||||
"mutable-interface",
|
||||
None,
|
||||
writer.reference(),
|
||||
Some(b"schema-v0"),
|
||||
"label",
|
||||
BTreeMap::new(),
|
||||
))
|
||||
.unwrap_err();
|
||||
assert_eq!(mismatch.kind(), CallErrorKind::InterfaceValidatorMismatch);
|
||||
assert_eq!(publication.implementation().calls.get(), 0);
|
||||
|
||||
let result = publication
|
||||
.dispatch(request(
|
||||
"mutable-interface",
|
||||
None,
|
||||
writer.reference(),
|
||||
provider.reader.reference(),
|
||||
Some(b"schema-v1"),
|
||||
"label",
|
||||
BTreeMap::new(),
|
||||
))
|
||||
.unwrap();
|
||||
assert_eq!(result.value, Value::String("writer".into()));
|
||||
|
||||
let strict = provider
|
||||
.call_operation(request(
|
||||
"/document",
|
||||
Some(b"object-v1"),
|
||||
provider.reader.reference(),
|
||||
Some(b"schema-v1"),
|
||||
"label",
|
||||
BTreeMap::new(),
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(writer.result, Value::String("writer".into()));
|
||||
assert_eq!(reader.result, Value::String("reader".into()));
|
||||
assert_eq!(strict.result, Value::String("reader".into()));
|
||||
assert_eq!(strict.validator.as_deref(), Some(b"object-v1".as_slice()));
|
||||
assert_eq!(provider.resolutions.get() - before, 3);
|
||||
assert_eq!(provider.calls.get(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn host_failures_map_to_internal_without_becoming_domain_values() {
|
||||
#[interface]
|
||||
pub trait Failing {
|
||||
/// Always fails in the host implementation.
|
||||
fn fail(&self) -> OperationResult<String>;
|
||||
}
|
||||
fn call_failures_follow_canonical_precedence_without_execution() {
|
||||
let (_, provider, hidden) = registry_and_provider();
|
||||
|
||||
impl Failing for Document {
|
||||
fn fail(&self) -> OperationResult<String> {
|
||||
Err(HostError::new("backend unavailable"))
|
||||
}
|
||||
}
|
||||
assert_failure(
|
||||
provider
|
||||
.call_operation(request(
|
||||
"/missing",
|
||||
Some(b"stale"),
|
||||
hidden.reference(),
|
||||
Some(b"stale"),
|
||||
"missing",
|
||||
BTreeMap::new(),
|
||||
))
|
||||
.unwrap_err(),
|
||||
CallErrorKind::NotFound,
|
||||
ProtocolErrorCode::NotFound,
|
||||
);
|
||||
assert_failure(
|
||||
provider
|
||||
.call_operation(request(
|
||||
"/document",
|
||||
Some(b"stale"),
|
||||
hidden.reference(),
|
||||
None,
|
||||
"missing",
|
||||
BTreeMap::new(),
|
||||
))
|
||||
.unwrap_err(),
|
||||
CallErrorKind::ValidatorMismatch,
|
||||
ProtocolErrorCode::ValidatorMismatch,
|
||||
);
|
||||
assert_failure(
|
||||
provider
|
||||
.call_operation(request(
|
||||
"/document",
|
||||
Some(b"object-v1"),
|
||||
hidden.reference(),
|
||||
None,
|
||||
"hidden",
|
||||
BTreeMap::new(),
|
||||
))
|
||||
.unwrap_err(),
|
||||
CallErrorKind::InterfaceMismatch,
|
||||
ProtocolErrorCode::InterfaceMismatch,
|
||||
);
|
||||
assert_failure(
|
||||
provider
|
||||
.call_operation(request(
|
||||
"/document",
|
||||
Some(b"object-v1"),
|
||||
provider.reader.reference(),
|
||||
None,
|
||||
"missing",
|
||||
BTreeMap::new(),
|
||||
))
|
||||
.unwrap_err(),
|
||||
CallErrorKind::InterfaceValidatorRequired,
|
||||
ProtocolErrorCode::InterfaceValidatorRequired,
|
||||
);
|
||||
assert_failure(
|
||||
provider
|
||||
.call_operation(request(
|
||||
"/document",
|
||||
Some(b"object-v1"),
|
||||
provider.reader.reference(),
|
||||
Some(b"stale-schema"),
|
||||
"missing",
|
||||
BTreeMap::new(),
|
||||
))
|
||||
.unwrap_err(),
|
||||
CallErrorKind::InterfaceValidatorMismatch,
|
||||
ProtocolErrorCode::InterfaceValidatorMismatch,
|
||||
);
|
||||
assert_failure(
|
||||
provider
|
||||
.call_operation(request(
|
||||
"/document",
|
||||
None,
|
||||
provider.reader.reference(),
|
||||
Some(b"schema-v1"),
|
||||
"missing",
|
||||
BTreeMap::new(),
|
||||
))
|
||||
.unwrap_err(),
|
||||
CallErrorKind::OperationNotFound,
|
||||
ProtocolErrorCode::OperationNotFound,
|
||||
);
|
||||
assert_failure(
|
||||
provider
|
||||
.call_operation(request(
|
||||
"/document",
|
||||
None,
|
||||
provider.reader.reference(),
|
||||
Some(b"schema-v1"),
|
||||
"add",
|
||||
BTreeMap::new(),
|
||||
))
|
||||
.unwrap_err(),
|
||||
CallErrorKind::ValidatorRequired,
|
||||
ProtocolErrorCode::ValidatorRequired,
|
||||
);
|
||||
assert_failure(
|
||||
provider
|
||||
.call_operation(request(
|
||||
"/document",
|
||||
Some(b"object-v1"),
|
||||
provider.reader.reference(),
|
||||
Some(b"schema-v1"),
|
||||
"add",
|
||||
BTreeMap::from([("left".into(), Value::String("wrong".into()))]),
|
||||
))
|
||||
.unwrap_err(),
|
||||
CallErrorKind::InvalidArguments,
|
||||
ProtocolErrorCode::InvalidArguments,
|
||||
);
|
||||
|
||||
let mut registry = InterfaceRegistry::new();
|
||||
let failing = registry.register(FailingDefinition, None).unwrap();
|
||||
let publication = PublishedObject::new(Document::new("failure"))
|
||||
.expose(&failing)
|
||||
.unwrap();
|
||||
let error = publication
|
||||
.dispatch(request(
|
||||
"failure",
|
||||
None,
|
||||
failing.reference(),
|
||||
None,
|
||||
"fail",
|
||||
BTreeMap::new(),
|
||||
))
|
||||
.unwrap_err();
|
||||
assert_eq!(error.kind(), CallErrorKind::Implementation);
|
||||
assert_eq!(error.protocol_error().code, ProtocolErrorCode::Internal);
|
||||
provider.deny.set(true);
|
||||
assert_failure(
|
||||
provider
|
||||
.call_operation(request(
|
||||
"/document",
|
||||
Some(b"object-v1"),
|
||||
provider.reader.reference(),
|
||||
Some(b"schema-v1"),
|
||||
"label",
|
||||
BTreeMap::new(),
|
||||
))
|
||||
.unwrap_err(),
|
||||
CallErrorKind::PermissionDenied,
|
||||
ProtocolErrorCode::PermissionDenied,
|
||||
);
|
||||
assert_eq!(provider.authorization_checks.get(), 1);
|
||||
assert_eq!(provider.calls.get(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn immutable_interface_rejects_a_supplied_validator() {
|
||||
let (_, provider, _) = registry_and_provider();
|
||||
assert_failure(
|
||||
provider
|
||||
.call_operation(request(
|
||||
"/open",
|
||||
None,
|
||||
provider.writer.reference(),
|
||||
Some(b"unexpected"),
|
||||
"label",
|
||||
BTreeMap::new(),
|
||||
))
|
||||
.unwrap_err(),
|
||||
CallErrorKind::InterfaceValidatorMismatch,
|
||||
ProtocolErrorCode::InterfaceValidatorMismatch,
|
||||
);
|
||||
assert_eq!(provider.calls.get(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_results_and_backend_failures_keep_distinct_protocol_codes() {
|
||||
let (_, provider, _) = registry_and_provider();
|
||||
|
||||
assert_failure(
|
||||
provider
|
||||
.call_operation(request(
|
||||
"/broken",
|
||||
None,
|
||||
provider.broken.reference(),
|
||||
None,
|
||||
"broken",
|
||||
BTreeMap::new(),
|
||||
))
|
||||
.unwrap_err(),
|
||||
CallErrorKind::InvalidResult,
|
||||
ProtocolErrorCode::InvalidResult,
|
||||
);
|
||||
assert_failure(
|
||||
provider
|
||||
.call_operation(request(
|
||||
"/open",
|
||||
None,
|
||||
provider.reader.reference(),
|
||||
Some(b"schema-v1"),
|
||||
"fail",
|
||||
BTreeMap::new(),
|
||||
))
|
||||
.unwrap_err(),
|
||||
CallErrorKind::Implementation,
|
||||
ProtocolErrorCode::Internal,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use wip_host::{InterfaceRegistry, ObjectId, PublishedObject, WipObject};
|
||||
use wip_host::{InterfaceRegistry, PublishedObject, WipObject};
|
||||
use wip_interface::{OperationResult, interface};
|
||||
|
||||
#[interface]
|
||||
@@ -9,8 +9,8 @@ pub trait Search {
|
||||
struct Object;
|
||||
|
||||
impl WipObject for Object {
|
||||
fn id(&self) -> ObjectId {
|
||||
ObjectId("object".into())
|
||||
fn name(&self) -> &str {
|
||||
"object"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user