1211 lines
43 KiB
Rust
1211 lines
43 KiB
Rust
//! Host-side publication and operation dispatch for Web Interface Protocol objects.
|
|
//!
|
|
//! # Responsibility boundaries
|
|
//!
|
|
//! [`InterfaceRegistry::register`] validates and freezes a generated interface
|
|
//! descriptor, assigns an opaque [`InterfaceRef`], and leaves that reference
|
|
//! outside the descriptor. [`PublishedObject::expose`] explicitly combines a
|
|
//! request-scoped Rust object with a registered interface; its
|
|
//! [`InterfaceImplementation`] bound proves at compile time that the object
|
|
//! implements the interface. Implementing a Rust trait alone never publishes it,
|
|
//! and a publication with no interfaces is valid.
|
|
//!
|
|
//! [`ObjectProvider::call_operation`] resolves `target.path` exactly once and
|
|
//! keeps the resulting object, interface membership, descriptor, implementation,
|
|
//! and backend target fixed within the provider's [`ObjectProvider::with_object`]
|
|
//! callback. Dispatch is qualified by `(interface reference, operation name)`.
|
|
//! The same frozen operation declaration validates both arguments and results.
|
|
//!
|
|
//! A provider is responsible for entering a backend transaction, lock,
|
|
//! stable-handle scope, or conditional-update primitive before invoking the
|
|
//! operation callback and retaining it through commit or rollback. The typed
|
|
//! [`OperationAttempt`] boundary distinguishes pre-dispatch rejection from started
|
|
//! execution. Only an implementation-supplied non-commit guarantee or a
|
|
//! provider-confirmed rollback allows a post-invocation failure to become
|
|
//! `Internal` or `ResourceLimitExceeded`; every other outcome is
|
|
//! `OperationOutcomeUnknown`. Mutable operations must make object-validator
|
|
//! comparison and backend commit atomic, and the validator returned after dispatch
|
|
//! must describe the state that becomes committed.
|
|
|
|
#![deny(missing_docs)]
|
|
|
|
use std::any::TypeId;
|
|
use std::collections::{BTreeMap, HashMap, HashSet};
|
|
use std::error::Error;
|
|
use std::fmt::{self, Display, Formatter};
|
|
use std::marker::PhantomData;
|
|
use std::panic::{AssertUnwindSafe, catch_unwind};
|
|
use std::sync::Arc;
|
|
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
|
use std::time::Instant;
|
|
|
|
use wip_interface::DispatchError;
|
|
pub use wip_interface::{
|
|
EffectGuarantee, HostError, HostErrorKind, InterfaceDefinition, InterfaceImplementation,
|
|
};
|
|
pub use wip_protocol;
|
|
pub use wip_protocol::{
|
|
CallOperationRequest as CallRequest, CallOperationResponse as CallResult, InterfaceTarget,
|
|
Target,
|
|
};
|
|
use wip_protocol::{
|
|
FetchInterfaceRequest, FetchInterfaceResponse, InterfaceDescriptor, Object, ProtocolError,
|
|
ProtocolErrorCode, ValidationError, Value,
|
|
};
|
|
|
|
/// Opaque bytes used as a conditional execution precondition.
|
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
|
pub struct Validator(Vec<u8>);
|
|
|
|
impl Validator {
|
|
/// Creates a validator without assigning meaning to its bytes.
|
|
#[must_use]
|
|
pub fn new(bytes: impl Into<Vec<u8>>) -> Self {
|
|
Self(bytes.into())
|
|
}
|
|
|
|
/// Returns the opaque bytes for transport encoding or comparison.
|
|
#[must_use]
|
|
pub fn as_bytes(&self) -> &[u8] {
|
|
&self.0
|
|
}
|
|
}
|
|
|
|
/// Runtime-neutral cooperative cancellation signal.
|
|
///
|
|
/// Clones observe the same atomic state, so transports, providers, and worker
|
|
/// runtimes can share the token without depending on a particular async runtime.
|
|
#[derive(Debug, Clone, Default)]
|
|
pub struct CancellationToken(Arc<AtomicBool>);
|
|
|
|
impl CancellationToken {
|
|
/// Creates a cancellation token in the active state.
|
|
#[must_use]
|
|
pub fn new() -> Self {
|
|
Self::default()
|
|
}
|
|
|
|
/// Requests cooperative cancellation.
|
|
pub fn cancel(&self) {
|
|
self.0.store(true, Ordering::Release);
|
|
}
|
|
|
|
/// Returns whether cancellation has been requested.
|
|
#[must_use]
|
|
pub fn is_cancelled(&self) -> bool {
|
|
self.0.load(Ordering::Acquire)
|
|
}
|
|
}
|
|
|
|
/// Execution controls propagated through object resolution and operation dispatch.
|
|
///
|
|
/// Cancellation and deadlines are checked immediately before typed implementation
|
|
/// invocation. Providers also receive the complete context and remain responsible
|
|
/// for enforcing controls while backend work is in progress. `resource_budget` is
|
|
/// an opaque Host-defined work-unit ceiling; this crate deliberately does not
|
|
/// prescribe an async runtime or accounting mechanism.
|
|
#[derive(Debug, Clone)]
|
|
pub struct ExecutionContext {
|
|
cancellation: CancellationToken,
|
|
deadline: Option<Instant>,
|
|
resource_budget: Option<u64>,
|
|
}
|
|
|
|
impl Default for ExecutionContext {
|
|
fn default() -> Self {
|
|
Self {
|
|
cancellation: CancellationToken::new(),
|
|
deadline: None,
|
|
resource_budget: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ExecutionContext {
|
|
/// Creates an execution context without a deadline or resource budget.
|
|
#[must_use]
|
|
pub fn new() -> Self {
|
|
Self::default()
|
|
}
|
|
|
|
/// Uses `cancellation` as this call's cooperative cancellation signal.
|
|
#[must_use]
|
|
pub fn with_cancellation(mut self, cancellation: CancellationToken) -> Self {
|
|
self.cancellation = cancellation;
|
|
self
|
|
}
|
|
|
|
/// Sets the monotonic deadline for this call.
|
|
#[must_use]
|
|
pub fn with_deadline(mut self, deadline: Instant) -> Self {
|
|
self.deadline = Some(deadline);
|
|
self
|
|
}
|
|
|
|
/// Sets an opaque Host-defined work-unit ceiling for this call.
|
|
#[must_use]
|
|
pub fn with_resource_budget(mut self, resource_budget: u64) -> Self {
|
|
self.resource_budget = Some(resource_budget);
|
|
self
|
|
}
|
|
|
|
/// Returns the shared cooperative cancellation token.
|
|
#[must_use]
|
|
pub fn cancellation(&self) -> &CancellationToken {
|
|
&self.cancellation
|
|
}
|
|
|
|
/// Returns the monotonic deadline, if one was supplied.
|
|
#[must_use]
|
|
pub fn deadline(&self) -> Option<Instant> {
|
|
self.deadline
|
|
}
|
|
|
|
/// Returns the opaque Host-defined work-unit ceiling, if one was supplied.
|
|
#[must_use]
|
|
pub fn resource_budget(&self) -> Option<u64> {
|
|
self.resource_budget
|
|
}
|
|
|
|
fn preflight(&self) -> Result<(), CallError> {
|
|
if self.cancellation.is_cancelled() {
|
|
return Err(CallError::new(
|
|
CallErrorKind::ResourceLimitExceeded,
|
|
"operation was cancelled before execution",
|
|
));
|
|
}
|
|
if self
|
|
.deadline
|
|
.is_some_and(|deadline| Instant::now() >= deadline)
|
|
{
|
|
return Err(CallError::new(
|
|
CallErrorKind::ResourceLimitExceeded,
|
|
"operation deadline elapsed before execution",
|
|
));
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// Opaque host-assigned reference to one registered interface artifact.
|
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
|
pub struct InterfaceRef(String);
|
|
|
|
impl InterfaceRef {
|
|
/// Returns the opaque reference for a protocol transport.
|
|
#[must_use]
|
|
pub fn as_str(&self) -> &str {
|
|
&self.0
|
|
}
|
|
}
|
|
|
|
/// One atomic observation of an interface reference, descriptor, and validator.
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
pub struct InterfaceObservation {
|
|
/// Opaque reference selected by published objects and operation calls.
|
|
pub reference: InterfaceRef,
|
|
/// Canonical descriptor frozen when the interface was registered.
|
|
pub descriptor: InterfaceDescriptor,
|
|
/// Optional validator. Its presence makes it a required call precondition.
|
|
pub validator: Option<Validator>,
|
|
}
|
|
|
|
impl InterfaceObservation {
|
|
/// Converts this host observation to a validated canonical protocol response.
|
|
///
|
|
/// A Host-side response construction failure is returned as `Internal`; it is
|
|
/// never emitted as a successful response for a Client to reinterpret as a
|
|
/// local validation error.
|
|
pub fn protocol_response(&self) -> Result<FetchInterfaceResponse, ProtocolError> {
|
|
let response = FetchInterfaceResponse {
|
|
interface: self.reference.0.clone(),
|
|
descriptor: self.descriptor.clone(),
|
|
validator: self.validator.as_ref().map(|validator| validator.0.clone()),
|
|
};
|
|
response
|
|
.validate_for(&FetchInterfaceRequest {
|
|
interface: self.reference.0.clone(),
|
|
})
|
|
.map_err(|_| ProtocolError {
|
|
code: ProtocolErrorCode::Internal,
|
|
message: "Host produced an invalid interface response".into(),
|
|
})?;
|
|
Ok(response)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct InterfaceArtifact {
|
|
reference: InterfaceRef,
|
|
descriptor: InterfaceDescriptor,
|
|
validator: Option<Validator>,
|
|
}
|
|
|
|
/// A typed registration token for a generated interface definition.
|
|
///
|
|
/// Tokens can be reused by any number of request-scoped objects. They expose an
|
|
/// opaque reference, not the Rust type name or an address.
|
|
pub struct RegisteredInterface<D> {
|
|
artifact: Arc<InterfaceArtifact>,
|
|
definition: PhantomData<fn() -> D>,
|
|
}
|
|
|
|
impl<D> Clone for RegisteredInterface<D> {
|
|
fn clone(&self) -> Self {
|
|
Self {
|
|
artifact: Arc::clone(&self.artifact),
|
|
definition: PhantomData,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<D> RegisteredInterface<D> {
|
|
/// Returns the opaque protocol reference assigned by the registry.
|
|
#[must_use]
|
|
pub fn reference(&self) -> &InterfaceRef {
|
|
&self.artifact.reference
|
|
}
|
|
}
|
|
|
|
/// Registry of validated, immutable interface artifacts.
|
|
///
|
|
/// Generated descriptors are never rewritten to contain their assigned
|
|
/// references. A validator-free artifact is immutable for the lifetime of its
|
|
/// reference. A validator-bearing artifact requires that exact validator on a
|
|
/// call. Re-registering a definition is rejected rather than silently changing
|
|
/// the observation associated with an existing reference.
|
|
#[derive(Debug)]
|
|
pub struct InterfaceRegistry {
|
|
namespace: u64,
|
|
next_reference: u64,
|
|
definitions: HashSet<TypeId>,
|
|
artifacts: HashMap<InterfaceRef, Arc<InterfaceArtifact>>,
|
|
}
|
|
|
|
static NEXT_REGISTRY_NAMESPACE: AtomicU64 = AtomicU64::new(1);
|
|
|
|
impl Default for InterfaceRegistry {
|
|
fn default() -> Self {
|
|
Self {
|
|
namespace: NEXT_REGISTRY_NAMESPACE.fetch_add(1, Ordering::Relaxed),
|
|
next_reference: 0,
|
|
definitions: HashSet::new(),
|
|
artifacts: HashMap::new(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl InterfaceRegistry {
|
|
/// Creates an empty interface registry.
|
|
#[must_use]
|
|
pub fn new() -> Self {
|
|
Self::default()
|
|
}
|
|
|
|
/// Registers and freezes one generated interface definition.
|
|
pub fn register<D>(
|
|
&mut self,
|
|
_definition: D,
|
|
validator: Option<Validator>,
|
|
) -> Result<RegisteredInterface<D>, RegistrationError>
|
|
where
|
|
D: InterfaceDefinition + 'static,
|
|
{
|
|
let definition = TypeId::of::<D>();
|
|
if self.definitions.contains(&definition) {
|
|
return Err(RegistrationError::AlreadyRegistered);
|
|
}
|
|
|
|
let descriptor = D::descriptor();
|
|
descriptor
|
|
.validate()
|
|
.map_err(RegistrationError::InvalidDescriptor)?;
|
|
|
|
let reference = InterfaceRef(format!(
|
|
"wip-interface:{}:{}",
|
|
self.namespace, self.next_reference
|
|
));
|
|
self.next_reference = self
|
|
.next_reference
|
|
.checked_add(1)
|
|
.ok_or(RegistrationError::ReferenceSpaceExhausted)?;
|
|
let artifact = Arc::new(InterfaceArtifact {
|
|
reference: reference.clone(),
|
|
descriptor,
|
|
validator,
|
|
});
|
|
self.definitions.insert(definition);
|
|
self.artifacts.insert(reference, Arc::clone(&artifact));
|
|
|
|
Ok(RegisteredInterface {
|
|
artifact,
|
|
definition: PhantomData,
|
|
})
|
|
}
|
|
|
|
/// Resolves a transport-provided opaque reference string in this registry.
|
|
#[must_use]
|
|
pub fn resolve(&self, reference: &str) -> Option<InterfaceRef> {
|
|
let reference = InterfaceRef(reference.to_owned());
|
|
self.artifacts.contains_key(&reference).then_some(reference)
|
|
}
|
|
|
|
/// Observes a registered interface by its host-side reference.
|
|
#[must_use]
|
|
pub fn observe(&self, reference: &InterfaceRef) -> Option<InterfaceObservation> {
|
|
self.artifacts.get(reference).map(Self::observation)
|
|
}
|
|
|
|
/// Implements canonical `fetch_interface` semantics for an opaque reference.
|
|
///
|
|
/// Unknown and currently unpublished references map to `InterfaceNotFound`,
|
|
/// not object membership errors or authorization failures.
|
|
pub fn fetch_interface(&self, reference: &str) -> Result<InterfaceObservation, ProtocolError> {
|
|
let reference = InterfaceRef(reference.to_owned());
|
|
self.artifacts
|
|
.get(&reference)
|
|
.map(Self::observation)
|
|
.ok_or_else(|| ProtocolError {
|
|
code: ProtocolErrorCode::InterfaceNotFound,
|
|
message: "interface reference is not currently published".into(),
|
|
})
|
|
}
|
|
|
|
fn observation(artifact: &Arc<InterfaceArtifact>) -> InterfaceObservation {
|
|
InterfaceObservation {
|
|
reference: artifact.reference.clone(),
|
|
descriptor: artifact.descriptor.clone(),
|
|
validator: artifact.validator.clone(),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Failure while registering a generated interface definition.
|
|
#[derive(Debug)]
|
|
pub enum RegistrationError {
|
|
/// The same generated definition was already assigned a reference.
|
|
AlreadyRegistered,
|
|
/// The generated descriptor is structurally invalid.
|
|
InvalidDescriptor(ValidationError),
|
|
/// The registry cannot assign another opaque reference.
|
|
ReferenceSpaceExhausted,
|
|
}
|
|
|
|
impl Display for RegistrationError {
|
|
fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
|
|
match self {
|
|
Self::AlreadyRegistered => {
|
|
formatter.write_str("interface definition already registered")
|
|
}
|
|
Self::InvalidDescriptor(error) => {
|
|
write!(formatter, "invalid interface descriptor: {error}")
|
|
}
|
|
Self::ReferenceSpaceExhausted => {
|
|
formatter.write_str("interface reference space exhausted")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Error for RegistrationError {
|
|
fn source(&self) -> Option<&(dyn Error + 'static)> {
|
|
match self {
|
|
Self::InvalidDescriptor(error) => Some(error),
|
|
Self::AlreadyRegistered | Self::ReferenceSpaceExhausted => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Public metadata and conditional-execution policy for a host-private object.
|
|
///
|
|
/// Only values returned by this trait are projected into protocol metadata.
|
|
/// Paths, children, backend handles, credentials, and resolution state are not
|
|
/// part of this API and therefore cannot be serialized into [`Object`].
|
|
pub trait WipObject {
|
|
/// Returns the public path-segment name of this object.
|
|
fn name(&self) -> &str;
|
|
|
|
/// Returns a short public description, if one is available.
|
|
fn description(&self) -> Option<&str> {
|
|
None
|
|
}
|
|
|
|
/// Returns optional opaque identity within the client-facing Worldspace.
|
|
fn reference(&self) -> Option<&str> {
|
|
None
|
|
}
|
|
|
|
/// Returns the current opaque object validator, if any.
|
|
fn validator(&self) -> Option<&Validator> {
|
|
None
|
|
}
|
|
|
|
/// Whether this specific qualified operation requires an object validator.
|
|
///
|
|
/// This hook is evaluated only after membership, interface validators, and
|
|
/// operation existence have been checked, preserving canonical precedence.
|
|
fn requires_validator(&self, _interface: &InterfaceRef, _operation: &str) -> bool {
|
|
false
|
|
}
|
|
}
|
|
|
|
type DispatchFn<T> = fn(&T, &str, BTreeMap<String, Value>) -> Result<Value, DispatchError>;
|
|
|
|
struct BoundInterface<T> {
|
|
artifact: Arc<InterfaceArtifact>,
|
|
dispatch: DispatchFn<T>,
|
|
}
|
|
|
|
/// A host-private object instance combined with an explicit ordered interface set.
|
|
///
|
|
/// The instance is owned by this value and can therefore be request-scoped. No
|
|
/// global object registry is required. Worldspace paths remain provider context
|
|
/// and are deliberately absent from the publication.
|
|
pub struct PublishedObject<T: WipObject> {
|
|
implementation: T,
|
|
interfaces: Vec<BoundInterface<T>>,
|
|
}
|
|
|
|
impl<T: WipObject> PublishedObject<T> {
|
|
/// Starts a publication with no exposed interfaces.
|
|
#[must_use]
|
|
pub fn new(implementation: T) -> Self {
|
|
Self {
|
|
implementation,
|
|
interfaces: Vec::new(),
|
|
}
|
|
}
|
|
|
|
/// Explicitly exposes a registered generated interface in insertion order.
|
|
///
|
|
/// The `InterfaceImplementation<T>` bound is generated only when `T`
|
|
/// implements the corresponding Rust interface trait. Duplicate opaque
|
|
/// references are rejected.
|
|
pub fn expose<D>(mut self, interface: &RegisteredInterface<D>) -> Result<Self, PublicationError>
|
|
where
|
|
D: InterfaceImplementation<T>,
|
|
{
|
|
if self
|
|
.interfaces
|
|
.iter()
|
|
.any(|bound| bound.artifact.reference == interface.artifact.reference)
|
|
{
|
|
return Err(PublicationError::DuplicateInterface(
|
|
interface.artifact.reference.clone(),
|
|
));
|
|
}
|
|
|
|
self.interfaces.push(BoundInterface {
|
|
artifact: Arc::clone(&interface.artifact),
|
|
dispatch: D::dispatch,
|
|
});
|
|
Ok(self)
|
|
}
|
|
|
|
/// Returns the host-private implementation without projecting its fields.
|
|
#[must_use]
|
|
pub fn implementation(&self) -> &T {
|
|
&self.implementation
|
|
}
|
|
|
|
/// Builds canonical protocol object metadata in interface insertion order.
|
|
///
|
|
/// The observation contains no path, children, descriptor, backend handle,
|
|
/// credential, or provider resolution state.
|
|
#[must_use]
|
|
pub fn protocol_object(&self) -> Object {
|
|
Object {
|
|
name: self.implementation.name().to_owned(),
|
|
description: self.implementation.description().map(str::to_owned),
|
|
interfaces: self
|
|
.interfaces
|
|
.iter()
|
|
.map(|bound| bound.artifact.reference.0.clone())
|
|
.collect(),
|
|
r#ref: self.implementation.reference().map(str::to_owned),
|
|
validator: self
|
|
.implementation
|
|
.validator()
|
|
.map(|validator| validator.0.clone()),
|
|
}
|
|
}
|
|
|
|
fn dispatch_resolved(
|
|
&self,
|
|
context: &ExecutionContext,
|
|
request: CallRequest,
|
|
authorize: impl FnOnce(&T, &InterfaceRef, &str) -> bool,
|
|
) -> OperationAttempt {
|
|
let CallRequest {
|
|
target,
|
|
interface,
|
|
operation,
|
|
arguments,
|
|
} = request;
|
|
self.dispatch_resolved_with_decoder(
|
|
context,
|
|
target,
|
|
interface,
|
|
operation,
|
|
|_| Ok::<_, std::convert::Infallible>(arguments),
|
|
authorize,
|
|
)
|
|
}
|
|
|
|
fn dispatch_resolved_with_decoder<E>(
|
|
&self,
|
|
context: &ExecutionContext,
|
|
target: Target,
|
|
interface: InterfaceTarget,
|
|
operation: String,
|
|
decode_arguments: impl FnOnce(&InterfaceDescriptor) -> Result<BTreeMap<String, Value>, E>,
|
|
authorize: impl FnOnce(&T, &InterfaceRef, &str) -> bool,
|
|
) -> OperationAttempt {
|
|
let pre_dispatch_failure =
|
|
|kind, message: &str| OperationAttempt::NotStarted(CallError::new(kind, message));
|
|
let current_object_validator = self.implementation.validator();
|
|
if let Some(supplied) = target.validator.as_deref()
|
|
&& current_object_validator.map(Validator::as_bytes) != Some(supplied)
|
|
{
|
|
return pre_dispatch_failure(
|
|
CallErrorKind::ValidatorMismatch,
|
|
"object validator does not match current object state",
|
|
);
|
|
}
|
|
|
|
let Some(bound) = self
|
|
.interfaces
|
|
.iter()
|
|
.find(|bound| bound.artifact.reference.as_str() == interface.reference.as_str())
|
|
else {
|
|
return pre_dispatch_failure(
|
|
CallErrorKind::InterfaceMismatch,
|
|
"interface is not a member of the current object publication",
|
|
);
|
|
};
|
|
|
|
match (
|
|
bound.artifact.validator.as_ref(),
|
|
interface.validator.as_deref(),
|
|
) {
|
|
(Some(_), None) => {
|
|
return pre_dispatch_failure(
|
|
CallErrorKind::InterfaceValidatorRequired,
|
|
"registered interface requires a validator",
|
|
);
|
|
}
|
|
(expected, Some(actual)) if expected.map(Validator::as_bytes) != Some(actual) => {
|
|
return pre_dispatch_failure(
|
|
CallErrorKind::InterfaceValidatorMismatch,
|
|
"interface validator does not match the registered descriptor",
|
|
);
|
|
}
|
|
_ => {}
|
|
}
|
|
|
|
if !bound
|
|
.artifact
|
|
.descriptor
|
|
.operations
|
|
.iter()
|
|
.any(|declared| declared.name == operation)
|
|
{
|
|
return pre_dispatch_failure(
|
|
CallErrorKind::OperationNotFound,
|
|
&format!("operation `{operation}` is not declared"),
|
|
);
|
|
}
|
|
|
|
if target.validator.is_none()
|
|
&& self
|
|
.implementation
|
|
.requires_validator(&bound.artifact.reference, &operation)
|
|
{
|
|
return pre_dispatch_failure(
|
|
CallErrorKind::ValidatorRequired,
|
|
"this operation requires an object validator",
|
|
);
|
|
}
|
|
|
|
let arguments = match decode_arguments(&bound.artifact.descriptor) {
|
|
Ok(arguments) => arguments,
|
|
Err(_) => {
|
|
return pre_dispatch_failure(
|
|
CallErrorKind::InvalidArguments,
|
|
"operation arguments could not be decoded",
|
|
);
|
|
}
|
|
};
|
|
if let Err(error) = bound
|
|
.artifact
|
|
.descriptor
|
|
.validate_arguments(&operation, &arguments)
|
|
{
|
|
return pre_dispatch_failure(CallErrorKind::InvalidArguments, &error.to_string());
|
|
}
|
|
|
|
if !authorize(&self.implementation, &bound.artifact.reference, &operation) {
|
|
return pre_dispatch_failure(
|
|
CallErrorKind::PermissionDenied,
|
|
"operation is not permitted for this caller",
|
|
);
|
|
}
|
|
|
|
if let Err(error) = context.preflight() {
|
|
return OperationAttempt::NotStarted(error);
|
|
}
|
|
|
|
let value = match (bound.dispatch)(&self.implementation, &operation, arguments) {
|
|
Ok(value) => value,
|
|
Err(
|
|
error @ (DispatchError::UnknownOperation(_)
|
|
| DispatchError::InvalidArguments(_)
|
|
| DispatchError::Decode(_)),
|
|
) => return OperationAttempt::NotStarted(CallError::from_pre_dispatch(error)),
|
|
Err(error) => {
|
|
return OperationAttempt::Started(Err(OperationFailure::from_dispatch(error)));
|
|
}
|
|
};
|
|
|
|
if bound
|
|
.artifact
|
|
.descriptor
|
|
.validate_result(&operation, &value)
|
|
.is_err()
|
|
{
|
|
return OperationAttempt::Started(Err(OperationFailure::internal(
|
|
EffectGuarantee::Unknown,
|
|
"operation implementation produced an invalid result",
|
|
)));
|
|
}
|
|
|
|
OperationAttempt::Started(Ok(CallResult {
|
|
result: value,
|
|
validator: self
|
|
.implementation
|
|
.validator()
|
|
.map(|validator| validator.0.clone()),
|
|
}))
|
|
}
|
|
|
|
/// Consumes the publication and returns its host-private implementation.
|
|
#[must_use]
|
|
pub fn into_inner(self) -> T {
|
|
self.implementation
|
|
}
|
|
}
|
|
|
|
/// A post-invocation failure awaiting final provider effect disposition.
|
|
///
|
|
/// Providers may upgrade an unknown failure with [`confirm_rollback`](Self::confirm_rollback)
|
|
/// only after the backend transaction has definitely rolled back. Diagnostics are
|
|
/// intentionally generic and safe for protocol exposure.
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct OperationFailure {
|
|
kind: HostErrorKind,
|
|
effect_guarantee: EffectGuarantee,
|
|
message: String,
|
|
}
|
|
|
|
impl OperationFailure {
|
|
fn internal(effect_guarantee: EffectGuarantee, message: impl Into<String>) -> Self {
|
|
Self {
|
|
kind: HostErrorKind::Internal,
|
|
effect_guarantee,
|
|
message: bounded_message(message.into()),
|
|
}
|
|
}
|
|
|
|
fn from_dispatch(error: DispatchError) -> Self {
|
|
match error {
|
|
DispatchError::Host(error) => Self {
|
|
kind: error.kind(),
|
|
effect_guarantee: error.effect_guarantee(),
|
|
message: match error.kind() {
|
|
HostErrorKind::Internal => "operation implementation failed".into(),
|
|
HostErrorKind::ResourceLimitExceeded => {
|
|
"operation resource limit was exceeded".into()
|
|
}
|
|
},
|
|
},
|
|
DispatchError::Panic => Self::internal(
|
|
EffectGuarantee::Unknown,
|
|
"operation implementation panicked",
|
|
),
|
|
DispatchError::Encode(_) => Self::internal(
|
|
EffectGuarantee::Unknown,
|
|
"operation result could not be encoded",
|
|
),
|
|
DispatchError::InvalidResult(_) => Self::internal(
|
|
EffectGuarantee::Unknown,
|
|
"operation implementation produced an invalid result",
|
|
),
|
|
DispatchError::UnknownOperation(_)
|
|
| DispatchError::InvalidArguments(_)
|
|
| DispatchError::Decode(_) => {
|
|
unreachable!("pre-dispatch failures are classified before OperationFailure")
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Returns the Host failure category used when effects are not committed.
|
|
#[must_use]
|
|
pub fn kind(&self) -> HostErrorKind {
|
|
self.kind
|
|
}
|
|
|
|
/// Returns the current effect guarantee.
|
|
#[must_use]
|
|
pub fn effect_guarantee(&self) -> EffectGuarantee {
|
|
self.effect_guarantee
|
|
}
|
|
|
|
/// Confirms that the provider rolled back all operation effects.
|
|
///
|
|
/// Calling this method is a semantic assertion by the provider and must occur
|
|
/// only after its atomic backend boundary has completed rollback.
|
|
#[must_use]
|
|
pub fn confirm_rollback(mut self) -> Self {
|
|
self.effect_guarantee = EffectGuarantee::NotCommitted;
|
|
self
|
|
}
|
|
}
|
|
|
|
/// Dispatch progress returned to the provider's atomic execution boundary.
|
|
#[derive(Debug)]
|
|
pub enum OperationAttempt {
|
|
/// A canonical precondition failed and typed implementation code did not run.
|
|
NotStarted(CallError),
|
|
/// Typed implementation invocation began, with either a result or a typed
|
|
/// post-invocation failure.
|
|
Started(Result<CallResult, OperationFailure>),
|
|
}
|
|
|
|
impl OperationAttempt {
|
|
/// Marks a post-invocation failure as fully rolled back.
|
|
///
|
|
/// Successful attempts and pre-dispatch failures are returned unchanged.
|
|
#[must_use]
|
|
pub fn confirm_rollback(self) -> Self {
|
|
match self {
|
|
Self::Started(Err(error)) => Self::Started(Err(error.confirm_rollback())),
|
|
other => other,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Result of one provider resolution or execution boundary.
|
|
#[derive(Debug)]
|
|
pub enum ProviderResult<T> {
|
|
/// The path resolved and the provider callback completed.
|
|
Found(T),
|
|
/// The path is not currently published to this caller.
|
|
NotFound,
|
|
/// Provider or backend execution failed.
|
|
///
|
|
/// During `call_operation`, the embedded effect guarantee determines whether
|
|
/// the failure is safe to expose as `Internal`/`ResourceLimitExceeded` or must
|
|
/// become `OperationOutcomeUnknown`.
|
|
Failure(HostError),
|
|
}
|
|
|
|
/// A request-scoped Worldspace resolver and execution boundary.
|
|
///
|
|
/// `with_object` must resolve the current object at `path` exactly once and pass
|
|
/// a publication that pins its current interface set, implementations, and
|
|
/// backend target for the duration of `operation`. The execution context is the
|
|
/// runtime-neutral carrier for cancellation, deadline, and resource controls.
|
|
/// The borrowed publication cannot escape the callback.
|
|
///
|
|
/// Providers that can roll back mutable operations should override
|
|
/// [`ObjectProvider::execute_operation`], hold their transaction across the
|
|
/// callback, and call [`OperationAttempt::confirm_rollback`] only after rollback
|
|
/// succeeds. The default boundary conservatively preserves unknown outcomes.
|
|
pub trait ObjectProvider {
|
|
/// Host-private object type produced by this provider.
|
|
type Object: WipObject;
|
|
|
|
/// Runs a callback against the single object snapshot resolved at `path`.
|
|
fn with_object<R>(
|
|
&self,
|
|
context: &ExecutionContext,
|
|
path: &str,
|
|
operation: impl FnOnce(&PublishedObject<Self::Object>) -> R,
|
|
) -> ProviderResult<R>;
|
|
|
|
/// Runs one operation attempt inside the provider's atomic backend boundary.
|
|
///
|
|
/// The default implementation delegates to [`ObjectProvider::with_object`]
|
|
/// and therefore does not add a rollback guarantee. Transactional providers
|
|
/// can override this method, inspect the returned [`OperationAttempt`], roll
|
|
/// back a failed started attempt, and return its confirmed form.
|
|
fn execute_operation(
|
|
&self,
|
|
context: &ExecutionContext,
|
|
path: &str,
|
|
operation: impl FnOnce(&PublishedObject<Self::Object>) -> OperationAttempt,
|
|
) -> ProviderResult<OperationAttempt> {
|
|
self.with_object(context, path, operation)
|
|
}
|
|
|
|
/// Decides whether a declared operation may execute for the current caller.
|
|
///
|
|
/// The hook runs after all validators, membership, operation lookup, and
|
|
/// argument validation, but before implementation code. Returning `false`
|
|
/// exposes the operation while producing `PermissionDenied`.
|
|
fn authorize(
|
|
&self,
|
|
_path: &str,
|
|
_object: &Self::Object,
|
|
_interface: &InterfaceRef,
|
|
_operation: &str,
|
|
) -> bool {
|
|
true
|
|
}
|
|
|
|
/// Fetches the canonical object projection currently published at `path`.
|
|
fn fetch(&self, path: &str) -> Result<Object, CallError> {
|
|
self.fetch_with_context(&ExecutionContext::default(), path)
|
|
}
|
|
|
|
/// Fetches an object while propagating runtime execution controls.
|
|
fn fetch_with_context(
|
|
&self,
|
|
context: &ExecutionContext,
|
|
path: &str,
|
|
) -> Result<Object, CallError> {
|
|
wip_protocol::validate_path(path)
|
|
.map_err(|error| CallError::new(CallErrorKind::InvalidRequest, error.to_string()))?;
|
|
let resolved = catch_unwind(AssertUnwindSafe(|| {
|
|
self.with_object(context, path, PublishedObject::protocol_object)
|
|
}))
|
|
.map_err(|_| {
|
|
CallError::new(
|
|
CallErrorKind::Internal,
|
|
"provider panicked while fetching an object",
|
|
)
|
|
})?;
|
|
let object = match resolved {
|
|
ProviderResult::Found(object) => object,
|
|
ProviderResult::NotFound => {
|
|
return Err(CallError::new(
|
|
CallErrorKind::NotFound,
|
|
"target path is not published",
|
|
));
|
|
}
|
|
ProviderResult::Failure(error) => return Err(CallError::from_read_failure(error)),
|
|
};
|
|
object.validate_at_path(path).map_err(|_| {
|
|
CallError::new(
|
|
CallErrorKind::Internal,
|
|
"Host produced an invalid object response",
|
|
)
|
|
})?;
|
|
Ok(object)
|
|
}
|
|
|
|
/// Resolves and executes a canonical path-targeted operation request.
|
|
///
|
|
/// Structural path validation precedes provider resolution. For a valid
|
|
/// request, failure checks follow the exact canonical precedence: path,
|
|
/// supplied object validator mismatch, membership, interface validator
|
|
/// requirement and mismatch, operation existence, object validator
|
|
/// requirement, arguments, then authorization. No failed precondition
|
|
/// invokes typed implementation code.
|
|
fn call_operation(&self, request: CallRequest) -> Result<CallResult, CallError> {
|
|
self.call_operation_with_context(&ExecutionContext::default(), request)
|
|
}
|
|
|
|
/// Executes an operation while propagating cancellation, deadline, and
|
|
/// resource-budget controls to the provider and runtime preflight check.
|
|
fn call_operation_with_context(
|
|
&self,
|
|
context: &ExecutionContext,
|
|
request: CallRequest,
|
|
) -> Result<CallResult, CallError> {
|
|
wip_protocol::validate_path(&request.target.path)
|
|
.map_err(|error| CallError::new(CallErrorKind::InvalidRequest, error.to_string()))?;
|
|
let path = request.target.path.clone();
|
|
let execution = catch_unwind(AssertUnwindSafe(|| {
|
|
self.execute_operation(context, &path, |publication| {
|
|
publication.dispatch_resolved(context, request, |object, interface, operation| {
|
|
self.authorize(&path, object, interface, operation)
|
|
})
|
|
})
|
|
}));
|
|
|
|
let execution = execution.map_err(|_| {
|
|
CallError::from_operation_failure(OperationFailure::internal(
|
|
EffectGuarantee::Unknown,
|
|
"provider execution panicked",
|
|
))
|
|
})?;
|
|
match execution {
|
|
ProviderResult::Found(OperationAttempt::NotStarted(error)) => Err(error),
|
|
ProviderResult::Found(OperationAttempt::Started(Ok(result))) => Ok(result),
|
|
ProviderResult::Found(OperationAttempt::Started(Err(error))) => {
|
|
Err(CallError::from_operation_failure(error))
|
|
}
|
|
ProviderResult::NotFound => Err(CallError::new(
|
|
CallErrorKind::NotFound,
|
|
"target path is not published",
|
|
)),
|
|
ProviderResult::Failure(error) => Err(CallError::from_host_failure(error)),
|
|
}
|
|
}
|
|
|
|
/// Resolves one operation target and decodes its arguments only after all
|
|
/// descriptor-independent canonical preconditions have succeeded.
|
|
///
|
|
/// Transport bindings whose value representation depends on the selected
|
|
/// interface descriptor should use this method instead of resolving a
|
|
/// descriptor before [`ObjectProvider::call_operation_with_context`]. The
|
|
/// callback executes inside the same exact-once provider boundary after
|
|
/// object/interface validators, membership, operation lookup, and required
|
|
/// object-validator checks. A callback error becomes `InvalidArguments` and
|
|
/// typed implementation code is not invoked.
|
|
fn call_operation_with_context_and_decoder<E>(
|
|
&self,
|
|
context: &ExecutionContext,
|
|
target: Target,
|
|
interface: InterfaceTarget,
|
|
operation: String,
|
|
decode_arguments: impl FnOnce(&InterfaceDescriptor) -> Result<BTreeMap<String, Value>, E>,
|
|
) -> Result<CallResult, CallError> {
|
|
wip_protocol::validate_path(&target.path)
|
|
.map_err(|error| CallError::new(CallErrorKind::InvalidRequest, error.to_string()))?;
|
|
let path = target.path.clone();
|
|
let execution = catch_unwind(AssertUnwindSafe(|| {
|
|
self.execute_operation(context, &path, |publication| {
|
|
publication.dispatch_resolved_with_decoder(
|
|
context,
|
|
target,
|
|
interface,
|
|
operation,
|
|
decode_arguments,
|
|
|object, interface, operation| {
|
|
self.authorize(&path, object, interface, operation)
|
|
},
|
|
)
|
|
})
|
|
}));
|
|
|
|
let execution = execution.map_err(|_| {
|
|
CallError::from_operation_failure(OperationFailure::internal(
|
|
EffectGuarantee::Unknown,
|
|
"provider execution panicked",
|
|
))
|
|
})?;
|
|
match execution {
|
|
ProviderResult::Found(OperationAttempt::NotStarted(error)) => Err(error),
|
|
ProviderResult::Found(OperationAttempt::Started(Ok(result))) => Ok(result),
|
|
ProviderResult::Found(OperationAttempt::Started(Err(error))) => {
|
|
Err(CallError::from_operation_failure(error))
|
|
}
|
|
ProviderResult::NotFound => Err(CallError::new(
|
|
CallErrorKind::NotFound,
|
|
"target path is not published",
|
|
)),
|
|
ProviderResult::Failure(error) => Err(CallError::from_host_failure(error)),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Failure to construct an explicit object publication.
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub enum PublicationError {
|
|
/// The same opaque interface reference was exposed more than once.
|
|
DuplicateInterface(InterfaceRef),
|
|
}
|
|
|
|
impl Display for PublicationError {
|
|
fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
|
|
match self {
|
|
Self::DuplicateInterface(reference) => write!(
|
|
formatter,
|
|
"interface `{}` is already exposed by this object",
|
|
reference.as_str()
|
|
),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Error for PublicationError {}
|
|
|
|
/// Stable Host dispatch failure kind.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum CallErrorKind {
|
|
/// The request is structurally invalid.
|
|
InvalidRequest,
|
|
/// The target path is not currently published.
|
|
NotFound,
|
|
/// The selected interface is not exposed by the current object.
|
|
InterfaceMismatch,
|
|
/// The operation is absent from the selected descriptor.
|
|
OperationNotFound,
|
|
/// Supplied named values do not satisfy the operation declaration.
|
|
InvalidArguments,
|
|
/// A bounded cancellation, deadline, or resource limit was exceeded and the
|
|
/// operation effects are guaranteed not committed.
|
|
ResourceLimitExceeded,
|
|
/// The Host failed and operation effects are guaranteed not committed.
|
|
Internal,
|
|
/// The Host cannot guarantee whether operation effects were committed.
|
|
OperationOutcomeUnknown,
|
|
/// This operation requires a validator and the call omitted it.
|
|
ValidatorRequired,
|
|
/// The supplied object validator is stale or belongs to another object.
|
|
ValidatorMismatch,
|
|
/// The selected mutable interface requires its observed validator.
|
|
InterfaceValidatorRequired,
|
|
/// The supplied interface validator does not identify the current descriptor.
|
|
InterfaceValidatorMismatch,
|
|
/// The operation is declared but not authorized for the caller.
|
|
PermissionDenied,
|
|
}
|
|
|
|
/// A dispatch failure with a stable kind and bounded non-normative diagnostic.
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct CallError {
|
|
kind: CallErrorKind,
|
|
message: String,
|
|
}
|
|
|
|
impl CallError {
|
|
fn new(kind: CallErrorKind, message: impl Into<String>) -> Self {
|
|
Self {
|
|
kind,
|
|
message: bounded_message(message.into()),
|
|
}
|
|
}
|
|
|
|
fn from_pre_dispatch(error: DispatchError) -> Self {
|
|
match error {
|
|
DispatchError::UnknownOperation(operation) => Self::new(
|
|
CallErrorKind::OperationNotFound,
|
|
format!("operation `{operation}` is not declared"),
|
|
),
|
|
DispatchError::InvalidArguments(error) => {
|
|
Self::new(CallErrorKind::InvalidArguments, error.to_string())
|
|
}
|
|
DispatchError::Decode(_) => Self::new(
|
|
CallErrorKind::InvalidArguments,
|
|
"operation arguments could not be decoded",
|
|
),
|
|
DispatchError::Host(_)
|
|
| DispatchError::Panic
|
|
| DispatchError::Encode(_)
|
|
| DispatchError::InvalidResult(_) => {
|
|
unreachable!("post-invocation failure used as a pre-dispatch failure")
|
|
}
|
|
}
|
|
}
|
|
|
|
fn from_read_failure(error: HostError) -> Self {
|
|
match error.kind() {
|
|
HostErrorKind::Internal => Self::new(
|
|
CallErrorKind::Internal,
|
|
"provider failed while fetching data",
|
|
),
|
|
HostErrorKind::ResourceLimitExceeded => Self::new(
|
|
CallErrorKind::ResourceLimitExceeded,
|
|
"provider resource limit was exceeded while fetching data",
|
|
),
|
|
}
|
|
}
|
|
|
|
fn from_host_failure(error: HostError) -> Self {
|
|
let kind = classified_host_kind(error.kind(), error.effect_guarantee());
|
|
let message = match error.kind() {
|
|
HostErrorKind::Internal => "provider execution failed",
|
|
HostErrorKind::ResourceLimitExceeded => {
|
|
"provider execution resource limit was exceeded"
|
|
}
|
|
};
|
|
Self::new(kind, message)
|
|
}
|
|
|
|
fn from_operation_failure(error: OperationFailure) -> Self {
|
|
Self::new(
|
|
classified_host_kind(error.kind, error.effect_guarantee),
|
|
error.message,
|
|
)
|
|
}
|
|
|
|
/// Returns the stable failure kind.
|
|
#[must_use]
|
|
pub fn kind(&self) -> CallErrorKind {
|
|
self.kind
|
|
}
|
|
|
|
/// Returns the bounded protocol-safe human-readable diagnostic.
|
|
#[must_use]
|
|
pub fn message(&self) -> &str {
|
|
&self.message
|
|
}
|
|
|
|
/// Maps this Host failure to the canonical protocol error taxonomy.
|
|
#[must_use]
|
|
pub fn protocol_error(&self) -> ProtocolError {
|
|
let code = match self.kind {
|
|
CallErrorKind::InvalidRequest => ProtocolErrorCode::InvalidRequest,
|
|
CallErrorKind::NotFound => ProtocolErrorCode::NotFound,
|
|
CallErrorKind::InterfaceMismatch => ProtocolErrorCode::InterfaceMismatch,
|
|
CallErrorKind::OperationNotFound => ProtocolErrorCode::OperationNotFound,
|
|
CallErrorKind::InvalidArguments => ProtocolErrorCode::InvalidArguments,
|
|
CallErrorKind::ResourceLimitExceeded => ProtocolErrorCode::ResourceLimitExceeded,
|
|
CallErrorKind::Internal => ProtocolErrorCode::Internal,
|
|
CallErrorKind::OperationOutcomeUnknown => ProtocolErrorCode::OperationOutcomeUnknown,
|
|
CallErrorKind::ValidatorRequired => ProtocolErrorCode::ValidatorRequired,
|
|
CallErrorKind::ValidatorMismatch => ProtocolErrorCode::ValidatorMismatch,
|
|
CallErrorKind::InterfaceValidatorRequired => {
|
|
ProtocolErrorCode::InterfaceValidatorRequired
|
|
}
|
|
CallErrorKind::InterfaceValidatorMismatch => {
|
|
ProtocolErrorCode::InterfaceValidatorMismatch
|
|
}
|
|
CallErrorKind::PermissionDenied => ProtocolErrorCode::PermissionDenied,
|
|
};
|
|
ProtocolError {
|
|
code,
|
|
message: self.message.clone(),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn classified_host_kind(kind: HostErrorKind, effect_guarantee: EffectGuarantee) -> CallErrorKind {
|
|
match effect_guarantee {
|
|
EffectGuarantee::Unknown => CallErrorKind::OperationOutcomeUnknown,
|
|
EffectGuarantee::NotCommitted => match kind {
|
|
HostErrorKind::Internal => CallErrorKind::Internal,
|
|
HostErrorKind::ResourceLimitExceeded => CallErrorKind::ResourceLimitExceeded,
|
|
},
|
|
}
|
|
}
|
|
|
|
const MAX_PROTOCOL_MESSAGE_BYTES: usize = 512;
|
|
|
|
fn bounded_message(mut message: String) -> String {
|
|
if message.len() <= MAX_PROTOCOL_MESSAGE_BYTES {
|
|
return message;
|
|
}
|
|
let mut end = MAX_PROTOCOL_MESSAGE_BYTES;
|
|
while !message.is_char_boundary(end) {
|
|
end -= 1;
|
|
}
|
|
message.truncate(end);
|
|
message
|
|
}
|
|
|
|
impl Display for CallError {
|
|
fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
|
|
write!(
|
|
formatter,
|
|
"host dispatch failed ({:?}): {}",
|
|
self.kind, self.message
|
|
)
|
|
}
|
|
}
|
|
|
|
impl Error for CallError {}
|