#![deny(missing_docs)]
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, 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, InterfaceReference, Object,
ProtocolError, ProtocolErrorCode, ValidationError, Value,
};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Validator(Vec<u8>);
impl Validator {
#[must_use]
pub fn new(bytes: impl Into<Vec<u8>>) -> Self {
Self(bytes.into())
}
#[must_use]
pub fn as_bytes(&self) -> &[u8] {
&self.0
}
}
#[derive(Debug, Clone, Default)]
pub struct CancellationToken(Arc<AtomicBool>);
impl CancellationToken {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn cancel(&self) {
self.0.store(true, Ordering::Release);
}
#[must_use]
pub fn is_cancelled(&self) -> bool {
self.0.load(Ordering::Acquire)
}
}
#[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 {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_cancellation(mut self, cancellation: CancellationToken) -> Self {
self.cancellation = cancellation;
self
}
#[must_use]
pub fn with_deadline(mut self, deadline: Instant) -> Self {
self.deadline = Some(deadline);
self
}
#[must_use]
pub fn with_resource_budget(mut self, resource_budget: u64) -> Self {
self.resource_budget = Some(resource_budget);
self
}
#[must_use]
pub fn cancellation(&self) -> &CancellationToken {
&self.cancellation
}
#[must_use]
pub fn deadline(&self) -> Option<Instant> {
self.deadline
}
#[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(())
}
}
#[derive(Debug, Clone)]
pub struct Scope {
path: String,
lifetime: Arc<AtomicBool>,
}
impl Scope {
pub fn new(path: impl Into<String>) -> Result<Self, ValidationError> {
let path = path.into();
wip_protocol::validate_path(&path)?;
Ok(Self {
path,
lifetime: Arc::new(AtomicBool::new(true)),
})
}
#[must_use]
pub fn path(&self) -> &str {
&self.path
}
fn same_lifetime(&self, other: &Self) -> bool {
self.path == other.path && Arc::ptr_eq(&self.lifetime, &other.lifetime)
}
fn active(&self) -> bool {
self.lifetime.load(Ordering::Acquire)
}
}
#[derive(Debug, Clone)]
pub struct ScopeObservation {
scope: Scope,
reference: Option<String>,
}
impl ScopeObservation {
#[must_use]
pub fn new(scope: &Scope, object: &impl WipObject) -> Self {
Self {
scope: scope.clone(),
reference: object.reference().map(str::to_owned),
}
}
#[must_use]
pub fn reference(&self) -> Option<&str> {
self.reference.as_deref()
}
#[must_use]
pub fn scope(&self) -> &Scope {
&self.scope
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct InterfaceRef(InterfaceReference);
impl InterfaceRef {
#[must_use]
pub fn as_str(&self) -> &str {
&self.0.name
}
#[must_use]
pub fn protocol_reference(&self) -> InterfaceReference {
self.0.clone()
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct InterfaceObservation {
pub reference: InterfaceRef,
pub descriptor: InterfaceDescriptor,
pub scope_ref: Option<String>,
pub validator: Option<Validator>,
}
impl InterfaceObservation {
#[must_use]
pub fn project_operations(mut self, visible: impl Fn(&str) -> bool) -> Self {
restrict_descriptor(&mut self.descriptor, visible);
self
}
pub fn protocol_response(&self) -> Result<FetchInterfaceResponse, ProtocolError> {
let response = FetchInterfaceResponse {
interface: self.reference.protocol_reference(),
descriptor: self.descriptor.clone(),
scope_ref: self.scope_ref.clone(),
validator: self.validator.as_ref().map(|validator| validator.0.clone()),
};
response
.validate_for(&FetchInterfaceRequest {
interface: self.reference.protocol_reference(),
})
.map_err(|_| ProtocolError {
code: ProtocolErrorCode::Internal,
message: "Host produced an invalid interface response".into(),
})?;
Ok(response)
}
}
#[derive(Debug)]
struct InterfaceArtifact {
scope: Scope,
reference: InterfaceRef,
descriptor: InterfaceDescriptor,
validator: Option<Validator>,
}
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> {
#[must_use]
pub fn reference(&self) -> &InterfaceRef {
&self.artifact.reference
}
}
#[derive(Debug, Default)]
pub struct InterfaceRegistry {
artifacts: HashMap<InterfaceRef, Arc<InterfaceArtifact>>,
}
impl InterfaceRegistry {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn register<D>(
&mut self,
scope: &Scope,
name: impl Into<String>,
_definition: D,
validator: Option<Validator>,
) -> Result<RegisteredInterface<D>, RegistrationError>
where
D: InterfaceDefinition + 'static,
{
let reference = InterfaceRef(InterfaceReference {
scope: scope.path.clone(),
name: name.into(),
});
reference
.0
.validate()
.map_err(RegistrationError::InvalidReference)?;
if !scope.active() {
return Err(RegistrationError::RetiredScope);
}
if self.artifacts.contains_key(&reference) {
return Err(RegistrationError::AlreadyRegistered);
}
let descriptor = D::descriptor();
descriptor
.validate()
.map_err(RegistrationError::InvalidDescriptor)?;
let artifact = Arc::new(InterfaceArtifact {
scope: scope.clone(),
reference: reference.clone(),
descriptor,
validator,
});
self.artifacts.insert(reference, Arc::clone(&artifact));
Ok(RegisteredInterface {
artifact,
definition: PhantomData,
})
}
pub fn remove_scope(&mut self, scope: &Scope) {
scope.lifetime.store(false, Ordering::Release);
self.artifacts
.retain(|_, artifact| !artifact.scope.same_lifetime(scope));
}
#[must_use]
pub fn resolve(&self, reference: &InterfaceReference) -> Option<InterfaceRef> {
let reference = InterfaceRef(reference.clone());
self.artifacts.contains_key(&reference).then_some(reference)
}
pub fn scopes(&self) -> impl Iterator<Item = &Scope> {
self.artifacts.values().map(|artifact| &artifact.scope)
}
pub fn fetch_interface(
&self,
reference: &InterfaceReference,
current: &ScopeObservation,
) -> Result<InterfaceObservation, ProtocolError> {
reference.validate().map_err(|error| ProtocolError {
code: ProtocolErrorCode::InvalidRequest,
message: error.to_string(),
})?;
self.artifacts
.get(&InterfaceRef(reference.clone()))
.filter(|artifact| {
artifact.scope.active() && artifact.scope.same_lifetime(¤t.scope)
})
.map(|artifact| InterfaceObservation {
reference: artifact.reference.clone(),
descriptor: artifact.descriptor.clone(),
scope_ref: current.reference.clone(),
validator: artifact.validator.clone(),
})
.ok_or_else(interface_not_found)
}
}
fn interface_not_found() -> ProtocolError {
ProtocolError {
code: ProtocolErrorCode::InterfaceNotFound,
message: "interface is not currently published".into(),
}
}
#[derive(Debug)]
pub enum RegistrationError {
AlreadyRegistered,
InvalidDescriptor(ValidationError),
InvalidReference(ValidationError),
RetiredScope,
}
impl Display for RegistrationError {
fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::AlreadyRegistered => {
formatter.write_str("interface scope/name already registered")
}
Self::InvalidDescriptor(error) => {
write!(formatter, "invalid interface descriptor: {error}")
}
Self::InvalidReference(error) => {
write!(formatter, "invalid interface reference: {error}")
}
Self::RetiredScope => formatter.write_str("scope publication lifetime has ended"),
}
}
}
impl Error for RegistrationError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::InvalidDescriptor(error) | Self::InvalidReference(error) => Some(error),
Self::AlreadyRegistered | Self::RetiredScope => None,
}
}
}
pub trait WipObject {
fn name(&self) -> &str;
fn description(&self) -> Option<&str> {
None
}
fn reference(&self) -> Option<&str> {
None
}
fn validator(&self) -> Option<&Validator> {
None
}
fn requires_validator(&self, _interface: &InterfaceRef, _operation: &str) -> bool {
false
}
}
fn restrict_descriptor(descriptor: &mut InterfaceDescriptor, visible: impl Fn(&str) -> bool) {
let count = descriptor.operations.len();
descriptor
.operations
.retain(|operation| visible(&operation.name));
if descriptor.operations.len() == count {
return;
}
descriptor.documentation = None;
let mut used = HashSet::new();
fn references(expression: &wip_protocol::TypeExpr, used: &mut HashSet<String>) {
use wip_protocol::TypeExpr;
match expression {
TypeExpr::Named { name } => {
used.insert(name.clone());
}
TypeExpr::Record { fields } => {
for field in fields {
references(&field.r#type, used);
}
}
TypeExpr::List { items } => references(items, used),
TypeExpr::Union { cases } => {
for case in cases {
if let Some(payload) = &case.payload {
references(payload, used);
}
}
}
_ => {}
}
}
for operation in &descriptor.operations {
for parameter in &operation.parameters {
references(¶meter.r#type, &mut used);
}
references(&operation.returns.r#type, &mut used);
}
loop {
let before = used.len();
for declaration in &descriptor.types {
if used.contains(&declaration.name) {
references(&declaration.definition, &mut used);
}
}
if before == used.len() {
break;
}
}
descriptor
.types
.retain(|declaration| used.contains(&declaration.name));
}
type DispatchFn<T> = fn(&T, &str, BTreeMap<String, Value>) -> Result<Value, DispatchError>;
struct BoundInterface<T> {
artifact: Arc<InterfaceArtifact>,
dispatch: DispatchFn<T>,
}
pub struct PublishedObject<T: WipObject> {
implementation: T,
interfaces: Vec<BoundInterface<T>>,
scopes: Vec<ScopeObservation>,
}
impl<T: WipObject> PublishedObject<T> {
#[must_use]
pub fn new(implementation: T) -> Self {
Self {
implementation,
interfaces: Vec::new(),
scopes: Vec::new(),
}
}
#[must_use]
pub fn bind_scope(mut self, current: ScopeObservation) -> Self {
self.scopes
.retain(|old| old.scope.path != current.scope.path);
self.scopes.push(current);
self
}
fn current_scope(&self, scope: &Scope) -> Option<&ScopeObservation> {
self.scopes
.iter()
.find(|current| scope.active() && scope.same_lifetime(¤t.scope))
}
fn scopes_valid_at(&self, path: &str) -> bool {
self.interfaces.iter().all(|bound| {
bound.artifact.reference.0.validate_for_path(path).is_ok()
&& self
.current_scope(&bound.artifact.scope)
.is_some_and(|current| {
current.scope.path != path
|| current.reference() == self.implementation.reference()
})
})
}
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)
}
pub fn project(
mut self,
interfaces: &[InterfaceRef],
visible: impl Fn(&InterfaceRef, &str) -> bool,
) -> Result<Self, PublicationError> {
let mut projected = Vec::with_capacity(interfaces.len());
for reference in interfaces {
if projected
.iter()
.any(|bound: &BoundInterface<T>| &bound.artifact.reference == reference)
{
return Err(PublicationError::DuplicateInterface(reference.clone()));
}
let Some(index) = self
.interfaces
.iter()
.position(|bound| &bound.artifact.reference == reference)
else {
return Err(PublicationError::UnexposedInterface(reference.clone()));
};
let mut bound = self.interfaces.remove(index);
let mut descriptor = bound.artifact.descriptor.clone();
restrict_descriptor(&mut descriptor, |operation| visible(reference, operation));
bound.artifact = Arc::new(InterfaceArtifact {
scope: bound.artifact.scope.clone(),
reference: reference.clone(),
descriptor,
validator: bound.artifact.validator.clone(),
});
projected.push(bound);
}
self.interfaces = projected;
Ok(self)
}
#[must_use]
pub fn implementation(&self) -> &T {
&self.implementation
}
#[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.protocol_reference())
.collect(),
r#ref: self.implementation.reference().map(str::to_owned),
validator: self
.implementation
.validator()
.map(|validator| validator.0.clone()),
}
}
pub fn protocol_object_at(&self, path: &str) -> Result<Object, CallError> {
wip_protocol::validate_path(path)
.map_err(|error| CallError::new(CallErrorKind::InvalidRequest, error.to_string()))?;
if !self.scopes_valid_at(path) {
return Err(CallError::new(
CallErrorKind::Internal,
"Host published interfaces without current compatible scope Objects",
));
}
let object = self.protocol_object();
object.validate_at_path(path).map_err(|_| {
CallError::new(
CallErrorKind::Internal,
"Host produced an invalid object response",
)
})?;
Ok(object)
}
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",
);
}
if interface.reference.validate_for_path(&target.path).is_err() {
return pre_dispatch_failure(
CallErrorKind::InterfaceMismatch,
"interface scope is not an ancestor of the target path",
);
}
let Some(bound) = self
.interfaces
.iter()
.find(|bound| bound.artifact.reference.protocol_reference() == interface.reference)
else {
return pre_dispatch_failure(
CallErrorKind::InterfaceMismatch,
"interface is not a member of the current object publication",
);
};
let Some(current_scope) = self.current_scope(&bound.artifact.scope) else {
return pre_dispatch_failure(
CallErrorKind::InterfaceMismatch,
"interface scope Object is not currently associated with this publication",
);
};
if current_scope.scope.path == target.path
&& current_scope.reference() != self.implementation.reference()
{
return pre_dispatch_failure(
CallErrorKind::Internal,
"provider produced inconsistent self-scope metadata",
);
}
if interface
.scope_ref
.as_deref()
.is_some_and(|supplied| current_scope.reference() != Some(supplied))
{
return pre_dispatch_failure(
CallErrorKind::InterfaceMismatch,
"scope Object ref does not match the current association",
);
}
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()),
}))
}
#[must_use]
pub fn into_inner(self) -> T {
self.implementation
}
}
#[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")
}
}
}
#[must_use]
pub fn kind(&self) -> HostErrorKind {
self.kind
}
#[must_use]
pub fn effect_guarantee(&self) -> EffectGuarantee {
self.effect_guarantee
}
#[must_use]
pub fn confirm_rollback(mut self) -> Self {
self.effect_guarantee = EffectGuarantee::NotCommitted;
self
}
}
#[derive(Debug)]
pub enum OperationAttempt {
NotStarted(CallError),
Started(Result<CallResult, OperationFailure>),
}
impl OperationAttempt {
#[must_use]
pub fn reject(kind: CallErrorKind) -> Self {
Self::NotStarted(CallError::new(kind, "provider precondition failed"))
}
#[must_use]
pub fn confirm_rollback(self) -> Self {
match self {
Self::Started(Err(error)) => Self::Started(Err(error.confirm_rollback())),
other => other,
}
}
}
#[derive(Debug)]
pub enum ProviderResult<T> {
Found(T),
NotFound,
Failure(HostError),
}
pub trait ObjectProvider {
type Object: WipObject;
fn with_object<R>(
&self,
context: &ExecutionContext,
path: &str,
operation: impl FnOnce(&PublishedObject<Self::Object>) -> R,
) -> ProviderResult<R>;
fn execute_operation(
&self,
context: &ExecutionContext,
path: &str,
operation: impl FnOnce(&PublishedObject<Self::Object>) -> OperationAttempt,
) -> ProviderResult<OperationAttempt> {
self.with_object(context, path, operation)
}
fn authorize(
&self,
_path: &str,
_object: &Self::Object,
_interface: &InterfaceRef,
_operation: &str,
) -> bool {
true
}
fn observe_object(&self, path: &str) -> Result<Object, CallError> {
self.observe_object_with_context(&ExecutionContext::default(), path)
}
fn observe_object_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, |publication| {
publication.protocol_object_at(path)
})
}))
.map_err(|_| {
CallError::new(
CallErrorKind::Internal,
"provider panicked while observing 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)),
};
Ok(object)
}
fn fetch_interface(
&self,
registry: &InterfaceRegistry,
reference: &InterfaceReference,
) -> Result<InterfaceObservation, ProtocolError> {
reference.validate().map_err(|error| ProtocolError {
code: ProtocolErrorCode::InvalidRequest,
message: error.to_string(),
})?;
let observed = catch_unwind(AssertUnwindSafe(|| {
self.with_object(
&ExecutionContext::default(),
&reference.scope,
|publication| {
publication
.scopes
.iter()
.find(|current| current.scope.path == reference.scope)
.ok_or_else(interface_not_found)
.and_then(|current| {
if current.reference() != publication.implementation.reference() {
return Err(ProtocolError {
code: ProtocolErrorCode::Internal,
message: "provider produced inconsistent scope Object metadata"
.into(),
});
}
registry.fetch_interface(reference, current)
})
},
)
}))
.map_err(|_| ProtocolError {
code: ProtocolErrorCode::Internal,
message: "provider panicked while observing an interface".into(),
})?;
match observed {
ProviderResult::Found(observation) => observation,
ProviderResult::NotFound => Err(interface_not_found()),
ProviderResult::Failure(error) => {
Err(CallError::from_read_failure(error).protocol_error())
}
}
}
fn call_operation(&self, request: CallRequest) -> Result<CallResult, CallError> {
self.call_operation_with_context(&ExecutionContext::default(), request)
}
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()))?;
request
.interface
.reference
.validate()
.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)),
}
}
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()))?;
interface
.reference
.validate()
.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)),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PublicationError {
DuplicateInterface(InterfaceRef),
UnexposedInterface(InterfaceRef),
}
impl Display for PublicationError {
fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::UnexposedInterface(reference) => write!(
formatter,
"interface `{}` has no implementation",
reference.as_str()
),
Self::DuplicateInterface(reference) => write!(
formatter,
"interface `{}` is already exposed by this object",
reference.as_str()
),
}
}
}
impl Error for PublicationError {}
#[cfg(test)]
mod projection_tests {
use super::*;
use wip_protocol::{
Documentation, OperationDeclaration, ReturnDeclaration, TypeDeclaration, TypeExpr,
};
#[test]
fn operation_views_prune_hidden_types_and_documentation_without_mutating_registry() {
let named = |name: &str| TypeExpr::Named { name: name.into() };
let operation = |name: &str, returns| OperationDeclaration {
name: name.into(),
documentation: None,
parameters: Vec::new(),
returns: ReturnDeclaration {
documentation: None,
r#type: returns,
},
};
let declaration = |name: &str, definition| TypeDeclaration {
name: name.into(),
documentation: None,
definition,
};
let descriptor = InterfaceDescriptor {
format: "wip/1".into(),
documentation: Some(Documentation {
summary: "contains hidden operation details".into(),
details: None,
}),
operations: vec![
operation("visible", named("Public")),
operation("secret", named("Secret")),
],
types: vec![
declaration(
"Public",
TypeExpr::List {
items: Box::new(named("Shared")),
},
),
declaration("Shared", TypeExpr::String),
declaration("Secret", TypeExpr::Bytes),
],
};
let mut projected = descriptor.clone();
restrict_descriptor(&mut projected, |operation| operation == "visible");
assert_eq!(projected.operations.len(), 1);
assert!(projected.documentation.is_none());
assert_eq!(
projected
.types
.iter()
.map(|declaration| declaration.name.as_str())
.collect::<Vec<_>>(),
vec!["Public", "Shared"]
);
assert_eq!(descriptor.types.len(), 3);
restrict_descriptor(&mut projected, |_| false);
assert!(projected.types.is_empty());
assert!(projected.operations.is_empty());
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CallErrorKind {
InvalidRequest,
NotFound,
InterfaceMismatch,
OperationNotFound,
InvalidArguments,
ResourceLimitExceeded,
Internal,
OperationOutcomeUnknown,
ValidatorRequired,
ValidatorMismatch,
InterfaceValidatorRequired,
InterfaceValidatorMismatch,
PermissionDenied,
}
#[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,
)
}
#[must_use]
pub fn kind(&self) -> CallErrorKind {
self.kind
}
#[must_use]
pub fn message(&self) -> &str {
&self.message
}
#[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 {}