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; /// Adds two integers. fn add(&self, left: i64, right: i64) -> OperationResult; } #[interface] pub trait Writer { /// Returns the writer-qualified label. fn label(&self) -> OperationResult; } #[interface] pub trait Hidden { /// Returns a hidden value. fn hidden(&self) -> OperationResult; } #[interface] pub trait Broken { /// Returns a deliberately invalid encoded value. fn broken(&self) -> OperationResult; } #[derive(Debug)] pub struct BadValue; impl WipType for BadValue { fn type_expr() -> TypeExpr { TypeExpr::Integer } fn collect_declarations(_: &mut Vec) {} fn encode(self) -> Result { Ok(Value::String("not an integer".into())) } fn decode(_: Value) -> Result { Ok(Self) } } struct Document { id: String, validator: Option, require_validator: bool, calls: Cell, private_backend_handle: RefCell, } 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 { self.record_call(); Ok("reader".into()) } fn add(&self, left: i64, right: i64) -> OperationResult { self.record_call(); Ok(left + right) } } impl Writer for Document { fn label(&self) -> OperationResult { self.record_call(); Ok("writer".into()) } } impl Hidden for Document { fn hidden(&self) -> OperationResult { self.record_call(); Ok("hidden".into()) } } impl Broken for Document { fn broken(&self) -> OperationResult { 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, ) -> 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; } impl Failing for Document { fn fail(&self) -> OperationResult { 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 ); }