use std::collections::BTreeMap; use wip_interface::wip_protocol::{INTERFACE_FORMAT_V1, TypeExpr, Value}; use wip_interface::{ DispatchError, DispatchPhase, EffectGuarantee, HostError, HostErrorKind, OperationResult, WipType, interface, }; /// Publication request. #[derive(Debug, Clone, PartialEq, WipType)] pub struct PublishRequest { /// Item title. pub title: String, } /// Application outcome, including domain failure. #[derive(Debug, Clone, PartialEq, WipType)] pub enum PublishOutcome { /// The item was accepted. Published(bool), /// The application rejected the item. Rejected(String), } /// Catalog editing capability. /// /// The shape is implementation-independent. #[interface] pub trait Catalog { /// Publishes one item. /// /// This operation can report a domain rejection in its application union. /// /// # Returns /// The application publication outcome. fn publish( &self, /// Item to publish. request: PublishRequest, /// Optional audit note. note: Option, ) -> OperationResult; /// Checks connectivity without returning application data. fn ping(&self) -> OperationResult<()>; /// Demonstrates a host/runtime failure. /// /// # Returns /// A value only when the host succeeds. fn host_value(&self) -> OperationResult; } struct AcceptingCatalog; impl Catalog for AcceptingCatalog { fn publish( &self, request: PublishRequest, note: Option, ) -> OperationResult { assert_eq!(request.title, "example"); assert_eq!(note.as_deref(), Some("audit")); Ok(PublishOutcome::Published(true)) } fn ping(&self) -> OperationResult<()> { Ok(()) } fn host_value(&self) -> OperationResult { Err(HostError::internal( "backend unavailable", EffectGuarantee::Unknown, )) } } struct RejectingCatalog; impl Catalog for RejectingCatalog { fn publish( &self, _request: PublishRequest, _note: Option, ) -> OperationResult { Ok(PublishOutcome::Rejected("policy".into())) } fn ping(&self) -> OperationResult<()> { Ok(()) } fn host_value(&self) -> OperationResult { Ok("ready".into()) } } struct PanickingCatalog; impl Catalog for PanickingCatalog { fn publish( &self, _request: PublishRequest, _note: Option, ) -> OperationResult { panic!("private panic payload") } fn ping(&self) -> OperationResult<()> { panic!("private panic payload") } fn host_value(&self) -> OperationResult { panic!("private panic payload") } } fn publish_arguments(with_note: bool) -> BTreeMap { let mut arguments = BTreeMap::from([( "request".into(), PublishRequest { title: "example".into(), } .encode() .unwrap(), )]); if with_note { arguments.insert("note".into(), Value::String("audit".into())); } arguments } #[test] fn descriptor_is_canonical_and_generated_only_from_the_trait_declaration() { let first = CatalogDefinition::descriptor(); let second = CatalogDefinition::descriptor(); assert_eq!(first, second); first.validate().unwrap(); assert_eq!(first.format, INTERFACE_FORMAT_V1); assert_eq!( first.documentation.as_ref().unwrap().summary, "Catalog editing capability." ); assert_eq!( first.documentation.as_ref().unwrap().details.as_deref(), Some("The shape is implementation-independent.") ); assert_eq!(first.operations.len(), 3); let publish = &first.operations[0]; assert_eq!(publish.name, "publish"); assert_eq!(publish.parameters[0].name, "request"); assert_eq!( publish.parameters[0] .documentation .as_ref() .unwrap() .summary, "Item to publish." ); assert!(publish.parameters[0].required); assert!(!publish.parameters[1].required); assert_eq!( publish.returns.documentation.as_ref().unwrap().summary, "The application publication outcome." ); assert_eq!( publish.returns.r#type, TypeExpr::Named { name: "PublishOutcome".into(), } ); assert_eq!(first.operations[1].returns.r#type, TypeExpr::Unit); assert!( first .types .iter() .any(|declaration| matches!(declaration.definition, TypeExpr::Record { .. })) ); assert!( first .types .iter() .any(|declaration| matches!(declaration.definition, TypeExpr::Union { .. })) ); // Both implementation types satisfy one immutable generated definition; // implementation selection cannot contribute fields to the descriptor. fn accepts_definition(_: &T) {} accepts_definition(&AcceptingCatalog); accepts_definition(&RejectingCatalog); assert_eq!(CatalogDefinition::descriptor(), first); } #[test] fn generated_argument_decoder_and_result_encoder_use_raw_values() { let decoded = CatalogPublishArguments::decode(publish_arguments(false)).unwrap(); assert_eq!(decoded.request.title, "example"); assert_eq!(decoded.note, None); let encoded = CatalogPublishArguments::encode_result(PublishOutcome::Rejected("domain".into())).unwrap(); assert_eq!( PublishOutcome::decode(encoded).unwrap(), PublishOutcome::Rejected("domain".into()) ); assert_eq!( CatalogPingArguments::encode_result(()).unwrap(), Value::Unit ); } #[test] fn typed_dispatch_validates_decodes_invokes_encodes_and_validates() { let value = CatalogDefinition::dispatch(&AcceptingCatalog, "publish", publish_arguments(true)).unwrap(); assert_eq!( PublishOutcome::decode(value).unwrap(), PublishOutcome::Published(true) ); assert_eq!( CatalogDefinition::dispatch(&AcceptingCatalog, "ping", BTreeMap::new()).unwrap(), Value::Unit ); let invalid = CatalogDefinition::dispatch(&AcceptingCatalog, "publish", BTreeMap::new()).unwrap_err(); assert!(matches!(invalid, DispatchError::InvalidArguments(_))); assert_eq!(invalid.phase(), DispatchPhase::BeforeInvocation); assert_eq!(invalid.effect_guarantee(), None); let host = CatalogDefinition::dispatch(&AcceptingCatalog, "host_value", BTreeMap::new()).unwrap_err(); assert!( matches!(&host, DispatchError::Host(error) if error.message() == "backend unavailable") ); assert_eq!(host.phase(), DispatchPhase::AfterInvocation); assert_eq!(host.effect_guarantee(), Some(EffectGuarantee::Unknown)); let panic = CatalogDefinition::dispatch(&PanickingCatalog, "host_value", BTreeMap::new()).unwrap_err(); assert!(matches!(panic, DispatchError::Panic)); assert_eq!(panic.phase(), DispatchPhase::AfterInvocation); assert_eq!(panic.effect_guarantee(), Some(EffectGuarantee::Unknown)); assert!(!panic.to_string().contains("private panic payload")); let unknown = CatalogDefinition::dispatch(&AcceptingCatalog, "missing", BTreeMap::new()).unwrap_err(); assert!(matches!(unknown, DispatchError::UnknownOperation(name) if name == "missing")); } #[test] fn host_errors_require_a_typed_kind_and_effect_guarantee() { let internal = HostError::internal("x".repeat(4_096), EffectGuarantee::NotCommitted); assert_eq!(internal.kind(), HostErrorKind::Internal); assert_eq!(internal.effect_guarantee(), EffectGuarantee::NotCommitted); assert!(internal.message().len() <= 512); let resource = HostError::resource_limit("deadline", EffectGuarantee::Unknown); assert_eq!(resource.kind(), HostErrorKind::ResourceLimitExceeded); assert_eq!(resource.effect_guarantee(), EffectGuarantee::Unknown); } /// Operations whose distinct source names normalize to the same PascalCase. #[interface] pub trait DistinctOperations { /// Returns the first item marker. fn item(&self) -> OperationResult; /// Returns the underscored item marker. fn item_(&self) -> OperationResult; } struct DistinctImplementation; impl DistinctOperations for DistinctImplementation { fn item(&self) -> OperationResult { Ok("item".into()) } fn item_(&self) -> OperationResult { Ok("item_".into()) } } #[test] fn distinct_operation_names_always_generate_distinct_public_items() { let descriptor = DistinctOperationsDefinition::descriptor(); assert_eq!(descriptor.operations[0].name, "item"); assert_eq!(descriptor.operations[1].name, "item_"); let first = DistinctOperationsDefinition::dispatch(&DistinctImplementation, "item", BTreeMap::new()) .unwrap(); let second = DistinctOperationsDefinition::dispatch(&DistinctImplementation, "item_", BTreeMap::new()) .unwrap(); assert_eq!(first, Value::String("item".into())); assert_eq!(second, Value::String("item_".into())); }