feat(example): add public API end-to-end flow

This commit is contained in:
2026-09-29 00:04:11 +09:00
parent 5992322371
commit 0705101c97
6 changed files with 791 additions and 25 deletions
Generated
+3 -1
View File
@@ -270,9 +270,11 @@ name = "wip-protocol"
version = "0.1.0"
[[package]]
name = "wip-rs"
name = "wip-public-api-example"
version = "0.1.0"
dependencies = [
"wip-host",
"wip-interface",
"wip-protocol",
]
+6 -1
View File
@@ -9,9 +9,14 @@ members = [
resolver = "3"
[package]
name = "wip-rs"
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" }
+115 -21
View File
@@ -2,6 +2,65 @@
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.
## Crate graph and responsibilities
```text
wip-public-api-example (workspace-only executable, integration tests, doctest)
├── wip-protocol logical declarations, values, calls, and validation
├── wip-interface application DTO codecs and interface authoring facade
│ ├── wip-protocol
│ └── wip-interface-macros proc-macro implementation detail
└── wip-host descriptor registry, publication, and qualified dispatch
├── wip-interface
└── wip-protocol
```
| Crate | Responsibility | Does not own |
| --- | --- | --- |
| `wip-protocol` | Transport-independent descriptors, object/call/result types, logical `Value`s, validation, and stable protocol error categories. | Application DTOs, implementations, persistence, or wire encoding. |
| `wip-interface` | The public `WipType`/`interface` authoring API, Rust-to-protocol codecs, generated descriptors, and typed implementation dispatch. | Object publication, backend lifetime, or transport policy. |
| `wip-interface-macros` | Procedural-macro expansion used by `wip-interface`. It is not an application dependency. | A supported direct user API. |
| `wip-host` | Opaque interface registration, explicit object membership, validator checks, and interface-qualified dispatch against one host-owned instance. | Backend atomicity, global object lookup, or network serving. |
| `wip-public-api-example` | An independently compiled consumer of only the three public crates. Its DTOs and traits are application-owned; its implementation and backend handle remain host-private. | Reusable library API or a production server. |
The boundary is intentional: protocol types describe what can cross a
transport; application DTOs opt into that model with `WipType`; a host-private
implementation owns credentials, handles, and backend state; and a future
transport maps its own routes/envelopes to `OperationCall`/`OperationResult`
without placing transport-specific types in descriptors 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:
1. public application DTOs derived as `WipType` records and unions;
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;
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.
Run it inside the development shell:
```console
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.
## Interface authoring
Applications depend on `wip-interface`, normally imported as `wip`:
@@ -30,12 +89,31 @@ pub trait Messenger {
```
The `interface` attribute and `WipType` derive are re-exported by the facade;
users do not import `wip-interface-macros` directly. The generated
`MessengerDefinition` constructs an `InterfaceDescriptor`, decodes named
arguments, encodes results, validates both boundaries, and provides typed
dispatch for any `T: Messenger`.
users do not import `wip-interface-macros` or generated private support symbols.
The generated `MessengerDefinition` constructs an `InterfaceDescriptor`,
decodes named arguments, encodes results, validates both boundaries, and
provides typed dispatch for any `T: Messenger`.
### Type mapping
### Supported Rust syntax
A supported interface is a public, non-generic trait containing methods with:
- exactly an immutable `&self` receiver;
- simple named, owned parameters;
- an explicit `wip::OperationResult<T>` return;
- `()` only as the application return when no value is produced; and
- direct `Option<T>` only for an omittable parameter.
`WipType` derives support named-field structs, all-unit enums, and enums whose
variants each contain exactly one unnamed payload. The macros reject private,
generic, inherited, unsafe, auto, or associated-item traits; const, async,
unsafe, extern, variadic, generic, default-bodied, or non-`&self` methods;
borrowed/custom-lifetime values; tuple/unit/generic structs; Rust unions;
mixed/named-field enum variants; explicit discriminants; nested `Option`; and
`Option` in returns, collections, entries, or union payloads. These diagnostics
are compile-fail tested under `crates/wip-interface/tests/ui`.
### Rust type mapping
| Rust | Protocol |
| --- | --- |
@@ -45,27 +123,21 @@ dispatch for any `T: Messenger`.
| `f32`, `f64` | finite number |
| `String` | string |
| `wip::Bytes` | bytes |
| `wip::wip_protocol::JsonValue` | JSON |
| `wip::JsonValue` | JSON |
| `wip::Entry<K, V>` | entry |
| `Vec<T>` | list |
| named-field `#[derive(wip::WipType)]` struct | named record |
| all-unit `#[derive(wip::WipType)]` enum | named enum |
| all-newtype `#[derive(wip::WipType)]` enum | named tagged union |
`Option<T>` is accepted only for a direct DTO field or operation parameter and
means that the named value may be omitted. It is not protocol null. Domain
failures are modeled in the application result (usually a derived union) and
returned through `Ok`; `wip::HostError` is only for host/runtime failures.
`Option<T>` means that a direct DTO field or operation parameter may be
omitted; it is not protocol null. JSON null remains available inside
`wip::JsonValue`.
Rust doc comments become descriptor metadata. For an operation, a `# Returns`
section documents the return declaration; text before that section documents
the operation itself.
The supported interface slice is deliberately non-generic: public traits,
`&self`, named parameters, and `OperationResult<T>`. Generic traits or methods,
associated items, custom lifetimes, inheritance, default method bodies, and
other receiver forms are rejected at their source spans.
## Host publication and dispatch
Applications use `wip-host` to register generated definitions and explicitly
@@ -84,16 +156,38 @@ let protocol_object = published.protocol_object();
interface trait. Implementing a trait does not expose it automatically, empty
interface sets are valid, insertion order is preserved, and duplicate
references are rejected. Registry references are opaque; descriptor names and
Rust type names are not used as wire identity.
Rust type names are not used as independent wire identity.
Dispatch is qualified by interface reference and operation name. The host checks
current object identity and validator policy, current interface membership,
interface validator, descriptor operation, arguments, and result while retaining
the selected object and registered artifact. Host-private fields and backend
handles are never projected by `WipObject`.
Dispatch is qualified by interface reference and operation name. The host
checks current object identity and validator policy, current interface
membership, interface validator, descriptor operation, arguments, and result
while retaining the selected object and registered artifact. Host-private
fields and backend handles are never projected by `WipObject`.
Application domain failures belong in a derived result union and are returned
through `Ok`. `wip::HostError` is reserved for runtime/backend failures. Invalid
arguments are rejected before invocation; malformed encoded results are
rejected as internal host failures and never returned as application data.
Providers remain responsible for backend atomicity. Enter a transaction, lock,
stable-handle scope, or conditional-update primitive before constructing and
dispatching the request-scoped `PublishedObject`, and retain it until dispatch
returns. The runtime never validates one object and then resolves a second
snapshot, but it cannot make an arbitrary external backend atomic by itself.
## Validation
Enter the dev shell and run the acceptance commands exactly:
```console
nix develop
cargo fmt --all -- --check
cargo check --workspace --all-targets
cargo test --workspace --all-targets
cargo clippy --workspace --all-targets -- -D warnings
```
`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.
+627
View File
@@ -0,0 +1,627 @@
//! End-to-end use of the public WIP protocol, interface, and host APIs.
//!
//! This workspace package is deliberately separate from the three library
//! crates. It defines application DTOs and interfaces with `wip-interface`,
//! keeps its implementation private, publishes that implementation with
//! `wip-host`, and crosses the call boundary only with `wip-protocol` values.
//!
//! The complete scenario can be run as a normal binary or called directly:
//!
//! ```
//! let report = wip_public_api_example::run()?;
//!
//! assert_eq!(report.published_interfaces.len(), 2);
//! assert_eq!(report.catalog_status, "catalog-ready");
//! assert_eq!(report.audit_status, "audit-ready");
//! assert_eq!(
//! report.host_failure,
//! wip_host::CallErrorKind::Implementation,
//! );
//! # Ok::<(), Box<dyn std::error::Error>>(())
//! ```
#![deny(missing_docs)]
use std::error::Error;
use std::io;
use wip_host::{
CallErrorKind, CallRequest, InterfaceRef, InterfaceRegistry, InterfaceTarget, PublishedObject,
Validator, WipObject,
};
use wip_interface as wip;
use wip_interface::WipType;
use wip_protocol::{
InterfaceObservation, NamedValue, ObjectId, OperationOutcome, OperationResult, OperationTarget,
};
const OBJECT_ID: &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.
#[derive(Debug, Clone, PartialEq, Eq, 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<String>,
}
/// The application domain result of a publication attempt.
///
/// Rejection is an ordinary typed result. It is intentionally distinct from a
/// host or backend failure, which uses [`wip::HostError`].
#[derive(Debug, Clone, PartialEq, Eq, wip::WipType)]
pub enum PublishOutcome {
/// Publication succeeded and returned its application identifier.
Published(String),
/// An application policy rejected the publication.
Rejected(String),
}
/// Article publication operations.
#[wip::interface]
pub trait Catalog {
/// Publishes an article or reports an application-domain rejection.
///
/// # Returns
/// The typed application outcome.
fn publish(
&self,
/// Application publication request.
request: PublishRequest,
) -> wip::OperationResult<PublishOutcome>;
/// Returns the catalog-qualified status.
///
/// # Returns
/// A catalog status label.
fn status(&self) -> wip::OperationResult<String>;
/// Demonstrates a host/backend failure rather than a domain result.
///
/// # Returns
/// A backend status only when the host can reach it.
fn backend_status(&self) -> wip::OperationResult<String>;
}
/// Audit operations exposed by the same object.
#[wip::interface]
pub trait Audit {
/// Returns the audit-qualified status.
///
/// This deliberately has the same operation name as [`Catalog::status`].
///
/// # Returns
/// An audit status label.
fn status(&self) -> wip::OperationResult<String>;
}
/// A registered but deliberately unexposed interface used to demonstrate that
/// Rust trait implementation does not imply protocol membership.
#[wip::interface]
pub trait Diagnostics {
/// Returns host-private diagnostics when the interface is explicitly exposed.
///
/// # Returns
/// A diagnostic label.
fn diagnostics(&self) -> wip::OperationResult<String>;
}
/// Observable outcomes from the complete public-API scenario.
#[derive(Debug, Clone, PartialEq)]
pub struct DemoReport {
/// Opaque interface references published by the object, in exposure order.
pub published_interfaces: Vec<String>,
/// Descriptor identity observed for the catalog interface.
pub catalog_descriptor: String,
/// Descriptor identity observed for the audit interface.
pub audit_descriptor: String,
/// Typed successful application result decoded from a validated `Value`.
pub published: PublishOutcome,
/// Typed application-domain rejection decoded from a validated `Value`.
pub rejected: PublishOutcome,
/// Result of the catalog-qualified `status` operation.
pub catalog_status: String,
/// Result of the audit-qualified `status` operation.
pub audit_status: String,
/// Stable failure kind produced by the deliberate host/backend failure.
pub host_failure: CallErrorKind,
/// A protocol-level operation result built from the host failure.
pub protocol_failure: OperationResult,
}
/// Runs the complete end-to-end example.
///
/// The function registers and observes generated descriptors, explicitly
/// exposes two interfaces on one host-private object, validates the resulting
/// protocol observation, dispatches interface-qualified calls using named
/// [`wip_protocol::Value`] arguments, decodes validated values back to
/// application types, and keeps a domain rejection distinct from a host
/// failure.
///
/// # Errors
///
/// Returns an error if registration, publication, observation validation,
/// dispatch, or application-value decoding fails unexpectedly.
pub fn run() -> Result<DemoReport, Box<dyn Error>> {
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 publication = PublishedObject::new(ApplicationService::new())
.expose(&catalog)?
.expose(&audit)?;
let object = publication.protocol_object();
// Model a transport/provider that retained only the protocol object's
// opaque strings, then resolved them back through the registry's public API.
let catalog_wire_reference = object
.interfaces
.first()
.ok_or_else(|| io::Error::other("catalog interface was not published"))?;
let audit_wire_reference = object
.interfaces
.get(1)
.ok_or_else(|| io::Error::other("audit interface was not published"))?;
let catalog_reference = registry
.resolve(catalog_wire_reference)
.ok_or_else(|| io::Error::other("published catalog reference did not resolve"))?;
let audit_reference = registry
.resolve(audit_wire_reference)
.ok_or_else(|| io::Error::other("published audit reference did not resolve"))?;
let catalog_observation = registry
.observe(&catalog_reference)
.ok_or_else(|| io::Error::other("registered catalog descriptor was not observable"))?;
let audit_observation = registry
.observe(&audit_reference)
.ok_or_else(|| io::Error::other("registered audit descriptor was not observable"))?;
InterfaceObservation {
object: object.clone(),
descriptors: vec![
catalog_observation.descriptor.clone(),
audit_observation.descriptor.clone(),
],
}
.validate()?;
let published = dispatch_publish(
&publication,
&catalog_observation.reference,
PublishRequest {
title: "Public APIs".into(),
body: Some("protocol values in, validated values out".into()),
},
)?;
let rejected = dispatch_publish(
&publication,
&catalog_observation.reference,
PublishRequest {
title: String::new(),
body: None,
},
)?;
let catalog_status = dispatch_string(
&publication,
request(
catalog_observation.reference.clone(),
Some(Validator::new(CATALOG_VALIDATOR)),
"status",
vec![],
),
"catalog status",
)?;
let audit_status = dispatch_string(
&publication,
request(audit_observation.reference.clone(), None, "status", vec![]),
"audit status",
)?;
let host_error = match publication.dispatch(request(
catalog_observation.reference.clone(),
Some(Validator::new(CATALOG_VALIDATOR)),
"backend_status",
vec![],
)) {
Ok(_) => {
return Err(io::Error::other(
"the deliberately unavailable example backend unexpectedly succeeded",
)
.into());
}
Err(error) => error,
};
if host_error.kind() != CallErrorKind::Implementation {
return Err(io::Error::other(format!(
"unexpected host failure kind: {:?}",
host_error.kind()
))
.into());
}
let protocol_error = host_error.protocol_error();
let protocol_failure = OperationResult {
target: OperationTarget {
object: ObjectId(OBJECT_ID.into()),
interface: catalog_observation.reference.as_str().into(),
operation: "backend_status".into(),
},
outcome: OperationOutcome::Failed(protocol_error),
};
Ok(DemoReport {
published_interfaces: object.interfaces,
catalog_descriptor: catalog_observation.descriptor.name,
audit_descriptor: audit_observation.descriptor.name,
published,
rejected,
catalog_status,
audit_status,
host_failure: host_error.kind(),
protocol_failure,
})
}
fn dispatch_publish(
publication: &PublishedObject<ApplicationService>,
catalog: &InterfaceRef,
request_value: PublishRequest,
) -> Result<PublishOutcome, Box<dyn Error>> {
let encoded_request = request_value.encode()?;
let result = publication.dispatch(request(
catalog.clone(),
Some(Validator::new(CATALOG_VALIDATOR)),
"publish",
vec![NamedValue {
name: "request".into(),
value: encoded_request,
}],
))?;
let value = result
.value
.ok_or_else(|| io::Error::other("publish returned no application value"))?;
Ok(PublishOutcome::decode(value)?)
}
fn dispatch_string(
publication: &PublishedObject<ApplicationService>,
request: CallRequest,
context: &str,
) -> Result<String, Box<dyn Error>> {
let result = publication.dispatch(request)?;
let value = result
.value
.ok_or_else(|| io::Error::other(format!("{context} returned no value")))?;
Ok(String::decode(value)?)
}
fn request(
interface: InterfaceRef,
interface_validator: Option<Validator>,
operation: &str,
arguments: Vec<NamedValue>,
) -> CallRequest {
CallRequest {
object: ObjectId(OBJECT_ID.into()),
object_validator: Some(Validator::new(OBJECT_VALIDATOR)),
interface: InterfaceTarget {
reference: interface,
validator: interface_validator,
},
operation: operation.into(),
arguments,
}
}
struct ApplicationService {
object_validator: Validator,
backend_connection: String,
}
impl ApplicationService {
fn new() -> Self {
Self {
object_validator: Validator::new(OBJECT_VALIDATOR),
backend_connection: "private://article-database".into(),
}
}
}
impl WipObject for ApplicationService {
fn id(&self) -> ObjectId {
ObjectId(OBJECT_ID.into())
}
fn validator(&self) -> Option<&Validator> {
Some(&self.object_validator)
}
fn requires_validator(&self) -> bool {
true
}
}
impl Catalog for ApplicationService {
fn publish(&self, request: PublishRequest) -> wip::OperationResult<PublishOutcome> {
if request.title.trim().is_empty() {
Ok(PublishOutcome::Rejected("a title is required".into()))
} else {
Ok(PublishOutcome::Published("article-1".into()))
}
}
fn status(&self) -> wip::OperationResult<String> {
Ok("catalog-ready".into())
}
fn backend_status(&self) -> wip::OperationResult<String> {
if self.backend_connection.starts_with("private://") {
Err(wip::HostError::new("backend unavailable"))
} else {
Ok("backend-ready".into())
}
}
}
impl Audit for ApplicationService {
fn status(&self) -> wip::OperationResult<String> {
Ok("audit-ready".into())
}
}
impl Diagnostics for ApplicationService {
fn diagnostics(&self) -> wip::OperationResult<String> {
Ok("host-private".into())
}
}
#[cfg(test)]
mod tests {
use super::*;
use wip_host::CallError;
use wip_interface::{CodecError, TypeDeclaration, TypeExpr};
use wip_protocol::{ProtocolErrorCategory, Value};
/// A deliberately inconsistent custom codec used to prove result validation.
pub struct InvalidValue;
impl WipType for InvalidValue {
fn type_expr() -> TypeExpr {
TypeExpr::String
}
fn collect_declarations(_: &mut Vec<TypeDeclaration>) {}
fn encode(self) -> Result<Value, CodecError> {
Ok(Value::Integer(7))
}
fn decode(_: Value) -> Result<Self, CodecError> {
Ok(Self)
}
}
/// Test-only interface whose custom codec emits an invalid result.
#[wip::interface]
pub trait BrokenResult {
/// Returns a value that violates its declared protocol type.
fn broken_result(&self) -> wip::OperationResult<InvalidValue>;
}
impl BrokenResult for ApplicationService {
fn broken_result(&self) -> wip::OperationResult<InvalidValue> {
Ok(InvalidValue)
}
}
struct Fixture {
publication: PublishedObject<ApplicationService>,
catalog: InterfaceRef,
audit: InterfaceRef,
diagnostics: InterfaceRef,
}
fn fixture() -> Fixture {
let mut registry = InterfaceRegistry::new();
let catalog = registry
.register(CatalogDefinition, Some(Validator::new(CATALOG_VALIDATOR)))
.unwrap();
let audit = registry.register(AuditDefinition, None).unwrap();
let diagnostics = registry.register(DiagnosticsDefinition, None).unwrap();
let publication = PublishedObject::new(ApplicationService::new())
.expose(&catalog)
.unwrap()
.expose(&audit)
.unwrap();
Fixture {
publication,
catalog: catalog.reference().clone(),
audit: audit.reference().clone(),
diagnostics: diagnostics.reference().clone(),
}
}
fn assert_failure(error: CallError, kind: CallErrorKind, category: ProtocolErrorCategory) {
assert_eq!(error.kind(), kind);
assert_eq!(error.protocol_error().category, category);
}
#[test]
fn interface_membership_and_same_name_operations_are_qualified() {
let fixture = fixture();
let catalog = dispatch_string(
&fixture.publication,
request(
fixture.catalog.clone(),
Some(Validator::new(CATALOG_VALIDATOR)),
"status",
vec![],
),
"catalog status",
)
.unwrap();
let audit = dispatch_string(
&fixture.publication,
request(fixture.audit.clone(), None, "status", vec![]),
"audit status",
)
.unwrap();
assert_eq!(catalog, "catalog-ready");
assert_eq!(audit, "audit-ready");
let not_exposed = fixture
.publication
.dispatch(request(fixture.diagnostics, None, "diagnostics", vec![]))
.unwrap_err();
assert_failure(
not_exposed,
CallErrorKind::InterfaceMismatch,
ProtocolErrorCategory::InterfaceNotFound,
);
}
#[test]
fn every_host_precondition_and_dispatch_failure_is_typed() {
let fixture = fixture();
let mut wrong_object = request(
fixture.catalog.clone(),
Some(Validator::new(CATALOG_VALIDATOR)),
"status",
vec![],
);
wrong_object.object = ObjectId("other-object".into());
assert_failure(
fixture.publication.dispatch(wrong_object).unwrap_err(),
CallErrorKind::ObjectNotFound,
ProtocolErrorCategory::ObjectNotFound,
);
let mut missing_object_validator = request(
fixture.catalog.clone(),
Some(Validator::new(CATALOG_VALIDATOR)),
"status",
vec![],
);
missing_object_validator.object_validator = None;
assert_failure(
fixture
.publication
.dispatch(missing_object_validator)
.unwrap_err(),
CallErrorKind::ValidatorRequired,
ProtocolErrorCategory::InvalidRequest,
);
let mut stale_object_validator = request(
fixture.catalog.clone(),
Some(Validator::new(CATALOG_VALIDATOR)),
"status",
vec![],
);
stale_object_validator.object_validator = Some(Validator::new(b"stale-object"));
assert_failure(
fixture
.publication
.dispatch(stale_object_validator)
.unwrap_err(),
CallErrorKind::ValidatorMismatch,
ProtocolErrorCategory::InvalidRequest,
);
assert_failure(
fixture
.publication
.dispatch(request(fixture.catalog.clone(), None, "status", vec![]))
.unwrap_err(),
CallErrorKind::InterfaceValidatorRequired,
ProtocolErrorCategory::InvalidRequest,
);
assert_failure(
fixture
.publication
.dispatch(request(
fixture.catalog.clone(),
Some(Validator::new(b"stale-interface")),
"status",
vec![],
))
.unwrap_err(),
CallErrorKind::InterfaceValidatorMismatch,
ProtocolErrorCategory::InvalidRequest,
);
assert_failure(
fixture
.publication
.dispatch(request(
fixture.catalog.clone(),
Some(Validator::new(CATALOG_VALIDATOR)),
"missing",
vec![],
))
.unwrap_err(),
CallErrorKind::OperationNotFound,
ProtocolErrorCategory::OperationNotFound,
);
assert_failure(
fixture
.publication
.dispatch(request(
fixture.catalog.clone(),
Some(Validator::new(CATALOG_VALIDATOR)),
"publish",
vec![NamedValue {
name: "request".into(),
value: Value::String("not a record".into()),
}],
))
.unwrap_err(),
CallErrorKind::InvalidArguments,
ProtocolErrorCategory::InvalidArguments,
);
assert_failure(
fixture
.publication
.dispatch(request(
fixture.catalog,
Some(Validator::new(CATALOG_VALIDATOR)),
"backend_status",
vec![],
))
.unwrap_err(),
CallErrorKind::Implementation,
ProtocolErrorCategory::Internal,
);
}
#[test]
fn invalid_implementation_result_never_crosses_the_host_boundary() {
let mut registry = InterfaceRegistry::new();
let broken = registry.register(BrokenResultDefinition, None).unwrap();
let publication = PublishedObject::new(ApplicationService::new())
.expose(&broken)
.unwrap();
let error = publication
.dispatch(request(
broken.reference().clone(),
None,
"broken_result",
vec![],
))
.unwrap_err();
assert_failure(
error,
CallErrorKind::InvalidResult,
ProtocolErrorCategory::Internal,
);
}
}
+13 -2
View File
@@ -1,3 +1,14 @@
fn main() {
println!("Hello, world!");
use std::error::Error;
fn main() -> Result<(), Box<dyn Error>> {
let report = wip_public_api_example::run()?;
println!(
"published {} interfaces; catalog={}, audit={}; domain={:?}; host={:?}",
report.published_interfaces.len(),
report.catalog_status,
report.audit_status,
report.rejected,
report.host_failure,
);
Ok(())
}
+27
View File
@@ -0,0 +1,27 @@
use wip_host::CallErrorKind;
use wip_protocol::{OperationOutcome, ProtocolErrorCategory};
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, report.published_interfaces[0]);
assert_eq!(report.audit_descriptor, report.published_interfaces[1]);
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!(matches!(
report.protocol_failure.outcome,
OperationOutcome::Failed(error) if error.category == ProtocolErrorCategory::Internal
));
}