feat(example): add public API end-to-end flow
This commit is contained in:
+627
@@ -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
@@ -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(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user