508 lines
14 KiB
Rust
508 lines
14 KiB
Rust
use std::cell::{Cell, RefCell};
|
|
|
|
use wip_host::{
|
|
CallErrorKind, CallRequest, InterfaceRegistry, InterfaceTarget, PublishedObject, Validator,
|
|
WipObject,
|
|
};
|
|
use wip_interface::{
|
|
CodecError, HostError, NamedValue, OperationResult, TypeDeclaration, TypeExpr, Value, WipType,
|
|
interface,
|
|
};
|
|
use wip_protocol::{ObjectId, ProtocolErrorCategory};
|
|
|
|
#[interface]
|
|
pub trait Reader {
|
|
/// Returns the reader-qualified label.
|
|
fn label(&self) -> OperationResult<String>;
|
|
|
|
/// Adds two integers.
|
|
fn add(&self, left: i64, right: i64) -> OperationResult<i64>;
|
|
}
|
|
|
|
#[interface]
|
|
pub trait Writer {
|
|
/// Returns the writer-qualified label.
|
|
fn label(&self) -> OperationResult<String>;
|
|
}
|
|
|
|
#[interface]
|
|
pub trait Hidden {
|
|
/// Returns a hidden value.
|
|
fn hidden(&self) -> OperationResult<String>;
|
|
}
|
|
|
|
#[interface]
|
|
pub trait Broken {
|
|
/// Returns a deliberately invalid encoded value.
|
|
fn broken(&self) -> OperationResult<BadValue>;
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct BadValue;
|
|
|
|
impl WipType for BadValue {
|
|
fn type_expr() -> TypeExpr {
|
|
TypeExpr::Integer
|
|
}
|
|
|
|
fn collect_declarations(_: &mut Vec<TypeDeclaration>) {}
|
|
|
|
fn encode(self) -> Result<Value, CodecError> {
|
|
Ok(Value::String("not an integer".into()))
|
|
}
|
|
|
|
fn decode(_: Value) -> Result<Self, CodecError> {
|
|
Ok(Self)
|
|
}
|
|
}
|
|
|
|
struct Document {
|
|
id: String,
|
|
validator: Option<Validator>,
|
|
require_validator: bool,
|
|
calls: Cell<usize>,
|
|
private_backend_handle: RefCell<String>,
|
|
}
|
|
|
|
impl Document {
|
|
fn new(id: &str) -> Self {
|
|
Self {
|
|
id: id.into(),
|
|
validator: None,
|
|
require_validator: false,
|
|
calls: Cell::new(0),
|
|
private_backend_handle: RefCell::new("/private/backend/path".into()),
|
|
}
|
|
}
|
|
|
|
fn strict(id: &str, validator: &[u8]) -> Self {
|
|
Self {
|
|
id: id.into(),
|
|
validator: Some(Validator::new(validator)),
|
|
require_validator: true,
|
|
calls: Cell::new(0),
|
|
private_backend_handle: RefCell::new("secret-handle".into()),
|
|
}
|
|
}
|
|
|
|
fn record_call(&self) {
|
|
self.calls.set(self.calls.get() + 1);
|
|
}
|
|
}
|
|
|
|
impl WipObject for Document {
|
|
fn id(&self) -> ObjectId {
|
|
ObjectId(self.id.clone())
|
|
}
|
|
|
|
fn validator(&self) -> Option<&Validator> {
|
|
self.validator.as_ref()
|
|
}
|
|
|
|
fn requires_validator(&self) -> bool {
|
|
self.require_validator
|
|
}
|
|
}
|
|
|
|
impl Reader for Document {
|
|
fn label(&self) -> OperationResult<String> {
|
|
self.record_call();
|
|
Ok("reader".into())
|
|
}
|
|
|
|
fn add(&self, left: i64, right: i64) -> OperationResult<i64> {
|
|
self.record_call();
|
|
Ok(left + right)
|
|
}
|
|
}
|
|
|
|
impl Writer for Document {
|
|
fn label(&self) -> OperationResult<String> {
|
|
self.record_call();
|
|
Ok("writer".into())
|
|
}
|
|
}
|
|
|
|
impl Hidden for Document {
|
|
fn hidden(&self) -> OperationResult<String> {
|
|
self.record_call();
|
|
Ok("hidden".into())
|
|
}
|
|
}
|
|
|
|
impl Broken for Document {
|
|
fn broken(&self) -> OperationResult<BadValue> {
|
|
self.record_call();
|
|
Ok(BadValue)
|
|
}
|
|
}
|
|
|
|
fn request(
|
|
object: &str,
|
|
object_validator: Option<&[u8]>,
|
|
reference: &wip_host::InterfaceRef,
|
|
interface_validator: Option<&[u8]>,
|
|
operation: &str,
|
|
arguments: Vec<NamedValue>,
|
|
) -> CallRequest {
|
|
CallRequest {
|
|
object: ObjectId(object.into()),
|
|
object_validator: object_validator.map(Validator::new),
|
|
interface: InterfaceTarget {
|
|
reference: reference.clone(),
|
|
validator: interface_validator.map(Validator::new),
|
|
},
|
|
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.name, reader.reference().as_str());
|
|
assert_eq!(observation.validator, None);
|
|
observation.descriptor.validate().unwrap();
|
|
|
|
assert!(registry.register(ReaderDefinition, None).is_err());
|
|
|
|
let mut other_registry = InterfaceRegistry::new();
|
|
let other_reader = other_registry.register(ReaderDefinition, None).unwrap();
|
|
assert_ne!(reader.reference(), other_reader.reference());
|
|
assert!(
|
|
registry
|
|
.resolve(other_reader.reference().as_str())
|
|
.is_none()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
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();
|
|
|
|
let publication = PublishedObject::new(Document::new("document-1"))
|
|
.expose(&writer)
|
|
.unwrap()
|
|
.expose(&reader)
|
|
.unwrap();
|
|
|
|
let object = publication.protocol_object();
|
|
assert_eq!(
|
|
object.interfaces,
|
|
vec![
|
|
writer.reference().as_str().to_owned(),
|
|
reader.reference().as_str().to_owned()
|
|
]
|
|
);
|
|
wip_protocol::InterfaceObservation {
|
|
object: object.clone(),
|
|
descriptors: vec![
|
|
registry.observe(writer.reference()).unwrap().descriptor,
|
|
registry.observe(reader.reference()).unwrap().descriptor,
|
|
],
|
|
}
|
|
.validate()
|
|
.unwrap();
|
|
assert!(!format!("{object:?}").contains("private/backend/path"));
|
|
|
|
let writer_result = publication
|
|
.dispatch(request(
|
|
"document-1",
|
|
None,
|
|
writer.reference(),
|
|
None,
|
|
"label",
|
|
vec![],
|
|
))
|
|
.unwrap();
|
|
let reader_result = publication
|
|
.dispatch(request(
|
|
"document-1",
|
|
None,
|
|
reader.reference(),
|
|
None,
|
|
"label",
|
|
vec![],
|
|
))
|
|
.unwrap();
|
|
assert_eq!(writer_result.value, Some(Value::String("writer".into())));
|
|
assert_eq!(reader_result.value, Some(Value::String("reader".into())));
|
|
assert_eq!(publication.implementation().calls.get(), 2);
|
|
assert_eq!(
|
|
publication
|
|
.implementation()
|
|
.private_backend_handle
|
|
.borrow()
|
|
.as_str(),
|
|
"/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",
|
|
vec![],
|
|
))
|
|
.unwrap_err();
|
|
assert_eq!(error.kind(), CallErrorKind::InterfaceMismatch);
|
|
assert_eq!(
|
|
error.protocol_error().category,
|
|
ProtocolErrorCategory::InterfaceNotFound
|
|
);
|
|
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.id, ObjectId("empty".into()));
|
|
assert!(object.interfaces.is_empty());
|
|
object.validate().unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn duplicate_exposure_is_rejected_without_reordering() {
|
|
let mut registry = InterfaceRegistry::new();
|
|
let reader = registry.register(ReaderDefinition, None).unwrap();
|
|
let publication = PublishedObject::new(Document::new("duplicate"))
|
|
.expose(&reader)
|
|
.unwrap();
|
|
let error = match publication.expose(&reader) {
|
|
Ok(_) => panic!("duplicate exposure unexpectedly succeeded"),
|
|
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",
|
|
vec![],
|
|
))
|
|
.unwrap_err();
|
|
assert_eq!(missing.kind(), CallErrorKind::OperationNotFound);
|
|
assert_eq!(
|
|
missing.protocol_error().category,
|
|
ProtocolErrorCategory::OperationNotFound
|
|
);
|
|
|
|
let invalid = publication
|
|
.dispatch(request(
|
|
"validation",
|
|
None,
|
|
reader.reference(),
|
|
None,
|
|
"add",
|
|
vec![NamedValue {
|
|
name: "left".into(),
|
|
value: Value::String("wrong".into()),
|
|
}],
|
|
))
|
|
.unwrap_err();
|
|
assert_eq!(invalid.kind(), CallErrorKind::InvalidArguments);
|
|
assert_eq!(
|
|
invalid.protocol_error().category,
|
|
ProtocolErrorCategory::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();
|
|
|
|
let error = publication
|
|
.dispatch(request(
|
|
"broken",
|
|
None,
|
|
broken.reference(),
|
|
None,
|
|
"broken",
|
|
vec![],
|
|
))
|
|
.unwrap_err();
|
|
assert_eq!(error.kind(), CallErrorKind::InvalidResult);
|
|
assert_eq!(
|
|
error.protocol_error().category,
|
|
ProtocolErrorCategory::Internal
|
|
);
|
|
assert_eq!(publication.implementation().calls.get(), 1);
|
|
}
|
|
|
|
#[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();
|
|
|
|
let missing = publication
|
|
.dispatch(request(
|
|
"strict",
|
|
None,
|
|
reader.reference(),
|
|
None,
|
|
"label",
|
|
vec![],
|
|
))
|
|
.unwrap_err();
|
|
assert_eq!(missing.kind(), CallErrorKind::ValidatorRequired);
|
|
|
|
let mismatch = publication
|
|
.dispatch(request(
|
|
"strict",
|
|
Some(b"stale"),
|
|
reader.reference(),
|
|
None,
|
|
"label",
|
|
vec![],
|
|
))
|
|
.unwrap_err();
|
|
assert_eq!(mismatch.kind(), CallErrorKind::ValidatorMismatch);
|
|
assert_eq!(publication.implementation().calls.get(), 0);
|
|
|
|
let result = publication
|
|
.dispatch(request(
|
|
"strict",
|
|
Some(b"object-v1"),
|
|
reader.reference(),
|
|
None,
|
|
"label",
|
|
vec![],
|
|
))
|
|
.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",
|
|
None,
|
|
writer.reference(),
|
|
None,
|
|
"label",
|
|
vec![],
|
|
))
|
|
.unwrap_err();
|
|
assert_eq!(missing.kind(), CallErrorKind::InterfaceValidatorRequired);
|
|
|
|
let mismatch = publication
|
|
.dispatch(request(
|
|
"mutable-interface",
|
|
None,
|
|
writer.reference(),
|
|
Some(b"schema-v0"),
|
|
"label",
|
|
vec![],
|
|
))
|
|
.unwrap_err();
|
|
assert_eq!(mismatch.kind(), CallErrorKind::InterfaceValidatorMismatch);
|
|
assert_eq!(publication.implementation().calls.get(), 0);
|
|
|
|
let result = publication
|
|
.dispatch(request(
|
|
"mutable-interface",
|
|
None,
|
|
writer.reference(),
|
|
Some(b"schema-v1"),
|
|
"label",
|
|
vec![],
|
|
))
|
|
.unwrap();
|
|
assert_eq!(result.value, Some(Value::String("writer".into())));
|
|
}
|
|
|
|
#[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>;
|
|
}
|
|
|
|
impl Failing for Document {
|
|
fn fail(&self) -> OperationResult<String> {
|
|
Err(HostError::new("backend unavailable"))
|
|
}
|
|
}
|
|
|
|
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",
|
|
vec![],
|
|
))
|
|
.unwrap_err();
|
|
assert_eq!(error.kind(), CallErrorKind::Implementation);
|
|
assert_eq!(
|
|
error.protocol_error().category,
|
|
ProtocolErrorCategory::Internal
|
|
);
|
|
}
|