feat: model operation outcome guarantees
This commit is contained in:
@@ -40,8 +40,9 @@ use std::error::Error;
|
||||
use std::io;
|
||||
|
||||
use wip_host::{
|
||||
CallErrorKind, CallRequest, InterfaceRef, InterfaceRegistry, InterfaceTarget, ObjectProvider,
|
||||
PublishedObject, RegisteredInterface, Target, Validator, WipObject,
|
||||
CallErrorKind, CallRequest, ExecutionContext, InterfaceRef, InterfaceRegistry, InterfaceTarget,
|
||||
ObjectProvider, ProviderResult, PublishedObject, RegisteredInterface, Target, Validator,
|
||||
WipObject,
|
||||
};
|
||||
use wip_interface as wip;
|
||||
use wip_interface::WipType;
|
||||
@@ -100,9 +101,12 @@ pub trait Catalog {
|
||||
/// Returns the catalog-qualified status.
|
||||
fn status(&self) -> wip::OperationResult<String>;
|
||||
|
||||
/// Demonstrates a host/backend failure rather than a domain result.
|
||||
/// Demonstrates a rollback-confirmed host/backend failure.
|
||||
fn backend_status(&self) -> wip::OperationResult<String>;
|
||||
|
||||
/// Demonstrates a host/backend failure with an unknown operation outcome.
|
||||
fn uncertain_backend_status(&self) -> wip::OperationResult<String>;
|
||||
|
||||
/// Demonstrates denial after every structural precondition succeeds.
|
||||
fn restricted_status(&self) -> wip::OperationResult<String>;
|
||||
}
|
||||
@@ -150,8 +154,10 @@ pub struct DemoReport {
|
||||
pub audit_status: String,
|
||||
/// Stable failure kind produced by the deliberate host/backend failure.
|
||||
pub host_failure: CallErrorKind,
|
||||
/// A protocol failure whose operation effects cannot be guaranteed absent.
|
||||
/// Protocol failure whose operation effects are guaranteed not committed.
|
||||
pub protocol_failure: ProtocolError,
|
||||
/// Protocol failure whose operation effects cannot be proven absent.
|
||||
pub unknown_protocol_failure: ProtocolError,
|
||||
/// Authorization failure after a fully qualified valid request.
|
||||
pub permission_failure: ProtocolErrorCode,
|
||||
/// Recovery signal for a stale object validator.
|
||||
@@ -185,18 +191,22 @@ pub fn run() -> Result<DemoReport, Box<dyn Error>> {
|
||||
};
|
||||
|
||||
let object = provider.fetch(OBJECT_PATH)?;
|
||||
let catalog_observation = registry.fetch_interface(
|
||||
object
|
||||
.interfaces
|
||||
.first()
|
||||
.ok_or_else(|| io::Error::other("catalog interface was not published"))?,
|
||||
)?;
|
||||
let audit_observation = registry.fetch_interface(
|
||||
object
|
||||
.interfaces
|
||||
.get(1)
|
||||
.ok_or_else(|| io::Error::other("audit interface was not published"))?,
|
||||
)?;
|
||||
let catalog_response = registry
|
||||
.fetch_interface(
|
||||
object
|
||||
.interfaces
|
||||
.first()
|
||||
.ok_or_else(|| io::Error::other("catalog interface was not published"))?,
|
||||
)?
|
||||
.protocol_response()?;
|
||||
let audit_response = registry
|
||||
.fetch_interface(
|
||||
object
|
||||
.interfaces
|
||||
.get(1)
|
||||
.ok_or_else(|| io::Error::other("audit interface was not published"))?,
|
||||
)?
|
||||
.protocol_response()?;
|
||||
|
||||
let published = dispatch_publish(
|
||||
&provider,
|
||||
@@ -253,9 +263,20 @@ pub fn run() -> Result<DemoReport, Box<dyn Error>> {
|
||||
BTreeMap::new(),
|
||||
))
|
||||
.expect_err("backend_status must model a host failure");
|
||||
ensure_kind(&host_error, CallErrorKind::Implementation)?;
|
||||
ensure_kind(&host_error, CallErrorKind::Internal)?;
|
||||
let protocol_failure = host_error.protocol_error();
|
||||
|
||||
let unknown_protocol_failure = provider
|
||||
.call_operation(request(
|
||||
OBJECT_PATH,
|
||||
catalog.reference(),
|
||||
Some(CATALOG_VALIDATOR),
|
||||
"uncertain_backend_status",
|
||||
BTreeMap::new(),
|
||||
))
|
||||
.expect_err("uncertain_backend_status must preserve an unknown outcome")
|
||||
.protocol_error();
|
||||
|
||||
let permission_failure = provider
|
||||
.call_operation(request(
|
||||
OBJECT_PATH,
|
||||
@@ -326,14 +347,15 @@ pub fn run() -> Result<DemoReport, Box<dyn Error>> {
|
||||
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,
|
||||
catalog_descriptor_format: catalog_response.descriptor.format,
|
||||
audit_descriptor_format: audit_response.descriptor.format,
|
||||
published,
|
||||
rejected,
|
||||
catalog_status,
|
||||
audit_status,
|
||||
host_failure: host_error.kind(),
|
||||
protocol_failure,
|
||||
unknown_protocol_failure,
|
||||
permission_failure,
|
||||
validator_failure,
|
||||
interface_mismatch,
|
||||
@@ -408,18 +430,19 @@ impl ObjectProvider for ApplicationProvider {
|
||||
|
||||
fn with_object<R>(
|
||||
&self,
|
||||
_context: &ExecutionContext,
|
||||
path: &str,
|
||||
operation: impl FnOnce(&PublishedObject<Self::Object>) -> R,
|
||||
) -> Option<R> {
|
||||
) -> ProviderResult<R> {
|
||||
if path != OBJECT_PATH {
|
||||
return None;
|
||||
return ProviderResult::NotFound;
|
||||
}
|
||||
let publication = PublishedObject::new(ApplicationService::new())
|
||||
.expose(&self.catalog)
|
||||
.expect("ApplicationService implements Catalog")
|
||||
.expose(&self.audit)
|
||||
.expect("ApplicationService implements Audit");
|
||||
Some(operation(&publication))
|
||||
ProviderResult::Found(operation(&publication))
|
||||
}
|
||||
|
||||
fn authorize(
|
||||
@@ -484,12 +507,22 @@ impl Catalog for ApplicationService {
|
||||
|
||||
fn backend_status(&self) -> wip::OperationResult<String> {
|
||||
if self.backend_connection.starts_with("private://") {
|
||||
Err(wip::HostError::new("backend unavailable"))
|
||||
Err(wip::HostError::internal(
|
||||
"backend unavailable",
|
||||
wip::EffectGuarantee::NotCommitted,
|
||||
))
|
||||
} else {
|
||||
Ok("backend-ready".into())
|
||||
}
|
||||
}
|
||||
|
||||
fn uncertain_backend_status(&self) -> wip::OperationResult<String> {
|
||||
Err(wip::HostError::internal(
|
||||
"backend commit acknowledgement unavailable",
|
||||
wip::EffectGuarantee::Unknown,
|
||||
))
|
||||
}
|
||||
|
||||
fn restricted_status(&self) -> wip::OperationResult<String> {
|
||||
Ok("authorization hook should prevent this call".into())
|
||||
}
|
||||
|
||||
@@ -49,9 +49,10 @@ 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.host_failure, CallErrorKind::Internal);
|
||||
assert_eq!(report.protocol_failure.code, ProtocolErrorCode::Internal);
|
||||
assert_eq!(
|
||||
report.protocol_failure.code,
|
||||
report.unknown_protocol_failure.code,
|
||||
ProtocolErrorCode::OperationOutcomeUnknown
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -126,7 +127,13 @@ fn interface_reference_is_observed_separately_from_descriptor_fields() {
|
||||
.iter()
|
||||
.map(|operation| operation.name.as_str())
|
||||
.collect::<Vec<_>>(),
|
||||
["publish", "status", "backend_status", "restricted_status"]
|
||||
[
|
||||
"publish",
|
||||
"status",
|
||||
"backend_status",
|
||||
"uncertain_backend_status",
|
||||
"restricted_status",
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user