fix: align protocol error semantics
This commit is contained in:
@@ -74,8 +74,9 @@ the distinct domain, host, authorization, validator-recovery, and interface
|
|||||||
failure classes. Exhaustive public type construction and matching make legacy
|
failure classes. Exhaustive public type construction and matching make legacy
|
||||||
ObjectId, descriptor `name`, and semantic `Value` variants compile-time
|
ObjectId, descriptor `name`, and semantic `Value` variants compile-time
|
||||||
regressions. Unit tests in the public crates additionally cover every host
|
regressions. Unit tests in the public crates additionally cover every host
|
||||||
dispatch failure, invalid arguments and results, interface membership,
|
dispatch failure, `InvalidRequest` versus descriptor-bound `InvalidArguments`,
|
||||||
validators, and same-named operations.
|
invalid local responses, validators, same-named operations, and exhaustive
|
||||||
|
per-interaction protocol error-code sets.
|
||||||
|
|
||||||
## Interface authoring
|
## Interface authoring
|
||||||
|
|
||||||
@@ -200,11 +201,20 @@ are never projected by `WipObject`; host-private fields and backend handles are
|
|||||||
also excluded.
|
also excluded.
|
||||||
|
|
||||||
Application domain failures belong in a derived result union and are returned
|
Application domain failures belong in a derived result union and are returned
|
||||||
through `Ok`. `wip::HostError` is reserved for runtime/backend failures. Invalid
|
through `Ok`. `wip::HostError` is reserved for runtime/backend failures. A
|
||||||
arguments are rejected before invocation; malformed encoded results are
|
structurally malformed call is `InvalidRequest`; only argument values checked
|
||||||
reported as `InvalidResult` and never returned as application data. Validator,
|
after the current Interface Descriptor is fixed are `InvalidArguments`. A Host
|
||||||
membership, authorization, and result failures retain their distinct canonical
|
must never return a wire `InvalidResult`: Clients keep invalid Object,
|
||||||
protocol error codes.
|
Descriptor, operation-result, and error responses as local `InvalidResponse`
|
||||||
|
failures, while unsupported descriptor formats remain local
|
||||||
|
`UnsupportedDescriptorFormat` failures.
|
||||||
|
|
||||||
|
For operation calls, `Internal` and `ResourceLimitExceeded` guarantee that the
|
||||||
|
operation did not run or that its effects were rolled back.
|
||||||
|
`OperationOutcomeUnknown` is the only protocol code that makes no such
|
||||||
|
guarantee, so Clients must not automatically retry it. The current host API maps
|
||||||
|
implementation failures and invalid post-execution results conservatively to
|
||||||
|
`OperationOutcomeUnknown`.
|
||||||
|
|
||||||
Providers remain responsible for backend atomicity. Enter a transaction, lock,
|
Providers remain responsible for backend atomicity. Enter a transaction, lock,
|
||||||
stable-handle scope, or conditional-update primitive before invoking the
|
stable-handle scope, or conditional-update primitive before invoking the
|
||||||
|
|||||||
@@ -545,9 +545,9 @@ pub trait ObjectProvider {
|
|||||||
self.with_object(path, PublishedObject::protocol_object)
|
self.with_object(path, PublishedObject::protocol_object)
|
||||||
.ok_or_else(|| CallError::new(CallErrorKind::NotFound, "target path is not published"))
|
.ok_or_else(|| CallError::new(CallErrorKind::NotFound, "target path is not published"))
|
||||||
.and_then(|object| {
|
.and_then(|object| {
|
||||||
object.validate_at_path(path).map_err(|error| {
|
object
|
||||||
CallError::new(CallErrorKind::InvalidResult, error.to_string())
|
.validate_at_path(path)
|
||||||
})?;
|
.map_err(|error| CallError::new(CallErrorKind::Internal, error.to_string()))?;
|
||||||
Ok(object)
|
Ok(object)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -607,7 +607,9 @@ pub enum CallErrorKind {
|
|||||||
OperationNotFound,
|
OperationNotFound,
|
||||||
/// Supplied named values do not satisfy the operation declaration.
|
/// Supplied named values do not satisfy the operation declaration.
|
||||||
InvalidArguments,
|
InvalidArguments,
|
||||||
/// The encoded implementation result violates its declaration.
|
/// The Host constructed an invalid response before operation execution.
|
||||||
|
Internal,
|
||||||
|
/// The encoded implementation result violates its declaration after execution.
|
||||||
InvalidResult,
|
InvalidResult,
|
||||||
/// This operation requires a validator and the call omitted it.
|
/// This operation requires a validator and the call omitted it.
|
||||||
ValidatorRequired,
|
ValidatorRequired,
|
||||||
@@ -683,7 +685,10 @@ impl CallError {
|
|||||||
CallErrorKind::InterfaceMismatch => ProtocolErrorCode::InterfaceMismatch,
|
CallErrorKind::InterfaceMismatch => ProtocolErrorCode::InterfaceMismatch,
|
||||||
CallErrorKind::OperationNotFound => ProtocolErrorCode::OperationNotFound,
|
CallErrorKind::OperationNotFound => ProtocolErrorCode::OperationNotFound,
|
||||||
CallErrorKind::InvalidArguments => ProtocolErrorCode::InvalidArguments,
|
CallErrorKind::InvalidArguments => ProtocolErrorCode::InvalidArguments,
|
||||||
CallErrorKind::InvalidResult => ProtocolErrorCode::InvalidResult,
|
CallErrorKind::Internal => ProtocolErrorCode::Internal,
|
||||||
|
CallErrorKind::InvalidResult | CallErrorKind::Implementation => {
|
||||||
|
ProtocolErrorCode::OperationOutcomeUnknown
|
||||||
|
}
|
||||||
CallErrorKind::ValidatorRequired => ProtocolErrorCode::ValidatorRequired,
|
CallErrorKind::ValidatorRequired => ProtocolErrorCode::ValidatorRequired,
|
||||||
CallErrorKind::ValidatorMismatch => ProtocolErrorCode::ValidatorMismatch,
|
CallErrorKind::ValidatorMismatch => ProtocolErrorCode::ValidatorMismatch,
|
||||||
CallErrorKind::InterfaceValidatorRequired => {
|
CallErrorKind::InterfaceValidatorRequired => {
|
||||||
@@ -693,7 +698,6 @@ impl CallError {
|
|||||||
ProtocolErrorCode::InterfaceValidatorMismatch
|
ProtocolErrorCode::InterfaceValidatorMismatch
|
||||||
}
|
}
|
||||||
CallErrorKind::PermissionDenied => ProtocolErrorCode::PermissionDenied,
|
CallErrorKind::PermissionDenied => ProtocolErrorCode::PermissionDenied,
|
||||||
CallErrorKind::Implementation => ProtocolErrorCode::Internal,
|
|
||||||
};
|
};
|
||||||
ProtocolError {
|
ProtocolError {
|
||||||
code,
|
code,
|
||||||
|
|||||||
@@ -235,6 +235,10 @@ impl ObjectProvider for TestProvider {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
"/empty" => (Document::open("empty", None, Rc::clone(&self.calls)), 0),
|
"/empty" => (Document::open("empty", None, Rc::clone(&self.calls)), 0),
|
||||||
|
"/invalid-object" => (
|
||||||
|
Document::open("wrong-name", None, Rc::clone(&self.calls)),
|
||||||
|
0,
|
||||||
|
),
|
||||||
"/broken" => (
|
"/broken" => (
|
||||||
Document::open("broken", Some("broken-ref"), Rc::clone(&self.calls)),
|
Document::open("broken", Some("broken-ref"), Rc::clone(&self.calls)),
|
||||||
3,
|
3,
|
||||||
@@ -624,9 +628,15 @@ fn immutable_interface_rejects_a_supplied_validator() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn invalid_results_and_backend_failures_keep_distinct_protocol_codes() {
|
fn invalid_host_responses_and_uncertain_operation_failures_use_canonical_codes() {
|
||||||
let (_, provider, _) = registry_and_provider();
|
let (_, provider, _) = registry_and_provider();
|
||||||
|
|
||||||
|
assert_failure(
|
||||||
|
provider.fetch("/invalid-object").unwrap_err(),
|
||||||
|
CallErrorKind::Internal,
|
||||||
|
ProtocolErrorCode::Internal,
|
||||||
|
);
|
||||||
|
|
||||||
assert_failure(
|
assert_failure(
|
||||||
provider
|
provider
|
||||||
.call_operation(request(
|
.call_operation(request(
|
||||||
@@ -639,7 +649,7 @@ fn invalid_results_and_backend_failures_keep_distinct_protocol_codes() {
|
|||||||
))
|
))
|
||||||
.unwrap_err(),
|
.unwrap_err(),
|
||||||
CallErrorKind::InvalidResult,
|
CallErrorKind::InvalidResult,
|
||||||
ProtocolErrorCode::InvalidResult,
|
ProtocolErrorCode::OperationOutcomeUnknown,
|
||||||
);
|
);
|
||||||
assert_failure(
|
assert_failure(
|
||||||
provider
|
provider
|
||||||
@@ -653,6 +663,6 @@ fn invalid_results_and_backend_failures_keep_distinct_protocol_codes() {
|
|||||||
))
|
))
|
||||||
.unwrap_err(),
|
.unwrap_err(),
|
||||||
CallErrorKind::Implementation,
|
CallErrorKind::Implementation,
|
||||||
ProtocolErrorCode::Internal,
|
ProtocolErrorCode::OperationOutcomeUnknown,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -223,10 +223,29 @@ impl Display for ValidationError {
|
|||||||
|
|
||||||
impl Error for ValidationError {}
|
impl Error for ValidationError {}
|
||||||
|
|
||||||
/// Stable protocol failure code returned by a host or produced by a client.
|
/// One core interaction whose failure codes form a closed set.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum ProtocolInteraction {
|
||||||
|
/// Fetch one object by its Worldspace path.
|
||||||
|
Fetch,
|
||||||
|
/// Fetch a bounded tree of objects.
|
||||||
|
FetchTree,
|
||||||
|
/// Fetch one interface descriptor by its opaque reference.
|
||||||
|
FetchInterface,
|
||||||
|
/// Invoke one operation on a path-targeted object.
|
||||||
|
CallOperation,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stable protocol failure code returned by a host.
|
||||||
|
///
|
||||||
|
/// This is the complete wire-level code set. A Client treats a malformed success
|
||||||
|
/// or failure response, including a code disallowed for its interaction, as a
|
||||||
|
/// Client-local `InvalidResponse`. An unsupported descriptor format is likewise
|
||||||
|
/// a Client-local `UnsupportedDescriptorFormat`; neither condition is a code a
|
||||||
|
/// Host can return.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum ProtocolErrorCode {
|
pub enum ProtocolErrorCode {
|
||||||
/// The request is structurally invalid.
|
/// The request is structurally invalid before an operation descriptor is fixed.
|
||||||
InvalidRequest,
|
InvalidRequest,
|
||||||
/// The target object is not published.
|
/// The target object is not published.
|
||||||
NotFound,
|
NotFound,
|
||||||
@@ -236,10 +255,8 @@ pub enum ProtocolErrorCode {
|
|||||||
InterfaceMismatch,
|
InterfaceMismatch,
|
||||||
/// The selected operation is not declared by the current interface.
|
/// The selected operation is not declared by the current interface.
|
||||||
OperationNotFound,
|
OperationNotFound,
|
||||||
/// Supplied operation arguments are invalid.
|
/// Supplied arguments do not match the fixed operation declaration.
|
||||||
InvalidArguments,
|
InvalidArguments,
|
||||||
/// A host result does not satisfy the declaration.
|
|
||||||
InvalidResult,
|
|
||||||
/// The operation requires an object validator and none was supplied.
|
/// The operation requires an object validator and none was supplied.
|
||||||
ValidatorRequired,
|
ValidatorRequired,
|
||||||
/// The supplied object validator does not match current state.
|
/// The supplied object validator does not match current state.
|
||||||
@@ -251,9 +268,88 @@ pub enum ProtocolErrorCode {
|
|||||||
/// A published operation is not permitted for the caller.
|
/// A published operation is not permitted for the caller.
|
||||||
PermissionDenied,
|
PermissionDenied,
|
||||||
/// A bounded protocol resource limit was exceeded.
|
/// A bounded protocol resource limit was exceeded.
|
||||||
|
///
|
||||||
|
/// For `call_operation`, the Host guarantees that the operation was not run
|
||||||
|
/// or that all of its effects were rolled back.
|
||||||
ResourceLimitExceeded,
|
ResourceLimitExceeded,
|
||||||
/// The host encountered an internal failure.
|
/// The Host encountered an internal failure.
|
||||||
|
///
|
||||||
|
/// For `call_operation`, the Host guarantees that the operation was not run
|
||||||
|
/// or that all of its effects were rolled back.
|
||||||
Internal,
|
Internal,
|
||||||
|
/// The Host cannot guarantee that the operation was not run or rolled back.
|
||||||
|
///
|
||||||
|
/// A Client must not automatically retry because the operation may have
|
||||||
|
/// committed effects even though no success response was available.
|
||||||
|
OperationOutcomeUnknown,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProtocolErrorCode {
|
||||||
|
/// Every canonical wire-level error code.
|
||||||
|
pub const ALL: [Self; 14] = [
|
||||||
|
Self::InvalidRequest,
|
||||||
|
Self::NotFound,
|
||||||
|
Self::InterfaceNotFound,
|
||||||
|
Self::InterfaceMismatch,
|
||||||
|
Self::OperationNotFound,
|
||||||
|
Self::InvalidArguments,
|
||||||
|
Self::ValidatorRequired,
|
||||||
|
Self::ValidatorMismatch,
|
||||||
|
Self::InterfaceValidatorRequired,
|
||||||
|
Self::InterfaceValidatorMismatch,
|
||||||
|
Self::PermissionDenied,
|
||||||
|
Self::ResourceLimitExceeded,
|
||||||
|
Self::Internal,
|
||||||
|
Self::OperationOutcomeUnknown,
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Whether a Host may return this code for `interaction`.
|
||||||
|
///
|
||||||
|
/// `false` identifies an invalid error response; it does not turn that
|
||||||
|
/// response into another wire-level protocol failure.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn is_allowed_for(self, interaction: ProtocolInteraction) -> bool {
|
||||||
|
use ProtocolErrorCode::{
|
||||||
|
InterfaceMismatch, InterfaceNotFound, InterfaceValidatorMismatch,
|
||||||
|
InterfaceValidatorRequired, Internal, InvalidArguments, InvalidRequest, NotFound,
|
||||||
|
OperationNotFound, OperationOutcomeUnknown, PermissionDenied, ResourceLimitExceeded,
|
||||||
|
ValidatorMismatch, ValidatorRequired,
|
||||||
|
};
|
||||||
|
use ProtocolInteraction::{CallOperation, Fetch, FetchInterface, FetchTree};
|
||||||
|
|
||||||
|
match (interaction, self) {
|
||||||
|
(_, InvalidRequest | ResourceLimitExceeded | Internal) => true,
|
||||||
|
(Fetch | FetchTree, NotFound) => true,
|
||||||
|
(FetchInterface, InterfaceNotFound) => true,
|
||||||
|
(
|
||||||
|
CallOperation,
|
||||||
|
NotFound
|
||||||
|
| InterfaceMismatch
|
||||||
|
| OperationNotFound
|
||||||
|
| InvalidArguments
|
||||||
|
| ValidatorRequired
|
||||||
|
| ValidatorMismatch
|
||||||
|
| InterfaceValidatorRequired
|
||||||
|
| InterfaceValidatorMismatch
|
||||||
|
| PermissionDenied
|
||||||
|
| OperationOutcomeUnknown,
|
||||||
|
) => true,
|
||||||
|
(
|
||||||
|
_,
|
||||||
|
NotFound
|
||||||
|
| InterfaceNotFound
|
||||||
|
| InterfaceMismatch
|
||||||
|
| OperationNotFound
|
||||||
|
| InvalidArguments
|
||||||
|
| ValidatorRequired
|
||||||
|
| ValidatorMismatch
|
||||||
|
| InterfaceValidatorRequired
|
||||||
|
| InterfaceValidatorMismatch
|
||||||
|
| PermissionDenied
|
||||||
|
| OperationOutcomeUnknown,
|
||||||
|
) => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Transport-independent protocol failure.
|
/// Transport-independent protocol failure.
|
||||||
|
|||||||
@@ -16,7 +16,20 @@
|
|||||||
//! | `fetch_interface` | [`FetchInterfaceRequest`] / [`FetchInterfaceResponse`] |
|
//! | `fetch_interface` | [`FetchInterfaceRequest`] / [`FetchInterfaceResponse`] |
|
||||||
//! | Object and interface call preconditions | [`Target`] / [`InterfaceTarget`] |
|
//! | Object and interface call preconditions | [`Target`] / [`InterfaceTarget`] |
|
||||||
//! | `call_operation` | [`CallOperationRequest`] / [`CallOperationResponse`] |
|
//! | `call_operation` | [`CallOperationRequest`] / [`CallOperationResponse`] |
|
||||||
//! | Stable failure taxonomy | [`ProtocolErrorCode`] / [`ProtocolError`] |
|
//! | Stable Host failure taxonomy | [`ProtocolErrorCode`] / [`ProtocolError`] |
|
||||||
|
//! | Per-interaction allowed failure codes | [`ProtocolInteraction`] / [`ProtocolErrorCode::is_allowed_for`] |
|
||||||
|
//!
|
||||||
|
//! [`ProtocolErrorCode`] is a closed set of Host-returned wire failures. After a
|
||||||
|
//! Client receives a response, an invalid Object, Descriptor, operation result,
|
||||||
|
//! malformed error, or error code disallowed by [`ProtocolInteraction`] is a
|
||||||
|
//! Client-local `InvalidResponse`, not another wire error. A descriptor whose
|
||||||
|
//! `format` is not supported is a Client-local `UnsupportedDescriptorFormat`.
|
||||||
|
//!
|
||||||
|
//! For `call_operation`, [`ProtocolErrorCode::Internal`] and
|
||||||
|
//! [`ProtocolErrorCode::ResourceLimitExceeded`] guarantee that the operation did
|
||||||
|
//! not run or that all effects were rolled back. Only
|
||||||
|
//! [`ProtocolErrorCode::OperationOutcomeUnknown`] means the Host cannot make that
|
||||||
|
//! guarantee; Clients must not automatically retry it.
|
||||||
//!
|
//!
|
||||||
//! Paths, object refs, interface refs, and validators remain separate concepts.
|
//! Paths, object refs, interface refs, and validators remain separate concepts.
|
||||||
//! They are represented by ordinary protocol strings/bytes in the corresponding
|
//! They are represented by ordinary protocol strings/bytes in the corresponding
|
||||||
@@ -35,8 +48,8 @@ mod model;
|
|||||||
mod validation;
|
mod validation;
|
||||||
|
|
||||||
pub use error::{
|
pub use error::{
|
||||||
NameNamespace, PathSegment, ProtocolError, ProtocolErrorCode, ValidationError,
|
NameNamespace, PathSegment, ProtocolError, ProtocolErrorCode, ProtocolInteraction,
|
||||||
ValidationErrorKind, ValueKind,
|
ValidationError, ValidationErrorKind, ValueKind,
|
||||||
};
|
};
|
||||||
pub use model::{
|
pub use model::{
|
||||||
CallOperationRequest, CallOperationResponse, Documentation, EnumCase, FetchInterfaceRequest,
|
CallOperationRequest, CallOperationResponse, Documentation, EnumCase, FetchInterfaceRequest,
|
||||||
|
|||||||
@@ -379,6 +379,9 @@ impl FetchRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Validates an object returned for this request.
|
/// Validates an object returned for this request.
|
||||||
|
///
|
||||||
|
/// Failure means the Client received an invalid success response; it is not a
|
||||||
|
/// Host-returned [`crate::ProtocolErrorCode`].
|
||||||
pub fn validate_response(&self, response: &FetchResponse) -> Result<(), ValidationError> {
|
pub fn validate_response(&self, response: &FetchResponse) -> Result<(), ValidationError> {
|
||||||
response.validate_at_path(&self.path)
|
response.validate_at_path(&self.path)
|
||||||
}
|
}
|
||||||
@@ -393,6 +396,9 @@ impl FetchTreeRequest {
|
|||||||
|
|
||||||
impl FetchTreeResponse {
|
impl FetchTreeResponse {
|
||||||
/// Validates tree shape, names, depth, and node limit for `request`.
|
/// Validates tree shape, names, depth, and node limit for `request`.
|
||||||
|
///
|
||||||
|
/// Failure means the Client received an invalid success response; it is not a
|
||||||
|
/// Host-returned [`crate::ProtocolErrorCode`].
|
||||||
pub fn validate_for(&self, request: &FetchTreeRequest) -> Result<(), ValidationError> {
|
pub fn validate_for(&self, request: &FetchTreeRequest) -> Result<(), ValidationError> {
|
||||||
crate::validation::validate_fetch_tree_response(self, request)
|
crate::validation::validate_fetch_tree_response(self, request)
|
||||||
}
|
}
|
||||||
@@ -400,6 +406,11 @@ impl FetchTreeResponse {
|
|||||||
|
|
||||||
impl FetchInterfaceResponse {
|
impl FetchInterfaceResponse {
|
||||||
/// Validates request correspondence and descriptor semantics.
|
/// Validates request correspondence and descriptor semantics.
|
||||||
|
///
|
||||||
|
/// Failure means the Client received an invalid success response; it is not a
|
||||||
|
/// Host-returned [`crate::ProtocolErrorCode`]. Descriptor format support is a
|
||||||
|
/// separate Client concern: an otherwise valid descriptor with an unsupported
|
||||||
|
/// `format` is a Client-local `UnsupportedDescriptorFormat`.
|
||||||
pub fn validate_for(&self, request: &FetchInterfaceRequest) -> Result<(), ValidationError> {
|
pub fn validate_for(&self, request: &FetchInterfaceRequest) -> Result<(), ValidationError> {
|
||||||
crate::validation::validate_fetch_interface_response(self, request)
|
crate::validation::validate_fetch_interface_response(self, request)
|
||||||
}
|
}
|
||||||
@@ -414,6 +425,9 @@ impl CallOperationRequest {
|
|||||||
|
|
||||||
impl CallOperationResponse {
|
impl CallOperationResponse {
|
||||||
/// Validates this result against the named operation in `descriptor`.
|
/// Validates this result against the named operation in `descriptor`.
|
||||||
|
///
|
||||||
|
/// Failure means the Client received an invalid success response; it is not a
|
||||||
|
/// Host-returned [`crate::ProtocolErrorCode`].
|
||||||
pub fn validate_for(
|
pub fn validate_for(
|
||||||
&self,
|
&self,
|
||||||
descriptor: &InterfaceDescriptor,
|
descriptor: &InterfaceDescriptor,
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ use wip_protocol::{
|
|||||||
FetchInterfaceResponse, FetchRequest, FetchTreeRequest, FetchTreeResponse, FieldDeclaration,
|
FetchInterfaceResponse, FetchRequest, FetchTreeRequest, FetchTreeResponse, FieldDeclaration,
|
||||||
INTERFACE_FORMAT_V1, InterfaceDescriptor, InterfaceTarget, MAX_SAFE_INTEGER, NameNamespace,
|
INTERFACE_FORMAT_V1, InterfaceDescriptor, InterfaceTarget, MAX_SAFE_INTEGER, NameNamespace,
|
||||||
Object, ObjectTreeNode, OperationDeclaration, ParameterDeclaration, PathSegment, ProtocolError,
|
Object, ObjectTreeNode, OperationDeclaration, ParameterDeclaration, PathSegment, ProtocolError,
|
||||||
ProtocolErrorCode, ProtocolResult, ReturnDeclaration, Target, TypeDeclaration, TypeExpr,
|
ProtocolErrorCode, ProtocolInteraction, ProtocolResult, ReturnDeclaration, Target,
|
||||||
UnionCase, ValidationErrorKind, Value, ValueKind,
|
TypeDeclaration, TypeExpr, UnionCase, ValidationErrorKind, Value, ValueKind,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn record(entries: impl IntoIterator<Item = (&'static str, Value)>) -> Value {
|
fn record(entries: impl IntoIterator<Item = (&'static str, Value)>) -> Value {
|
||||||
@@ -729,15 +729,16 @@ fn fetch_tree_dtos_validate_names_depth_limit_and_siblings() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn protocol_result_and_every_canonical_error_code_are_available() {
|
fn protocol_result_exposes_the_exact_canonical_wire_error_codes() {
|
||||||
let codes = [
|
assert_eq!(
|
||||||
|
ProtocolErrorCode::ALL,
|
||||||
|
[
|
||||||
ProtocolErrorCode::InvalidRequest,
|
ProtocolErrorCode::InvalidRequest,
|
||||||
ProtocolErrorCode::NotFound,
|
ProtocolErrorCode::NotFound,
|
||||||
ProtocolErrorCode::InterfaceNotFound,
|
ProtocolErrorCode::InterfaceNotFound,
|
||||||
ProtocolErrorCode::InterfaceMismatch,
|
ProtocolErrorCode::InterfaceMismatch,
|
||||||
ProtocolErrorCode::OperationNotFound,
|
ProtocolErrorCode::OperationNotFound,
|
||||||
ProtocolErrorCode::InvalidArguments,
|
ProtocolErrorCode::InvalidArguments,
|
||||||
ProtocolErrorCode::InvalidResult,
|
|
||||||
ProtocolErrorCode::ValidatorRequired,
|
ProtocolErrorCode::ValidatorRequired,
|
||||||
ProtocolErrorCode::ValidatorMismatch,
|
ProtocolErrorCode::ValidatorMismatch,
|
||||||
ProtocolErrorCode::InterfaceValidatorRequired,
|
ProtocolErrorCode::InterfaceValidatorRequired,
|
||||||
@@ -745,8 +746,9 @@ fn protocol_result_and_every_canonical_error_code_are_available() {
|
|||||||
ProtocolErrorCode::PermissionDenied,
|
ProtocolErrorCode::PermissionDenied,
|
||||||
ProtocolErrorCode::ResourceLimitExceeded,
|
ProtocolErrorCode::ResourceLimitExceeded,
|
||||||
ProtocolErrorCode::Internal,
|
ProtocolErrorCode::Internal,
|
||||||
];
|
ProtocolErrorCode::OperationOutcomeUnknown,
|
||||||
assert_eq!(codes.len(), 14);
|
]
|
||||||
|
);
|
||||||
|
|
||||||
let failure: ProtocolResult<()> = ProtocolResult::Failure(ProtocolError {
|
let failure: ProtocolResult<()> = ProtocolResult::Failure(ProtocolError {
|
||||||
code: ProtocolErrorCode::InvalidArguments,
|
code: ProtocolErrorCode::InvalidArguments,
|
||||||
@@ -758,3 +760,127 @@ fn protocol_result_and_every_canonical_error_code_are_available() {
|
|||||||
ProtocolResult::Success(Value::Unit)
|
ProtocolResult::Success(Value::Unit)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn assert_allowed_codes(interaction: ProtocolInteraction, expected: &[ProtocolErrorCode]) {
|
||||||
|
let actual: Vec<_> = ProtocolErrorCode::ALL
|
||||||
|
.into_iter()
|
||||||
|
.filter(|code| code.is_allowed_for(interaction))
|
||||||
|
.collect();
|
||||||
|
assert_eq!(actual, expected);
|
||||||
|
|
||||||
|
for code in ProtocolErrorCode::ALL {
|
||||||
|
assert_eq!(
|
||||||
|
code.is_allowed_for(interaction),
|
||||||
|
expected.contains(&code),
|
||||||
|
"unexpected {code:?} validity for {interaction:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fetch_error_codes_are_exhaustive() {
|
||||||
|
assert_allowed_codes(
|
||||||
|
ProtocolInteraction::Fetch,
|
||||||
|
&[
|
||||||
|
ProtocolErrorCode::InvalidRequest,
|
||||||
|
ProtocolErrorCode::NotFound,
|
||||||
|
ProtocolErrorCode::ResourceLimitExceeded,
|
||||||
|
ProtocolErrorCode::Internal,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fetch_tree_error_codes_are_exhaustive() {
|
||||||
|
assert_allowed_codes(
|
||||||
|
ProtocolInteraction::FetchTree,
|
||||||
|
&[
|
||||||
|
ProtocolErrorCode::InvalidRequest,
|
||||||
|
ProtocolErrorCode::NotFound,
|
||||||
|
ProtocolErrorCode::ResourceLimitExceeded,
|
||||||
|
ProtocolErrorCode::Internal,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fetch_interface_error_codes_are_exhaustive() {
|
||||||
|
assert_allowed_codes(
|
||||||
|
ProtocolInteraction::FetchInterface,
|
||||||
|
&[
|
||||||
|
ProtocolErrorCode::InvalidRequest,
|
||||||
|
ProtocolErrorCode::InterfaceNotFound,
|
||||||
|
ProtocolErrorCode::ResourceLimitExceeded,
|
||||||
|
ProtocolErrorCode::Internal,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn call_operation_error_codes_are_exhaustive() {
|
||||||
|
assert_allowed_codes(
|
||||||
|
ProtocolInteraction::CallOperation,
|
||||||
|
&[
|
||||||
|
ProtocolErrorCode::InvalidRequest,
|
||||||
|
ProtocolErrorCode::NotFound,
|
||||||
|
ProtocolErrorCode::InterfaceMismatch,
|
||||||
|
ProtocolErrorCode::OperationNotFound,
|
||||||
|
ProtocolErrorCode::InvalidArguments,
|
||||||
|
ProtocolErrorCode::ValidatorRequired,
|
||||||
|
ProtocolErrorCode::ValidatorMismatch,
|
||||||
|
ProtocolErrorCode::InterfaceValidatorRequired,
|
||||||
|
ProtocolErrorCode::InterfaceValidatorMismatch,
|
||||||
|
ProtocolErrorCode::PermissionDenied,
|
||||||
|
ProtocolErrorCode::ResourceLimitExceeded,
|
||||||
|
ProtocolErrorCode::Internal,
|
||||||
|
ProtocolErrorCode::OperationOutcomeUnknown,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn invalid_success_payloads_remain_client_local_validation_failures() {
|
||||||
|
let fetch = FetchRequest {
|
||||||
|
path: "/articles/one".into(),
|
||||||
|
};
|
||||||
|
assert!(matches!(
|
||||||
|
fetch.validate_response(&object("other")).unwrap_err().kind,
|
||||||
|
ValidationErrorKind::ObjectNameMismatch { .. }
|
||||||
|
));
|
||||||
|
|
||||||
|
let interface_request = FetchInterfaceRequest {
|
||||||
|
interface: "catalog-v1".into(),
|
||||||
|
};
|
||||||
|
let mut invalid_descriptor = descriptor();
|
||||||
|
invalid_descriptor
|
||||||
|
.operations
|
||||||
|
.push(invalid_descriptor.operations[0].clone());
|
||||||
|
assert!(matches!(
|
||||||
|
FetchInterfaceResponse {
|
||||||
|
interface: interface_request.interface.clone(),
|
||||||
|
descriptor: invalid_descriptor,
|
||||||
|
validator: None,
|
||||||
|
}
|
||||||
|
.validate_for(&interface_request)
|
||||||
|
.unwrap_err()
|
||||||
|
.kind,
|
||||||
|
ValidationErrorKind::DuplicateName {
|
||||||
|
namespace: NameNamespace::Operation,
|
||||||
|
..
|
||||||
|
}
|
||||||
|
));
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
CallOperationResponse {
|
||||||
|
result: Value::String("not a boolean".into()),
|
||||||
|
validator: None,
|
||||||
|
}
|
||||||
|
.validate_for(&descriptor(), "publish")
|
||||||
|
.unwrap_err()
|
||||||
|
.kind,
|
||||||
|
ValidationErrorKind::TypeMismatch {
|
||||||
|
expected: ValueKind::Boolean,
|
||||||
|
actual: ValueKind::String,
|
||||||
|
}
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ pub struct DemoReport {
|
|||||||
pub audit_status: String,
|
pub audit_status: String,
|
||||||
/// Stable failure kind produced by the deliberate host/backend failure.
|
/// Stable failure kind produced by the deliberate host/backend failure.
|
||||||
pub host_failure: CallErrorKind,
|
pub host_failure: CallErrorKind,
|
||||||
/// A protocol-level failure built from the host failure.
|
/// A protocol failure whose operation effects cannot be guaranteed absent.
|
||||||
pub protocol_failure: ProtocolError,
|
pub protocol_failure: ProtocolError,
|
||||||
/// Authorization failure after a fully qualified valid request.
|
/// Authorization failure after a fully qualified valid request.
|
||||||
pub permission_failure: ProtocolErrorCode,
|
pub permission_failure: ProtocolErrorCode,
|
||||||
|
|||||||
@@ -50,7 +50,10 @@ fn domain_host_permission_and_validator_failures_remain_distinct() {
|
|||||||
|
|
||||||
assert!(matches!(report.rejected, PublishOutcome::Rejected(_)));
|
assert!(matches!(report.rejected, PublishOutcome::Rejected(_)));
|
||||||
assert_eq!(report.host_failure, CallErrorKind::Implementation);
|
assert_eq!(report.host_failure, CallErrorKind::Implementation);
|
||||||
assert_eq!(report.protocol_failure.code, ProtocolErrorCode::Internal);
|
assert_eq!(
|
||||||
|
report.protocol_failure.code,
|
||||||
|
ProtocolErrorCode::OperationOutcomeUnknown
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
report.permission_failure,
|
report.permission_failure,
|
||||||
ProtocolErrorCode::PermissionDenied
|
ProtocolErrorCode::PermissionDenied
|
||||||
|
|||||||
Reference in New Issue
Block a user