use std::cell::Cell; use std::collections::BTreeMap; use std::rc::Rc; use wip_host::{ CallError, CallErrorKind, CallRequest, InterfaceRef, InterfaceRegistry, InterfaceTarget, ObjectProvider, PublishedObject, RegisteredInterface, Target, Validator, WipObject, }; use wip_interface::{ CodecError, HostError, OperationResult, TypeDeclaration, TypeExpr, Value, WipType, interface, }; use wip_protocol::ProtocolErrorCode; #[interface] pub trait Reader { /// Returns the reader-qualified label. fn label(&self) -> OperationResult; /// Adds two integers. fn add(&self, left: i64, right: i64) -> OperationResult; /// Reports a backend failure. fn fail(&self) -> 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 { name: String, description: Option, reference: Option, validator: Option, require_validator: bool, calls: Rc>, private_backend_handle: String, } impl Document { fn open(name: &str, reference: Option<&str>, calls: Rc>) -> Self { Self { name: name.into(), description: Some("public document".into()), reference: reference.map(str::to_owned), validator: None, require_validator: false, calls, private_backend_handle: "/private/backend/path".into(), } } fn strict(name: &str, reference: &str, validator: &[u8], calls: Rc>) -> Self { Self { name: name.into(), description: Some("public document".into()), reference: Some(reference.into()), validator: Some(Validator::new(validator)), require_validator: true, calls, private_backend_handle: "secret-handle".into(), } } fn record_call(&self) { self.calls.set(self.calls.get() + 1); } } impl WipObject for Document { 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, _: &InterfaceRef, _: &str) -> 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) } fn fail(&self) -> OperationResult { self.record_call(); Err(HostError::new("backend unavailable")) } } 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) } } struct TestProvider { reader: RegisteredInterface, writer: RegisteredInterface, broken: RegisteredInterface, calls: Rc>, resolutions: Cell, authorization_checks: Cell, deny: Cell, switched: Cell, } impl TestProvider { fn new( reader: RegisteredInterface, writer: RegisteredInterface, broken: RegisteredInterface, ) -> 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( &self, path: &str, operation: impl FnOnce(&PublishedObject) -> R, ) -> Option { 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, ) { 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( path: &str, object_validator: Option<&[u8]>, reference: &InterfaceRef, interface_validator: Option<&[u8]>, operation: &str, arguments: BTreeMap, ) -> CallRequest { CallRequest { target: Target { path: path.into(), validator: object_validator.map(<[u8]>::to_vec), }, interface: InterfaceTarget { reference: reference.as_str().to_owned(), validator: interface_validator.map(<[u8]>::to_vec), }, operation: operation.into(), arguments, } } fn assert_failure(error: CallError, kind: CallErrorKind, code: ProtocolErrorCode) { assert_eq!(error.kind(), kind); assert_eq!(error.protocol_error().code, code); } #[test] fn registry_returns_atomic_canonical_observation_without_rewriting_descriptor() { let (registry, provider, _) = registry_and_provider(); let reference = provider.reader.reference(); 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 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![ provider.writer.reference().as_str().to_owned(), provider.reader.reference().as_str().to_owned(), ] ); assert!(!format!("{object:?}").contains("secret-handle")); assert!(!format!("{object:?}").contains("/document")); 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, "/private/backend/path" ); let error = match publication.expose(&provider.reader) { Ok(_) => panic!("duplicate exposure unexpectedly succeeded"), Err(error) => error, }; assert!( error .to_string() .contains(provider.reader.reference().as_str()) ); } #[test] fn paths_are_resolution_context_not_object_identity() { let (_, provider, _) = registry_and_provider(); 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 path_dispatch_is_interface_qualified_and_resolves_once_per_call() { let (_, provider, _) = registry_and_provider(); let before = provider.resolutions.get(); let writer = provider .call_operation(request( "/open", None, provider.writer.reference(), None, "label", BTreeMap::new(), )) .unwrap(); let reader = provider .call_operation(request( "/open", None, provider.reader.reference(), Some(b"schema-v1"), "label", BTreeMap::new(), )) .unwrap(); 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 call_failures_follow_canonical_precedence_without_execution() { let (_, provider, hidden) = registry_and_provider(); let resolutions = provider.resolutions.get(); for invalid_path in [ "relative", "/trailing/", "/double//segment", "/dot/./segment", "/parent/../segment", ] { assert_failure( provider .call_operation(request( invalid_path, Some(b"stale"), hidden.reference(), Some(b"stale"), "missing", BTreeMap::new(), )) .unwrap_err(), CallErrorKind::InvalidRequest, ProtocolErrorCode::InvalidRequest, ); } assert_eq!(provider.resolutions.get(), resolutions); assert_failure( provider.fetch("/invalid/").unwrap_err(), CallErrorKind::InvalidRequest, ProtocolErrorCode::InvalidRequest, ); assert_eq!(provider.resolutions.get(), resolutions); 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, ); 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, ); }