feat(host): add object publication and dispatch

This commit is contained in:
2026-09-28 23:36:50 +09:00
parent 3f94d48f04
commit 32f3fb6fb1
11 changed files with 1267 additions and 8 deletions
+5
View File
@@ -0,0 +1,5 @@
#[test]
fn rejects_exposing_an_unimplemented_interface() {
let tests = trybuild::TestCases::new();
tests.compile_fail("tests/ui/unimplemented_interface.rs");
}
+488
View File
@@ -0,0 +1,488 @@
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");
let observation = registry.observe(reader.reference()).unwrap();
assert_eq!(observation.reference, *reader.reference());
assert_eq!(observation.descriptor.name, "Reader");
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());
}
#[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()
]
);
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
);
}
@@ -0,0 +1,22 @@
use wip_host::{InterfaceRegistry, PublishedObject, WipObject};
use wip_interface::{OperationResult, interface};
use wip_protocol::ObjectId;
#[interface]
pub trait Search {
fn search(&self) -> OperationResult<String>;
}
struct Object;
impl WipObject for Object {
fn id(&self) -> ObjectId {
ObjectId("object".into())
}
}
fn main() {
let mut registry = InterfaceRegistry::new();
let search = registry.register(SearchDefinition, None).unwrap();
let _published = PublishedObject::new(Object).expose(&search).unwrap();
}
@@ -0,0 +1,34 @@
error[E0277]: the trait bound `Object: Search` is not satisfied
--> tests/ui/unimplemented_interface.rs:21:58
|
21 | let _published = PublishedObject::new(Object).expose(&search).unwrap();
| ------ ^^^^^^^ unsatisfied trait bound
| |
| required by a bound introduced by this call
|
help: the trait `Search` is not implemented for `Object`
--> tests/ui/unimplemented_interface.rs:10:1
|
10 | struct Object;
| ^^^^^^^^^^^^^
help: this trait has no implementations, consider adding one
--> tests/ui/unimplemented_interface.rs:6:1
|
6 | pub trait Search {
| ^^^^^^^^^^^^^^^^
note: required for `SearchDefinition` to implement `InterfaceImplementation<Object>`
--> tests/ui/unimplemented_interface.rs:5:1
|
5 | #[interface]
| ^^^^^^^^^^^^
6 | pub trait Search {
| ^^^^^^
note: required by a bound in `PublishedObject::<T>::expose`
--> src/lib.rs
|
| pub fn expose<D>(mut self, interface: &RegisteredInterface<D>) -> Result<Self, PublicationError>
| ------ required by a bound in this associated function
| where
| D: InterfaceImplementation<T>,
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `PublishedObject::<T>::expose`
= note: this error originates in the attribute macro `interface` (in Nightly builds, run with -Z macro-backtrace for more info)