diff --git a/Cargo.toml b/Cargo.toml index 8e01e49..5cccf92 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,22 +1,3 @@ [workspace] -members = [ - ".", - "crates/wip-host", - "crates/wip-interface", - "crates/wip-interface-macros", - "crates/wip-protocol", -] +members = ["crates/*"] resolver = "3" - -[package] -name = "wip-public-api-example" -version = "0.1.0" -edition = "2024" -description = "End-to-end example of the public WIP Rust APIs" -license = "MIT OR Apache-2.0" -publish = false - -[dependencies] -wip-host = { path = "crates/wip-host" } -wip-interface = { path = "crates/wip-interface" } -wip-protocol = { path = "crates/wip-protocol" } diff --git a/README.md b/README.md index d6b1f9b..f478614 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,12 @@ Rust building blocks for the Web Interface Protocol (WIP). -The repository currently provides a transport-independent protocol model, a -Rust interface-authoring facade, and a request-scoped host runtime. HTTP and -other transport bindings are intentionally outside the supported slice. +The repository is a resolver-3 virtual Cargo workspace whose packages all live +under `crates/*`. It provides a transport-independent protocol model, a Rust +interface-authoring facade, a request-scoped host runtime, and a private +public-API conformance consumer. The repository root is not itself a package. +HTTP and other transport bindings are intentionally outside the supported +slice. ## Crate graph and responsibilities @@ -36,19 +39,25 @@ or application traits. ## End-to-end example -[`src/lib.rs`](src/lib.rs) is the complete public-API example and -[`src/main.rs`](src/main.rs) runs it. It demonstrates all of the following: +[`crates/wip-public-api-example/src/lib.rs`](crates/wip-public-api-example/src/lib.rs) +is the complete public-API example and +[`crates/wip-public-api-example/src/main.rs`](crates/wip-public-api-example/src/main.rs) +runs it. It demonstrates all of the following: -1. public application DTOs derived as `WipType` records and unions; +1. public application DTOs derived as `WipType` records, enums, and unions, + including canonical entry paths and schema-opaque JSON; 2. two public traits declared with `#[wip::interface]`; 3. a private host struct implementing both traits; -4. explicit publication of both interfaces on one object; -5. registry observation and validation of generated descriptors; -6. calls made with interface references, operation names, and `Value` arguments; +4. explicit publication of both interfaces as an ordered set on one object; +5. separate observation of opaque interface references, descriptors, and + descriptor validators; +6. calls qualified by `target.path`, object validator, interface reference and + validator, operation, and schema-neutral `Value` arguments; 7. validated result values decoded back to the typed application result; 8. independent dispatch of the same `status` operation name on both interfaces; -9. a domain rejection returned as data and a backend failure returned as a host - error. +9. distinct domain rejection, host/backend failure, `PermissionDenied`, stale + validator recovery, interface membership, and lookup failure codes; and +10. canonical failure precedence when several request fields are invalid. Run it inside the development shell: @@ -57,10 +66,16 @@ nix develop cargo run --package wip-public-api-example ``` -The normal integration test is in [`tests/end_to_end.rs`](tests/end_to_end.rs). -Unit tests beside the example additionally cover every host dispatch failure, -invalid arguments and results, interface membership, validators, and -same-named operations. +The public-only conformance suite is in +[`crates/wip-public-api-example/tests/end_to_end.rs`](crates/wip-public-api-example/tests/end_to_end.rs). +It covers canonical object/descriptor shape, ordered interfaces, +schema-neutral record/enum/union/entry/JSON values, fully qualified calls, and +the distinct domain, host, authorization, validator-recovery, and interface +failure classes. Exhaustive public type construction and matching make legacy +ObjectId, descriptor `name`, and semantic `Value` variants compile-time +regressions. Unit tests in the public crates additionally cover every host +dispatch failure, invalid arguments and results, interface membership, +validators, and same-named operations. ## Interface authoring @@ -208,10 +223,14 @@ nix develop cargo fmt --all -- --check cargo check --workspace --all-targets cargo test --workspace --all-targets +cargo test --workspace --doc cargo clippy --workspace --all-targets -- -D warnings +RUSTDOCFLAGS='-D warnings' cargo doc --workspace --no-deps +cargo run --package wip-public-api-example +git diff --check ``` -`cargo test --workspace --all-targets` includes the public example integration -test, runtime failure tests, and `trybuild` compile-fail suites. Run -`cargo test --workspace --doc` when validating the checked rustdoc examples -separately. +`cargo test --workspace --all-targets` includes the public-only conformance +consumer, runtime failure tests, and `trybuild` compile-fail suites. The separate +doctest, rustdoc, executable, and whitespace checks are all required acceptance +steps. diff --git a/crates/wip-public-api-example/Cargo.toml b/crates/wip-public-api-example/Cargo.toml new file mode 100644 index 0000000..77a165b --- /dev/null +++ b/crates/wip-public-api-example/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "wip-public-api-example" +version = "0.1.0" +edition = "2024" +description = "Public-only end-to-end conformance consumer for the WIP Rust APIs" +license = "MIT OR Apache-2.0" +publish = false + +[dependencies] +wip-host = { path = "../wip-host" } +wip-interface = { path = "../wip-interface" } +wip-protocol = { path = "../wip-protocol" } diff --git a/src/lib.rs b/crates/wip-public-api-example/src/lib.rs similarity index 53% rename from src/lib.rs rename to crates/wip-public-api-example/src/lib.rs index 36c5644..38baae9 100644 --- a/src/lib.rs +++ b/crates/wip-public-api-example/src/lib.rs @@ -1,9 +1,37 @@ -//! End-to-end use of the public WIP protocol, interface, and host APIs. +//! Public-only conformance consumer for the WIP Rust crates. //! -//! This workspace package defines application DTOs and interfaces with -//! `wip-interface`, keeps its implementation private, publishes it through a -//! request-scoped `wip-host` provider, and crosses the call boundary only with -//! canonical `wip-protocol` values and path targets. +//! # Architectural boundary +//! +//! This package is an ordinary application consumer. Its direct dependencies are +//! the public `wip-protocol`, `wip-interface`, and `wip-host` crates; it neither +//! imports workspace-private modules nor depends on `wip-interface-macros`. +//! `wip-protocol` owns transport-independent DTOs and schema-neutral values, +//! while this package owns its application DTOs and interface traits. The +//! request-scoped implementation, backend handle, authorization policy, and +//! publication wiring remain host-private. A transport binding would translate +//! its own wire routes and envelopes to the protocol call DTOs; no HTTP type is +//! part of an application trait or interface descriptor. +//! +//! # Breaking migration exercised here +//! +//! The canonical API targets operations by `target.path` rather than a mandatory +//! object identifier. Object `ref` and object/interface validators are separate, +//! optional observation fields. Interface references are opaque values observed +//! separately from descriptors, and descriptors contain neither a Rust trait +//! name nor an opaque-reference `name` field. Application records, enums, +//! unions, entry paths, and JSON all encode through the schema-neutral [`Value`] +//! variants: entries and enums are strings, unions are discriminator records, +//! and JSON recursively uses ordinary records/lists/primitives. The former +//! `Entry` and semantic `Value::Entry`, `Value::Enum`, `Value::Union`, and +//! `Value::Json` forms are intentionally unsupported. +//! +//! # Supported authoring subset +//! +//! Public, non-generic traits use immutable `&self` receivers, owned named +//! parameters, and explicit `wip::OperationResult` returns. Derived DTOs are +//! named-field records, all-unit enums, or unions with unit and single-payload +//! cases. A direct `Option` means field/argument omission, never protocol +//! null. See `wip-interface` for the complete rejected-syntax list. #![deny(missing_docs)] @@ -17,19 +45,34 @@ use wip_host::{ }; use wip_interface as wip; use wip_interface::WipType; -use wip_protocol::ProtocolError; +use wip_protocol::{ProtocolError, ProtocolErrorCode, Value}; const OBJECT_PATH: &str = "/article-service"; const OBJECT_VALIDATOR: &[u8] = b"object-v1"; const CATALOG_VALIDATOR: &[u8] = b"catalog-v1"; -/// An application-owned request DTO mapped to a protocol record. +/// Publication visibility represented as a protocol enum string. #[derive(Debug, Clone, PartialEq, Eq, wip::WipType)] +pub enum Visibility { + /// Visible to every authorized caller. + Public, + /// Visible only inside the application domain. + Private, +} + +/// An application-owned request DTO mapped to a protocol record. +#[derive(Debug, Clone, PartialEq, wip::WipType)] pub struct PublishRequest { /// Title of the article to publish. pub title: String, /// Optional article body; omission is distinct from JSON null. pub body: Option, + /// Canonical Worldspace path to the source entry. + pub source: wip::Entry, + /// Application enum represented by a schema-neutral string. + pub visibility: Visibility, + /// Schema-opaque JSON represented by ordinary protocol values. + pub metadata: wip::JsonValue, } /// The application domain result of a publication attempt. @@ -59,6 +102,9 @@ pub trait Catalog { /// Demonstrates a host/backend failure rather than a domain result. fn backend_status(&self) -> wip::OperationResult; + + /// Demonstrates denial after every structural precondition succeeds. + fn restricted_status(&self) -> wip::OperationResult; } /// Audit operations exposed by the same object. @@ -78,15 +124,25 @@ pub trait Diagnostics { /// Observable outcomes from the complete public-API scenario. #[derive(Debug, Clone, PartialEq)] pub struct DemoReport { + /// Path-segment name from the canonical object projection. + pub object_name: String, + /// Optional object identity, distinct from the resolution path. + pub object_reference: Option, + /// Optional object-state validator. + pub object_validator: Option>, /// Opaque interface references published by the object, in exposure order. pub published_interfaces: Vec, - /// Descriptor format observed for the catalog interface. + /// Opaque reference assigned to the catalog descriptor. + pub catalog_interface_reference: String, + /// Opaque reference assigned to the audit descriptor. + pub audit_interface_reference: String, + /// Descriptor format observed separately for the catalog reference. pub catalog_descriptor_format: String, - /// Descriptor format observed for the audit interface. + /// Descriptor format observed separately for the audit reference. pub audit_descriptor_format: String, - /// Typed successful application result decoded from a validated `Value`. + /// Typed successful application result decoded from a validated [`Value`]. pub published: PublishOutcome, - /// Typed application-domain rejection decoded from a validated `Value`. + /// Typed application-domain rejection decoded from a validated [`Value`]. pub rejected: PublishOutcome, /// Result of the catalog-qualified `status` operation. pub catalog_status: String, @@ -96,13 +152,23 @@ pub struct DemoReport { pub host_failure: CallErrorKind, /// A protocol-level failure built from the host failure. pub protocol_failure: ProtocolError, + /// Authorization failure after a fully qualified valid request. + pub permission_failure: ProtocolErrorCode, + /// Recovery signal for a stale object validator. + pub validator_failure: ProtocolErrorCode, + /// Failure for a known but unexposed interface reference. + pub interface_mismatch: ProtocolErrorCode, + /// Failure for an unknown interface observation reference. + pub interface_not_found: ProtocolErrorCode, + /// First failure for a request with an invalid path and later invalid fields. + pub precedence_failure: ProtocolErrorCode, } -/// Runs the complete end-to-end example. +/// Runs the complete end-to-end conformance example. /// /// The provider resolves the current object from a Worldspace path for each /// call. Interface observations remain separate from object projection, and all -/// calls carry independent object and interface validators. +/// successful calls carry independent object and interface validators. /// /// # Errors /// @@ -112,7 +178,7 @@ pub fn run() -> Result> { let mut registry = InterfaceRegistry::new(); let catalog = registry.register(CatalogDefinition, Some(Validator::new(CATALOG_VALIDATOR)))?; let audit = registry.register(AuditDefinition, None)?; - let _diagnostics = registry.register(DiagnosticsDefinition, None)?; + let diagnostics = registry.register(DiagnosticsDefinition, None)?; let provider = ApplicationProvider { catalog: catalog.clone(), audit: audit.clone(), @@ -138,6 +204,12 @@ pub fn run() -> Result> { PublishRequest { title: "Public APIs".into(), body: Some("protocol values in, validated values out".into()), + source: wip::Entry::new("/drafts/public-apis")?, + visibility: Visibility::Public, + metadata: wip::JsonValue::Object(vec![wip::JsonMember { + name: "reviewed".into(), + value: wip::JsonValue::Boolean(true), + }]), }, )?; let rejected = dispatch_publish( @@ -146,11 +218,15 @@ pub fn run() -> Result> { PublishRequest { title: String::new(), body: None, + source: wip::Entry::new("/drafts/untitled")?, + visibility: Visibility::Private, + metadata: wip::JsonValue::Null, }, )?; let catalog_status = dispatch_string( &provider, request( + OBJECT_PATH, catalog.reference(), Some(CATALOG_VALIDATOR), "status", @@ -159,28 +235,97 @@ pub fn run() -> Result> { )?; let audit_status = dispatch_string( &provider, - request(audit.reference(), None, "status", BTreeMap::new()), + request( + OBJECT_PATH, + audit.reference(), + None, + "status", + BTreeMap::new(), + ), )?; let host_error = provider .call_operation(request( + OBJECT_PATH, catalog.reference(), Some(CATALOG_VALIDATOR), "backend_status", BTreeMap::new(), )) - .unwrap_err(); - if host_error.kind() != CallErrorKind::Implementation { - return Err(io::Error::other(format!( - "unexpected host failure kind: {:?}", - host_error.kind() - )) - .into()); - } + .expect_err("backend_status must model a host failure"); + ensure_kind(&host_error, CallErrorKind::Implementation)?; let protocol_failure = host_error.protocol_error(); + let permission_failure = provider + .call_operation(request( + OBJECT_PATH, + catalog.reference(), + Some(CATALOG_VALIDATOR), + "restricted_status", + BTreeMap::new(), + )) + .expect_err("restricted_status must be denied") + .protocol_error() + .code; + + let validator_failure = provider + .call_operation(CallRequest { + target: Target { + path: OBJECT_PATH.into(), + validator: Some(b"stale-object".to_vec()), + }, + interface: InterfaceTarget { + reference: diagnostics.reference().as_str().to_owned(), + validator: Some(b"stale-interface".to_vec()), + }, + operation: "missing".into(), + arguments: BTreeMap::from([("unknown".into(), Value::Unit)]), + }) + .expect_err("the stale object validator must fail first") + .protocol_error() + .code; + + let interface_mismatch = provider + .call_operation(request( + OBJECT_PATH, + diagnostics.reference(), + None, + "diagnostics", + BTreeMap::new(), + )) + .expect_err("registered but unexposed interface must not dispatch") + .protocol_error() + .code; + + let interface_not_found = registry + .fetch_interface("missing-interface") + .expect_err("unknown interface references must not resolve") + .code; + + let precedence_failure = provider + .call_operation(CallRequest { + target: Target { + path: "relative/path".into(), + validator: Some(b"stale-object".to_vec()), + }, + interface: InterfaceTarget { + reference: diagnostics.reference().as_str().to_owned(), + validator: Some(b"stale-interface".to_vec()), + }, + operation: "missing".into(), + arguments: BTreeMap::from([("unknown".into(), Value::Unit)]), + }) + .expect_err("invalid path must have canonical precedence") + .protocol_error() + .code; + Ok(DemoReport { + object_name: object.name, + object_reference: object.r#ref, + object_validator: object.validator, published_interfaces: object.interfaces, + catalog_interface_reference: catalog.reference().as_str().to_owned(), + audit_interface_reference: audit.reference().as_str().to_owned(), catalog_descriptor_format: catalog_observation.descriptor.format, audit_descriptor_format: audit_observation.descriptor.format, published, @@ -189,9 +334,25 @@ pub fn run() -> Result> { audit_status, host_failure: host_error.kind(), protocol_failure, + permission_failure, + validator_failure, + interface_mismatch, + interface_not_found, + precedence_failure, }) } +fn ensure_kind(error: &wip_host::CallError, expected: CallErrorKind) -> Result<(), io::Error> { + if error.kind() == expected { + Ok(()) + } else { + Err(io::Error::other(format!( + "unexpected host failure kind: {:?}", + error.kind() + ))) + } +} + fn dispatch_publish( provider: &ApplicationProvider, catalog: &InterfaceRef, @@ -199,6 +360,7 @@ fn dispatch_publish( ) -> Result> { let encoded_request = request_value.encode()?; let result = provider.call_operation(request( + OBJECT_PATH, catalog, Some(CATALOG_VALIDATOR), "publish", @@ -216,14 +378,15 @@ fn dispatch_string( } fn request( + path: &str, interface: &InterfaceRef, interface_validator: Option<&[u8]>, operation: &str, - arguments: BTreeMap, + arguments: BTreeMap, ) -> CallRequest { CallRequest { target: Target { - path: OBJECT_PATH.into(), + path: path.into(), validator: Some(OBJECT_VALIDATOR.to_vec()), }, interface: InterfaceTarget { @@ -258,6 +421,16 @@ impl ObjectProvider for ApplicationProvider { .expect("ApplicationService implements Audit"); Some(operation(&publication)) } + + fn authorize( + &self, + _: &str, + _: &ApplicationService, + _: &InterfaceRef, + operation: &str, + ) -> bool { + operation != "restricted_status" + } } struct ApplicationService { @@ -316,6 +489,10 @@ impl Catalog for ApplicationService { Ok("backend-ready".into()) } } + + fn restricted_status(&self) -> wip::OperationResult { + Ok("authorization hook should prevent this call".into()) + } } impl Audit for ApplicationService { @@ -329,32 +506,3 @@ impl Diagnostics for ApplicationService { Ok("host-private".into()) } } - -#[cfg(test)] -mod tests { - use super::*; - use wip_protocol::ProtocolErrorCode; - - #[test] - fn unexposed_interfaces_are_membership_failures() { - let mut registry = InterfaceRegistry::new(); - let catalog = registry.register(CatalogDefinition, None).unwrap(); - let audit = registry.register(AuditDefinition, None).unwrap(); - let diagnostics = registry.register(DiagnosticsDefinition, None).unwrap(); - let provider = ApplicationProvider { catalog, audit }; - - let error = provider - .call_operation(request( - diagnostics.reference(), - None, - "diagnostics", - BTreeMap::new(), - )) - .unwrap_err(); - assert_eq!(error.kind(), CallErrorKind::InterfaceMismatch); - assert_eq!( - error.protocol_error().code, - ProtocolErrorCode::InterfaceMismatch - ); - } -} diff --git a/src/main.rs b/crates/wip-public-api-example/src/main.rs similarity index 100% rename from src/main.rs rename to crates/wip-public-api-example/src/main.rs diff --git a/crates/wip-public-api-example/tests/end_to_end.rs b/crates/wip-public-api-example/tests/end_to_end.rs new file mode 100644 index 0000000..84d6967 --- /dev/null +++ b/crates/wip-public-api-example/tests/end_to_end.rs @@ -0,0 +1,236 @@ +use std::collections::BTreeMap; + +use wip_host::CallErrorKind; +use wip_interface::{Entry, JsonMember, JsonValue, WipType}; +use wip_protocol::{INTERFACE_FORMAT_V1, InterfaceDescriptor, Object, ProtocolErrorCode, Value}; +use wip_public_api_example::{CatalogDefinition, PublishOutcome, PublishRequest, Visibility, run}; + +#[test] +fn public_crates_support_the_complete_qualified_end_to_end_scenario() { + let report = run().unwrap(); + + assert_eq!(report.object_name, "article-service"); + assert_eq!( + report.object_reference.as_deref(), + Some("article-service-instance") + ); + assert_eq!( + report.object_validator.as_deref(), + Some(b"object-v1".as_slice()) + ); + assert_eq!( + report.published_interfaces, + [ + report.catalog_interface_reference.clone(), + report.audit_interface_reference.clone(), + ], + "interfaces must retain explicit publication order" + ); + assert_ne!( + report.catalog_interface_reference, report.audit_interface_reference, + "interfaces form an ordered set of distinct opaque references" + ); + assert_eq!(report.catalog_descriptor_format, INTERFACE_FORMAT_V1); + assert_eq!(report.audit_descriptor_format, INTERFACE_FORMAT_V1); + assert_eq!( + report.published, + PublishOutcome::Published("article-1".into()) + ); + assert_eq!( + report.rejected, + PublishOutcome::Rejected("a title is required".into()) + ); + assert_eq!(report.catalog_status, "catalog-ready"); + assert_eq!(report.audit_status, "audit-ready"); +} + +#[test] +fn domain_host_permission_and_validator_failures_remain_distinct() { + let report = run().unwrap(); + + assert!(matches!(report.rejected, PublishOutcome::Rejected(_))); + assert_eq!(report.host_failure, CallErrorKind::Implementation); + assert_eq!(report.protocol_failure.code, ProtocolErrorCode::Internal); + assert_eq!( + report.permission_failure, + ProtocolErrorCode::PermissionDenied + ); + assert_eq!( + report.validator_failure, + ProtocolErrorCode::ValidatorMismatch, + "validator recovery signals must not collapse to InvalidRequest" + ); +} + +#[test] +fn interface_mismatch_is_not_interface_not_found() { + let report = run().unwrap(); + + assert_eq!( + report.interface_mismatch, + ProtocolErrorCode::InterfaceMismatch + ); + assert_eq!( + report.interface_not_found, + ProtocolErrorCode::InterfaceNotFound + ); + assert_ne!(report.interface_mismatch, report.interface_not_found); +} + +#[test] +fn invalid_path_keeps_canonical_failure_precedence() { + let report = run().unwrap(); + + assert_eq!(report.precedence_failure, ProtocolErrorCode::InvalidRequest); + assert_ne!(report.precedence_failure, report.validator_failure); + assert_ne!(report.precedence_failure, report.interface_mismatch); +} + +#[test] +fn object_fields_keep_path_context_and_optional_ref_separate() { + // Listing every field makes this test fail to compile if a mandatory + // ObjectId is reintroduced into the canonical object DTO. + let object = Object { + name: "article".into(), + description: None, + interfaces: vec!["catalog-ref".into(), "audit-ref".into()], + r#ref: None, + validator: None, + }; + + object.validate_at_path("/library/article").unwrap(); + assert_eq!(object.name, "article"); + assert_eq!(object.r#ref, None); + assert_eq!(object.interfaces, ["catalog-ref", "audit-ref"]); +} + +#[test] +fn interface_reference_is_observed_separately_from_descriptor_fields() { + // An exact destructuring pattern is compile-time coverage that the + // descriptor has no trait-name or opaque-reference `name` field. + let InterfaceDescriptor { + format, + documentation, + types, + operations, + } = CatalogDefinition::descriptor(); + + assert_eq!(format, INTERFACE_FORMAT_V1); + assert!(documentation.is_some()); + assert!(!types.is_empty()); + assert_eq!( + operations + .iter() + .map(|operation| operation.name.as_str()) + .collect::>(), + ["publish", "status", "backend_status", "restricted_status"] + ); +} + +#[test] +fn application_record_enum_union_entry_and_json_use_schema_neutral_values() { + let request = PublishRequest { + title: "Canonical values".into(), + body: None, + source: Entry::new("/drafts/canonical-values").unwrap(), + visibility: Visibility::Public, + metadata: JsonValue::Object(vec![ + JsonMember { + name: "tags".into(), + value: JsonValue::Array(vec![JsonValue::String("wip".into()), JsonValue::Null]), + }, + JsonMember { + name: "ready".into(), + value: JsonValue::Boolean(true), + }, + ]), + }; + + let encoded = request.encode().unwrap(); + assert_schema_neutral(&encoded); + let Value::Record(fields) = encoded else { + panic!("derived DTO record must use Value::Record") + }; + assert_eq!( + fields.get("title"), + Some(&Value::String("Canonical values".into())) + ); + assert!(!fields.contains_key("body")); + assert_eq!( + fields.get("source"), + Some(&Value::String("/drafts/canonical-values".into())) + ); + assert_eq!( + fields.get("visibility"), + Some(&Value::String("Public".into())) + ); + assert_eq!( + fields.get("metadata"), + Some(&Value::Record(BTreeMap::from([ + ("ready".into(), Value::Boolean(true)), + ( + "tags".into(), + Value::List(vec![Value::String("wip".into()), Value::Unit]) + ), + ]))) + ); + + let outcome = PublishOutcome::Rejected("policy".into()).encode().unwrap(); + assert_schema_neutral(&outcome); + assert_eq!( + outcome, + Value::Record(BTreeMap::from([ + ("$case".into(), Value::String("Rejected".into())), + ("value".into(), Value::String("policy".into())), + ])) + ); +} + +#[test] +fn entry_is_a_validated_canonical_absolute_path_string() { + let entry = Entry::new("/articles/one").unwrap(); + assert_eq!( + entry.encode().unwrap(), + Value::String("/articles/one".into()) + ); + + for invalid in [ + "articles/one", + "/articles/", + "/articles//one", + "/articles/./one", + "/articles/../one", + ] { + assert!( + Entry::new(invalid).is_err(), + "accepted invalid path {invalid}" + ); + assert!( + Entry::decode(Value::String(invalid.into())).is_err(), + "decoded invalid path {invalid}" + ); + } +} + +fn assert_schema_neutral(value: &Value) { + // This exhaustive match is a compile-time regression guard: reintroducing + // semantic Entry/Enum/Union/Json variants makes the consumer non-exhaustive. + match value { + Value::Unit + | Value::Boolean(_) + | Value::Integer(_) + | Value::Number(_) + | Value::String(_) + | Value::Bytes(_) => {} + Value::Record(fields) => { + for value in fields.values() { + assert_schema_neutral(value); + } + } + Value::List(values) => { + for value in values { + assert_schema_neutral(value); + } + } + } +} diff --git a/tests/end_to_end.rs b/tests/end_to_end.rs deleted file mode 100644 index 1a8fc73..0000000 --- a/tests/end_to_end.rs +++ /dev/null @@ -1,24 +0,0 @@ -use wip_host::CallErrorKind; -use wip_protocol::{INTERFACE_FORMAT_V1, ProtocolErrorCode}; -use wip_public_api_example::{PublishOutcome, run}; - -#[test] -fn public_crates_support_the_complete_end_to_end_scenario() { - let report = run().unwrap(); - - assert_eq!(report.published_interfaces.len(), 2); - assert_eq!(report.catalog_descriptor_format, INTERFACE_FORMAT_V1); - assert_eq!(report.audit_descriptor_format, INTERFACE_FORMAT_V1); - assert_eq!( - report.published, - PublishOutcome::Published("article-1".into()) - ); - assert_eq!( - report.rejected, - PublishOutcome::Rejected("a title is required".into()) - ); - assert_eq!(report.catalog_status, "catalog-ready"); - assert_eq!(report.audit_status, "audit-ready"); - assert_eq!(report.host_failure, CallErrorKind::Implementation); - assert_eq!(report.protocol_failure.code, ProtocolErrorCode::Internal); -}