Compare commits

..
16 changed files with 2782 additions and 1131 deletions
Generated
+9
View File
@@ -238,6 +238,15 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81"
[[package]]
name = "wip-host"
version = "0.1.0"
dependencies = [
"trybuild",
"wip-interface",
"wip-protocol",
]
[[package]] [[package]]
name = "wip-interface" name = "wip-interface"
version = "0.1.0" version = "0.1.0"
+1
View File
@@ -1,6 +1,7 @@
[workspace] [workspace]
members = [ members = [
".", ".",
"crates/wip-host",
"crates/wip-interface", "crates/wip-interface",
"crates/wip-interface-macros", "crates/wip-interface-macros",
"crates/wip-protocol", "crates/wip-protocol",
+32
View File
@@ -65,3 +65,35 @@ The supported interface slice is deliberately non-generic: public traits,
`&self`, named parameters, and `OperationResult<T>`. Generic traits or methods, `&self`, named parameters, and `OperationResult<T>`. Generic traits or methods,
associated items, custom lifetimes, inheritance, default method bodies, and associated items, custom lifetimes, inheritance, default method bodies, and
other receiver forms are rejected at their source spans. other receiver forms are rejected at their source spans.
## Host publication and dispatch
Applications use `wip-host` to register generated definitions and explicitly
choose the ordered interface set for each object publication:
```rust,ignore
let mut interfaces = wip_host::InterfaceRegistry::new();
let messenger = interfaces.register(MessengerDefinition, None)?;
let published = wip_host::PublishedObject::new(request_scoped_object)
.expose(&messenger)?;
let protocol_object = published.protocol_object();
```
`expose` compiles only when the object implements the generated definition's
interface trait. Implementing a trait does not expose it automatically, empty
interface sets are valid, insertion order is preserved, and duplicate
references are rejected. Registry references are opaque; descriptor names and
Rust type names are not used as wire identity.
Dispatch is qualified by interface reference and operation name. The host checks
current object identity and validator policy, current interface membership,
interface validator, descriptor operation, arguments, and result while retaining
the selected object and registered artifact. Host-private fields and backend
handles are never projected by `WipObject`.
Providers remain responsible for backend atomicity. Enter a transaction, lock,
stable-handle scope, or conditional-update primitive before constructing and
dispatching the request-scoped `PublishedObject`, and retain it until dispatch
returns. The runtime never validates one object and then resolves a second
snapshot, but it cannot make an arbitrary external backend atomic by itself.
+13
View File
@@ -0,0 +1,13 @@
[package]
name = "wip-host"
version = "0.1.0"
edition = "2024"
description = "Host-side object publication and operation dispatch for WIP"
license = "MIT OR Apache-2.0"
[dependencies]
wip-interface = { path = "../wip-interface" }
wip-protocol = { path = "../wip-protocol" }
[dev-dependencies]
trybuild = "1"
+630
View File
@@ -0,0 +1,630 @@
//! 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 makes the same descriptor
//! observation available to clients and dispatch. [`PublishedObject::expose`]
//! explicitly combines a request-scoped Rust object with a registered interface;
//! its [`InterfaceImplementation`] bound proves at compile time that the object
//! implements that interface. Implementing a Rust trait alone never publishes it,
//! and a publication with no interfaces is valid.
//!
//! [`PublishedObject::dispatch`] selects by `(InterfaceRef, operation name)`,
//! checks object and interface validators, current membership, the frozen
//! descriptor, and then invokes the matching generated typed dispatcher on the
//! same object instance. Arguments and results cross the protocol [`Value`]
//! boundary only through generated validation and codecs. Rust fields that are
//! not returned by [`WipObject`] are host-private and are never serialized by
//! this crate.
//!
//! The call method is deliberately an execution boundary rather than a global
//! object registry: a provider may resolve and publish an object per request and
//! drop it afterward. The provider must enter any backend-specific transaction,
//! lock, stable-handle scope, or conditional-update primitive *before* creating
//! or dispatching the publication and retain it until dispatch returns. This
//! crate keeps validator checks, descriptor and implementation selection, and
//! invocation in one borrow of that publication; it does not claim that an
//! arbitrary backend stays stable, and it never reopens a path or resolves a
//! second implementation snapshot after validation.
#![deny(missing_docs)]
use std::any::TypeId;
use std::collections::{HashMap, HashSet};
use std::error::Error;
use std::fmt::{self, Display, Formatter};
use std::marker::PhantomData;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use wip_interface::DispatchError;
pub use wip_interface::{InterfaceDefinition, InterfaceImplementation};
pub use wip_protocol;
use wip_protocol::{
InterfaceDescriptor, NamedValue, Object, ObjectId, OperationCall, OperationTarget,
ProtocolError, ProtocolErrorCategory, 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
}
}
/// 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
}
}
/// An immutable observation returned for an opaque interface reference.
#[derive(Debug, Clone, PartialEq)]
pub struct InterfaceObservation {
/// Opaque reference selected by published objects and operation calls.
pub reference: InterfaceRef,
/// Optional validator. Its presence makes it a required call precondition.
pub validator: Option<Validator>,
/// Validated descriptor frozen when the interface was registered.
pub descriptor: InterfaceDescriptor,
}
#[derive(Debug)]
struct InterfaceArtifact {
reference: InterfaceRef,
validator: Option<Validator>,
descriptor: InterfaceDescriptor,
}
/// 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.
///
/// This registry stores interface definitions, not object instances. Generated
/// definitions are registered explicitly and receive process-local opaque
/// 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 instead of silently changing
/// the descriptor 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.
///
/// `validator` should be present when the host intends the reference to
/// represent a versioned descriptor. Its presence makes the same value a
/// mandatory precondition for dispatch. Registration finalizes the
/// descriptor's protocol `name` to the assigned opaque reference so it
/// corresponds directly to [`Object::interfaces`].
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 mut descriptor = D::descriptor();
descriptor
.validate()
.map_err(RegistrationError::InvalidDescriptor)?;
let reference = InterfaceRef(format!(
"wip-interface:{}:{}",
self.namespace, self.next_reference
));
// The protocol model currently carries the interface identifier in the
// descriptor's `name` field. Finalize that identity to the same opaque
// reference placed on published objects; the generated Rust trait name
// is registration-local metadata, not wire identity.
descriptor.name = reference.0.clone();
self.next_reference = self
.next_reference
.checked_add(1)
.ok_or(RegistrationError::ReferenceSpaceExhausted)?;
let artifact = Arc::new(InterfaceArtifact {
reference: reference.clone(),
validator,
descriptor,
});
self.definitions.insert(definition);
self.artifacts.insert(reference, Arc::clone(&artifact));
Ok(RegisteredInterface {
artifact,
definition: PhantomData,
})
}
/// Observes the descriptor and optional validator for an opaque reference.
#[must_use]
pub fn observe(&self, reference: &InterfaceRef) -> Option<InterfaceObservation> {
self.artifacts
.get(reference)
.map(|artifact| InterfaceObservation {
reference: artifact.reference.clone(),
validator: artifact.validator.clone(),
descriptor: artifact.descriptor.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.
/// Backend handles, filesystem paths, credentials, and other private fields on
/// the implementing type are neither inspected nor serialized.
pub trait WipObject {
/// Returns the current logical identity of this object.
fn id(&self) -> ObjectId;
/// Returns the current opaque object validator, if any.
fn validator(&self) -> Option<&Validator> {
None
}
/// Whether a call must include the current object validator.
fn requires_validator(&self) -> bool {
false
}
}
type DispatchFn<T> = fn(&T, &str, Vec<NamedValue>) -> Result<Option<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 or pre-populated object registry is required.
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 the protocol object metadata with interface insertion order intact.
#[must_use]
pub fn protocol_object(&self) -> Object {
let object = Object {
id: self.implementation.id(),
interfaces: self
.interfaces
.iter()
.map(|bound| bound.artifact.reference.0.clone())
.collect(),
};
debug_assert!(object.validate().is_ok());
object
}
/// Dispatches one qualified operation against this current publication.
///
/// Preconditions are checked before generated operation code runs. The
/// selected artifact and function pointer are borrowed once and are not
/// resolved again after validator checks.
pub fn dispatch(&self, request: CallRequest) -> Result<CallResult, CallError> {
if request.object != self.implementation.id() {
return Err(CallError::new(
CallErrorKind::ObjectNotFound,
"call target does not identify this published object",
));
}
let current_object_validator = self.implementation.validator();
match request.object_validator.as_ref() {
Some(supplied) if Some(supplied) != current_object_validator => {
return Err(CallError::new(
CallErrorKind::ValidatorMismatch,
"object validator does not match current object state",
));
}
None if self.implementation.requires_validator() => {
return Err(CallError::new(
CallErrorKind::ValidatorRequired,
"this object requires a validator",
));
}
_ => {}
}
let Some(bound) = self
.interfaces
.iter()
.find(|bound| bound.artifact.reference == request.interface.reference)
else {
return Err(CallError::new(
CallErrorKind::InterfaceMismatch,
"interface is not a member of the current object publication",
));
};
match (
bound.artifact.validator.as_ref(),
request.interface.validator.as_ref(),
) {
(Some(_), None) => {
return Err(CallError::new(
CallErrorKind::InterfaceValidatorRequired,
"registered interface requires a validator",
));
}
(expected, Some(actual)) if expected != Some(actual) => {
return Err(CallError::new(
CallErrorKind::InterfaceValidatorMismatch,
"interface validator does not match the registered descriptor",
));
}
_ => {}
}
if !bound
.artifact
.descriptor
.operations
.iter()
.any(|operation| operation.name == request.operation)
{
return Err(CallError::new(
CallErrorKind::OperationNotFound,
format!("operation `{}` is not declared", request.operation),
));
}
bound
.artifact
.descriptor
.validate_call(&OperationCall {
target: OperationTarget {
object: request.object.clone(),
interface: bound.artifact.descriptor.name.clone(),
operation: request.operation.clone(),
},
arguments: request.arguments.clone(),
})
.map_err(|error| CallError::new(CallErrorKind::InvalidArguments, error.to_string()))?;
let value = (bound.dispatch)(&self.implementation, &request.operation, request.arguments)
.map_err(CallError::from_dispatch)?;
bound
.artifact
.descriptor
.validate_return(&request.operation, value.as_ref())
.map_err(|error| CallError::new(CallErrorKind::InvalidResult, error.to_string()))?;
Ok(CallResult {
value,
validator: self.implementation.validator().cloned(),
})
}
/// Consumes the publication and returns its host-private implementation.
#[must_use]
pub fn into_inner(self) -> T {
self.implementation
}
}
/// 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 {}
/// Interface portion of a qualified operation target.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InterfaceTarget {
/// Opaque reference selected from the current object's interface set.
pub reference: InterfaceRef,
/// Validator copied from the descriptor observation, when one was present.
pub validator: Option<Validator>,
}
/// One transport-independent request to a published object operation.
#[derive(Debug, Clone, PartialEq)]
pub struct CallRequest {
/// Current logical object identity.
pub object: ObjectId,
/// Optional current object-state precondition.
pub object_validator: Option<Validator>,
/// Qualified interface reference and descriptor precondition.
pub interface: InterfaceTarget,
/// Operation name within the selected interface descriptor.
pub operation: String,
/// Named protocol arguments.
pub arguments: Vec<NamedValue>,
}
/// Successful host dispatch result.
#[derive(Debug, Clone, PartialEq)]
pub struct CallResult {
/// Encoded application value, or `None` for a no-return operation.
pub value: Option<Value>,
/// Object validator observed after the implementation returned.
pub validator: Option<Validator>,
}
/// Stable host dispatch failure kind.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CallErrorKind {
/// The call names a different or unavailable object.
ObjectNotFound,
/// 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,
/// The encoded implementation result violates its declaration.
InvalidResult,
/// This object 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 typed implementation reported a host/backend failure.
Implementation,
}
/// A dispatch failure with a stable kind and 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: message.into(),
}
}
fn from_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(error) => {
Self::new(CallErrorKind::InvalidArguments, error.to_string())
}
DispatchError::Host(error) => {
Self::new(CallErrorKind::Implementation, error.to_string())
}
DispatchError::Encode(error) => {
Self::new(CallErrorKind::InvalidResult, error.to_string())
}
DispatchError::InvalidResult(error) => {
Self::new(CallErrorKind::InvalidResult, error.to_string())
}
}
}
/// Returns the stable failure kind.
#[must_use]
pub fn kind(&self) -> CallErrorKind {
self.kind
}
/// Returns the human-readable diagnostic.
#[must_use]
pub fn message(&self) -> &str {
&self.message
}
/// Maps this host failure to the existing protocol error taxonomy.
///
/// Validator details remain available through [`CallError::kind`], while
/// the current protocol model represents conditional-request failures as
/// `InvalidRequest`. Result/codec and implementation failures are internal,
/// because exposing malformed host output as an application failure would
/// be unsafe.
#[must_use]
pub fn protocol_error(&self) -> ProtocolError {
let category = match self.kind {
CallErrorKind::ObjectNotFound => ProtocolErrorCategory::ObjectNotFound,
CallErrorKind::InterfaceMismatch => ProtocolErrorCategory::InterfaceNotFound,
CallErrorKind::OperationNotFound => ProtocolErrorCategory::OperationNotFound,
CallErrorKind::InvalidArguments => ProtocolErrorCategory::InvalidArguments,
CallErrorKind::ValidatorRequired
| CallErrorKind::ValidatorMismatch
| CallErrorKind::InterfaceValidatorRequired
| CallErrorKind::InterfaceValidatorMismatch => ProtocolErrorCategory::InvalidRequest,
CallErrorKind::InvalidResult | CallErrorKind::Implementation => {
ProtocolErrorCategory::Internal
}
};
ProtocolError {
category,
message: self.message.clone(),
}
}
}
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 {}
+5
View File
@@ -0,0 +1,5 @@
#[test]
fn rejects_exposing_an_unimplemented_interface() {
let tests = trybuild::TestCases::new();
tests.compile_fail("tests/ui/unimplemented_interface.rs");
}
+497
View File
@@ -0,0 +1,497 @@
use std::cell::{Cell, RefCell};
use wip_host::{
CallErrorKind, CallRequest, InterfaceRegistry, InterfaceTarget, PublishedObject, Validator,
WipObject,
};
use wip_interface::{
CodecError, HostError, NamedValue, OperationResult, TypeDeclaration, TypeExpr, Value, WipType,
interface,
};
use wip_protocol::{ObjectId, ProtocolErrorCategory};
#[interface]
pub trait Reader {
/// Returns the reader-qualified label.
fn label(&self) -> OperationResult<String>;
/// Adds two integers.
fn add(&self, left: i64, right: i64) -> OperationResult<i64>;
}
#[interface]
pub trait Writer {
/// Returns the writer-qualified label.
fn label(&self) -> OperationResult<String>;
}
#[interface]
pub trait Hidden {
/// Returns a hidden value.
fn hidden(&self) -> OperationResult<String>;
}
#[interface]
pub trait Broken {
/// Returns a deliberately invalid encoded value.
fn broken(&self) -> OperationResult<BadValue>;
}
#[derive(Debug)]
pub struct BadValue;
impl WipType for BadValue {
fn type_expr() -> TypeExpr {
TypeExpr::Integer
}
fn collect_declarations(_: &mut Vec<TypeDeclaration>) {}
fn encode(self) -> Result<Value, CodecError> {
Ok(Value::String("not an integer".into()))
}
fn decode(_: Value) -> Result<Self, CodecError> {
Ok(Self)
}
}
struct Document {
id: String,
validator: Option<Validator>,
require_validator: bool,
calls: Cell<usize>,
private_backend_handle: RefCell<String>,
}
impl Document {
fn new(id: &str) -> Self {
Self {
id: id.into(),
validator: None,
require_validator: false,
calls: Cell::new(0),
private_backend_handle: RefCell::new("/private/backend/path".into()),
}
}
fn strict(id: &str, validator: &[u8]) -> Self {
Self {
id: id.into(),
validator: Some(Validator::new(validator)),
require_validator: true,
calls: Cell::new(0),
private_backend_handle: RefCell::new("secret-handle".into()),
}
}
fn record_call(&self) {
self.calls.set(self.calls.get() + 1);
}
}
impl WipObject for Document {
fn id(&self) -> ObjectId {
ObjectId(self.id.clone())
}
fn validator(&self) -> Option<&Validator> {
self.validator.as_ref()
}
fn requires_validator(&self) -> bool {
self.require_validator
}
}
impl Reader for Document {
fn label(&self) -> OperationResult<String> {
self.record_call();
Ok("reader".into())
}
fn add(&self, left: i64, right: i64) -> OperationResult<i64> {
self.record_call();
Ok(left + right)
}
}
impl Writer for Document {
fn label(&self) -> OperationResult<String> {
self.record_call();
Ok("writer".into())
}
}
impl Hidden for Document {
fn hidden(&self) -> OperationResult<String> {
self.record_call();
Ok("hidden".into())
}
}
impl Broken for Document {
fn broken(&self) -> OperationResult<BadValue> {
self.record_call();
Ok(BadValue)
}
}
fn request(
object: &str,
object_validator: Option<&[u8]>,
reference: &wip_host::InterfaceRef,
interface_validator: Option<&[u8]>,
operation: &str,
arguments: Vec<NamedValue>,
) -> CallRequest {
CallRequest {
object: ObjectId(object.into()),
object_validator: object_validator.map(Validator::new),
interface: InterfaceTarget {
reference: reference.clone(),
validator: interface_validator.map(Validator::new),
},
operation: operation.into(),
arguments,
}
}
#[test]
fn registry_exposes_opaque_validated_descriptor_observations() {
let mut registry = InterfaceRegistry::new();
let reader = registry.register(ReaderDefinition, None).unwrap();
assert_ne!(reader.reference().as_str(), "Reader");
let observation = registry.observe(reader.reference()).unwrap();
assert_eq!(observation.reference, *reader.reference());
assert_eq!(observation.descriptor.name, reader.reference().as_str());
assert_eq!(observation.validator, None);
observation.descriptor.validate().unwrap();
assert!(registry.register(ReaderDefinition, None).is_err());
let mut other_registry = InterfaceRegistry::new();
let other_reader = other_registry.register(ReaderDefinition, None).unwrap();
assert_ne!(reader.reference(), other_reader.reference());
}
#[test]
fn publication_is_explicit_ordered_qualified_and_request_scoped() {
let mut registry = InterfaceRegistry::new();
let reader = registry.register(ReaderDefinition, None).unwrap();
let writer = registry.register(WriterDefinition, None).unwrap();
let _hidden = registry.register(HiddenDefinition, None).unwrap();
let publication = PublishedObject::new(Document::new("document-1"))
.expose(&writer)
.unwrap()
.expose(&reader)
.unwrap();
let object = publication.protocol_object();
assert_eq!(
object.interfaces,
vec![
writer.reference().as_str().to_owned(),
reader.reference().as_str().to_owned()
]
);
wip_protocol::InterfaceObservation {
object: object.clone(),
descriptors: vec![
registry.observe(writer.reference()).unwrap().descriptor,
registry.observe(reader.reference()).unwrap().descriptor,
],
}
.validate()
.unwrap();
assert!(!format!("{object:?}").contains("private/backend/path"));
let writer_result = publication
.dispatch(request(
"document-1",
None,
writer.reference(),
None,
"label",
vec![],
))
.unwrap();
let reader_result = publication
.dispatch(request(
"document-1",
None,
reader.reference(),
None,
"label",
vec![],
))
.unwrap();
assert_eq!(writer_result.value, Some(Value::String("writer".into())));
assert_eq!(reader_result.value, Some(Value::String("reader".into())));
assert_eq!(publication.implementation().calls.get(), 2);
assert_eq!(
publication
.implementation()
.private_backend_handle
.borrow()
.as_str(),
"/private/backend/path"
);
}
#[test]
fn implemented_but_unexposed_interface_is_not_a_member() {
let mut registry = InterfaceRegistry::new();
let reader = registry.register(ReaderDefinition, None).unwrap();
let hidden = registry.register(HiddenDefinition, None).unwrap();
let publication = PublishedObject::new(Document::new("document-2"))
.expose(&reader)
.unwrap();
let error = publication
.dispatch(request(
"document-2",
None,
hidden.reference(),
None,
"hidden",
vec![],
))
.unwrap_err();
assert_eq!(error.kind(), CallErrorKind::InterfaceMismatch);
assert_eq!(
error.protocol_error().category,
ProtocolErrorCategory::InterfaceNotFound
);
assert_eq!(publication.implementation().calls.get(), 0);
}
#[test]
fn zero_interface_objects_are_valid() {
let publication = PublishedObject::new(Document::new("empty"));
let object = publication.protocol_object();
assert_eq!(object.id, ObjectId("empty".into()));
assert!(object.interfaces.is_empty());
object.validate().unwrap();
}
#[test]
fn duplicate_exposure_is_rejected_without_reordering() {
let mut registry = InterfaceRegistry::new();
let reader = registry.register(ReaderDefinition, None).unwrap();
let publication = PublishedObject::new(Document::new("duplicate"))
.expose(&reader)
.unwrap();
let error = match publication.expose(&reader) {
Ok(_) => panic!("duplicate exposure unexpectedly succeeded"),
Err(error) => error,
};
assert!(error.to_string().contains(reader.reference().as_str()));
}
#[test]
fn operation_and_argument_failures_are_mapped_without_execution() {
let mut registry = InterfaceRegistry::new();
let reader = registry.register(ReaderDefinition, None).unwrap();
let publication = PublishedObject::new(Document::new("validation"))
.expose(&reader)
.unwrap();
let missing = publication
.dispatch(request(
"validation",
None,
reader.reference(),
None,
"missing",
vec![],
))
.unwrap_err();
assert_eq!(missing.kind(), CallErrorKind::OperationNotFound);
assert_eq!(
missing.protocol_error().category,
ProtocolErrorCategory::OperationNotFound
);
let invalid = publication
.dispatch(request(
"validation",
None,
reader.reference(),
None,
"add",
vec![NamedValue {
name: "left".into(),
value: Value::String("wrong".into()),
}],
))
.unwrap_err();
assert_eq!(invalid.kind(), CallErrorKind::InvalidArguments);
assert_eq!(
invalid.protocol_error().category,
ProtocolErrorCategory::InvalidArguments
);
assert_eq!(publication.implementation().calls.get(), 0);
}
#[test]
fn invalid_implementation_result_is_internal_and_not_returned() {
let mut registry = InterfaceRegistry::new();
let broken = registry.register(BrokenDefinition, None).unwrap();
let publication = PublishedObject::new(Document::new("broken"))
.expose(&broken)
.unwrap();
let error = publication
.dispatch(request(
"broken",
None,
broken.reference(),
None,
"broken",
vec![],
))
.unwrap_err();
assert_eq!(error.kind(), CallErrorKind::InvalidResult);
assert_eq!(
error.protocol_error().category,
ProtocolErrorCategory::Internal
);
assert_eq!(publication.implementation().calls.get(), 1);
}
#[test]
fn object_validator_missing_and_mismatch_are_distinct_and_prevent_execution() {
let mut registry = InterfaceRegistry::new();
let reader = registry.register(ReaderDefinition, None).unwrap();
let publication = PublishedObject::new(Document::strict("strict", b"object-v1"))
.expose(&reader)
.unwrap();
let missing = publication
.dispatch(request(
"strict",
None,
reader.reference(),
None,
"label",
vec![],
))
.unwrap_err();
assert_eq!(missing.kind(), CallErrorKind::ValidatorRequired);
let mismatch = publication
.dispatch(request(
"strict",
Some(b"stale"),
reader.reference(),
None,
"label",
vec![],
))
.unwrap_err();
assert_eq!(mismatch.kind(), CallErrorKind::ValidatorMismatch);
assert_eq!(publication.implementation().calls.get(), 0);
let result = publication
.dispatch(request(
"strict",
Some(b"object-v1"),
reader.reference(),
None,
"label",
vec![],
))
.unwrap();
assert_eq!(result.validator, Some(Validator::new(b"object-v1")));
assert_eq!(publication.implementation().calls.get(), 1);
}
#[test]
fn interface_validator_missing_and_mismatch_are_distinct_and_prevent_execution() {
let mut registry = InterfaceRegistry::new();
let writer = registry
.register(WriterDefinition, Some(Validator::new(b"schema-v1")))
.unwrap();
let publication = PublishedObject::new(Document::new("mutable-interface"))
.expose(&writer)
.unwrap();
let observation = registry.observe(writer.reference()).unwrap();
assert_eq!(observation.validator, Some(Validator::new(b"schema-v1")));
let missing = publication
.dispatch(request(
"mutable-interface",
None,
writer.reference(),
None,
"label",
vec![],
))
.unwrap_err();
assert_eq!(missing.kind(), CallErrorKind::InterfaceValidatorRequired);
let mismatch = publication
.dispatch(request(
"mutable-interface",
None,
writer.reference(),
Some(b"schema-v0"),
"label",
vec![],
))
.unwrap_err();
assert_eq!(mismatch.kind(), CallErrorKind::InterfaceValidatorMismatch);
assert_eq!(publication.implementation().calls.get(), 0);
let result = publication
.dispatch(request(
"mutable-interface",
None,
writer.reference(),
Some(b"schema-v1"),
"label",
vec![],
))
.unwrap();
assert_eq!(result.value, Some(Value::String("writer".into())));
}
#[test]
fn host_failures_map_to_internal_without_becoming_domain_values() {
#[interface]
pub trait Failing {
/// Always fails in the host implementation.
fn fail(&self) -> OperationResult<String>;
}
impl Failing for Document {
fn fail(&self) -> OperationResult<String> {
Err(HostError::new("backend unavailable"))
}
}
let mut registry = InterfaceRegistry::new();
let failing = registry.register(FailingDefinition, None).unwrap();
let publication = PublishedObject::new(Document::new("failure"))
.expose(&failing)
.unwrap();
let error = publication
.dispatch(request(
"failure",
None,
failing.reference(),
None,
"fail",
vec![],
))
.unwrap_err();
assert_eq!(error.kind(), CallErrorKind::Implementation);
assert_eq!(
error.protocol_error().category,
ProtocolErrorCategory::Internal
);
}
@@ -0,0 +1,22 @@
use wip_host::{InterfaceRegistry, PublishedObject, WipObject};
use wip_interface::{OperationResult, interface};
use wip_protocol::ObjectId;
#[interface]
pub trait Search {
fn search(&self) -> OperationResult<String>;
}
struct Object;
impl WipObject for Object {
fn id(&self) -> ObjectId {
ObjectId("object".into())
}
}
fn main() {
let mut registry = InterfaceRegistry::new();
let search = registry.register(SearchDefinition, None).unwrap();
let _published = PublishedObject::new(Object).expose(&search).unwrap();
}
@@ -0,0 +1,34 @@
error[E0277]: the trait bound `Object: Search` is not satisfied
--> tests/ui/unimplemented_interface.rs:21:58
|
21 | let _published = PublishedObject::new(Object).expose(&search).unwrap();
| ------ ^^^^^^^ unsatisfied trait bound
| |
| required by a bound introduced by this call
|
help: the trait `Search` is not implemented for `Object`
--> tests/ui/unimplemented_interface.rs:10:1
|
10 | struct Object;
| ^^^^^^^^^^^^^
help: this trait has no implementations, consider adding one
--> tests/ui/unimplemented_interface.rs:6:1
|
6 | pub trait Search {
| ^^^^^^^^^^^^^^^^
note: required for `SearchDefinition` to implement `InterfaceImplementation<Object>`
--> tests/ui/unimplemented_interface.rs:5:1
|
5 | #[interface]
| ^^^^^^^^^^^^
6 | pub trait Search {
| ^^^^^^
note: required by a bound in `PublishedObject::<T>::expose`
--> src/lib.rs
|
| pub fn expose<D>(mut self, interface: &RegisteredInterface<D>) -> Result<Self, PublicationError>
| ------ required by a bound in this associated function
| where
| D: InterfaceImplementation<T>,
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `PublishedObject::<T>::expose`
= note: this error originates in the attribute macro `interface` (in Nightly builds, run with -Z macro-backtrace for more info)
+25 -8
View File
@@ -170,14 +170,8 @@ fn expand_interface(attribute: TokenStream2, item: ItemTrait) -> syn::Result<Tok
} }
} }
impl #definition_ident { impl<T: #trait_ident> ::wip_interface::InterfaceImplementation<T> for #definition_ident {
/// Builds the interface descriptor generated from the trait declaration. fn dispatch(
pub fn descriptor() -> ::wip_interface::wip_protocol::InterfaceDescriptor {
<Self as ::wip_interface::InterfaceDefinition>::descriptor()
}
/// Dispatches one validated logical operation call to a typed implementation.
pub fn dispatch<T: #trait_ident>(
implementation: &T, implementation: &T,
operation: &str, operation: &str,
arguments: ::std::vec::Vec<::wip_interface::wip_protocol::NamedValue>, arguments: ::std::vec::Vec<::wip_interface::wip_protocol::NamedValue>,
@@ -197,6 +191,29 @@ fn expand_interface(attribute: TokenStream2, item: ItemTrait) -> syn::Result<Tok
} }
} }
impl #definition_ident {
/// Builds the interface descriptor generated from the trait declaration.
pub fn descriptor() -> ::wip_interface::wip_protocol::InterfaceDescriptor {
<Self as ::wip_interface::InterfaceDefinition>::descriptor()
}
/// Dispatches one validated logical operation call to a typed implementation.
pub fn dispatch<T: #trait_ident>(
implementation: &T,
operation: &str,
arguments: ::std::vec::Vec<::wip_interface::wip_protocol::NamedValue>,
) -> ::std::result::Result<
::std::option::Option<::wip_interface::wip_protocol::Value>,
::wip_interface::DispatchError,
> {
<Self as ::wip_interface::InterfaceImplementation<T>>::dispatch(
implementation,
operation,
arguments,
)
}
}
#(#argument_items)* #(#argument_items)*
}) })
} }
+15
View File
@@ -92,6 +92,21 @@ pub trait InterfaceDefinition {
fn descriptor() -> InterfaceDescriptor; fn descriptor() -> InterfaceDescriptor;
} }
/// Typed dispatch implemented by every generated interface companion.
///
/// `T` must implement the Rust interface trait associated with this definition.
/// Host runtimes use this bound when an interface is explicitly exposed, so an
/// object that does not implement that interface is rejected at compile time.
pub trait InterfaceImplementation<T>: InterfaceDefinition {
/// Validates, decodes, and dispatches one operation to `implementation`,
/// then encodes and validates its result.
fn dispatch(
implementation: &T,
operation: &str,
arguments: Vec<NamedValue>,
) -> Result<Option<Value>, DispatchError>;
}
/// Result convention required for generated interface operations. /// Result convention required for generated interface operations.
/// ///
/// `Ok(T)` is an application result, including domain-failure union cases. /// `Ok(T)` is an application result, including domain-failure union cases.
+103 -77
View File
@@ -8,79 +8,65 @@ pub enum NameNamespace {
TypeDeclaration, TypeDeclaration,
/// Operations in an interface descriptor. /// Operations in an interface descriptor.
Operation, Operation,
/// Fields in a record declaration. /// Fields in an inline or named record type.
Field, Field,
/// Parameters in an operation declaration. /// Parameters in an operation declaration.
Parameter, Parameter,
/// Variants in an enum declaration. /// Cases in an enum type.
EnumVariant, EnumCase,
/// Variants in a union declaration. /// Cases in a union type.
UnionVariant, UnionCase,
/// Fields supplied in a record value. /// Child object names beneath one tree node.
RecordValueField, ChildObject,
/// Arguments supplied in an operation call.
Argument,
/// Members in a logical JSON object.
JsonMember,
/// Descriptors in an interface observation.
InterfaceDescriptor,
} }
/// Structural kind of a logical protocol value. /// Structural kind of a schema-neutral logical value.
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ValueKind { pub enum ValueKind {
/// String value. /// Unit value.
String, Unit,
/// Boolean value. /// Boolean value.
Boolean, Boolean,
/// Integer value. /// Integer value.
Integer, Integer,
/// Floating-point number value. /// Floating-point number value.
Number, Number,
/// String value.
String,
/// Byte sequence. /// Byte sequence.
Bytes, Bytes,
/// List value.
List,
/// Key/value entry.
Entry,
/// Record value. /// Record value.
Record, Record,
/// Enum value. /// List value.
Enum, List,
/// Tagged-union value.
Union,
/// Logical JSON value.
Json,
} }
/// A typed segment identifying a declaration or value location. /// A typed segment identifying a declaration or value location.
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub enum PathSegment { pub enum PathSegment {
/// Interface descriptor. /// Descriptor root.
Interface(String), Descriptor,
/// Named type declaration. /// Named type declaration.
Declaration(String), Declaration(String),
/// Operation declaration or call. /// Operation declaration or call.
Operation(String), Operation(String),
/// Record field declaration or value. /// Record field declaration or value.
Field(String), Field(String),
/// Operation parameter declaration or argument. /// Operation parameter or argument.
Parameter(String), Parameter(String),
/// Enum or union variant. /// Enum or union case.
Variant(String), Case(String),
/// List or JSON array index. /// List index.
Index(usize), Index(usize),
/// Entry key.
EntryKey,
/// Entry value.
EntryValue,
/// Operation return value. /// Operation return value.
Return, Return,
/// JSON object member. /// Object at a Worldspace path.
JsonMember(String), Object(String),
/// Child object in a tree response.
Child(String),
} }
/// Machine-readable reason why protocol validation failed. /// Machine-readable reason why logical protocol validation failed.
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub enum ValidationErrorKind { pub enum ValidationErrorKind {
/// A name occurred more than once in one namespace. /// A name occurred more than once in one namespace.
@@ -117,11 +103,15 @@ pub enum ValidationErrorKind {
/// Missing field name. /// Missing field name.
name: String, name: String,
}, },
/// An enum or union value selected an undeclared variant. /// An enum or union selected an undeclared case.
UnknownVariant { UnknownCase {
/// Unknown variant name. /// Unknown case name.
name: String, name: String,
}, },
/// A payload-bearing union case omitted `value`.
MissingUnionPayload,
/// A payload-free union case supplied `value`.
UnexpectedUnionPayload,
/// An operation call contains an undeclared argument. /// An operation call contains an undeclared argument.
UnknownParameter { UnknownParameter {
/// Unknown parameter name. /// Unknown parameter name.
@@ -132,44 +122,66 @@ pub enum ValidationErrorKind {
/// Missing parameter name. /// Missing parameter name.
name: String, name: String,
}, },
/// An integer is outside the JavaScript-lossless range. /// An operation is not declared by the descriptor.
UnknownOperation {
/// Unknown operation name.
name: String,
},
/// An integer is outside the interoperable safe range.
IntegerOutOfRange { IntegerOutOfRange {
/// Rejected integer. /// Rejected integer.
value: i64, value: i64,
}, },
/// A numeric value is NaN or infinite. /// A numeric value is NaN or infinite.
NonFiniteNumber, NonFiniteNumber,
/// A JSON-interpreted value tree contains protocol bytes.
BytesInJson,
/// An object name is not a valid root or non-root path segment.
InvalidObjectName {
/// Rejected object name.
name: String,
},
/// An object's ordered interface set contains a duplicate. /// An object's ordered interface set contains a duplicate.
DuplicateInterface { DuplicateInterface {
/// Duplicate interface name. /// Duplicate opaque interface reference.
name: String, reference: String,
}, },
/// An observation omits a descriptor named by its object. /// A string is not a canonical absolute Worldspace path.
MissingInterfaceDescriptor { InvalidPath {
/// Missing interface name. /// Rejected path.
name: String, path: String,
}, },
/// An observation contains a descriptor not named by its object. /// An object name does not match the path where it was observed.
UnexpectedInterfaceDescriptor { ObjectNameMismatch {
/// Unexpected interface name. /// Name required by the observation path.
name: String,
},
/// A call targets an interface other than the descriptor being used.
InterfaceMismatch {
/// Descriptor interface name.
expected: String, expected: String,
/// Call target interface name. /// Name supplied by the object.
actual: String, actual: String,
}, },
/// An operation is not declared by the interface. /// Equivalent ref/validator observations disagree on object representation.
UnknownOperation { InconsistentObjectObservation {
/// Unknown operation name. /// Shared opaque object reference.
name: String, reference: String,
},
/// A `fetch_tree` request used a zero limit.
ZeroTreeLimit,
/// A tree response exceeds the requested maximum depth.
TreeDepthExceeded {
/// Requested maximum depth.
maximum: u32,
},
/// A tree response exceeds the requested maximum node count.
TreeLimitExceeded {
/// Requested maximum node count.
limit: u32,
},
/// A response refers to a different interface than its request.
InterfaceResponseMismatch {
/// Requested opaque reference.
expected: String,
/// Returned opaque reference.
actual: String,
}, },
/// A no-return operation produced a value.
UnexpectedReturnValue,
/// An operation with a return declaration produced no value.
MissingReturnValue,
} }
/// A validation failure with a typed path to the failing location. /// A validation failure with a typed path to the failing location.
@@ -211,30 +223,44 @@ impl Display for ValidationError {
impl Error for ValidationError {} impl Error for ValidationError {}
/// Stable category of an error returned by a protocol host runtime. /// Stable protocol failure code returned by a host or produced by a client.
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProtocolErrorCategory { pub enum ProtocolErrorCode {
/// The request is structurally invalid. /// The request is structurally invalid.
InvalidRequest, InvalidRequest,
/// The target object is not published. /// The target object is not published.
ObjectNotFound, NotFound,
/// The target interface is not implemented by the object. /// The interface reference is not currently published.
InterfaceNotFound, InterfaceNotFound,
/// The target operation is not declared by the interface. /// The selected interface is not on the current object.
InterfaceMismatch,
/// The selected operation is not declared by the current interface.
OperationNotFound, OperationNotFound,
/// Supplied operation arguments are invalid. /// Supplied operation arguments are invalid.
InvalidArguments, InvalidArguments,
/// The implementation rejected the call for a domain reason. /// A host result does not satisfy the declaration.
OperationFailed, InvalidResult,
/// The operation requires an object validator and none was supplied.
ValidatorRequired,
/// The supplied object validator does not match current state.
ValidatorMismatch,
/// A mutable interface requires its validator and none was supplied.
InterfaceValidatorRequired,
/// The supplied interface validator does not match the descriptor.
InterfaceValidatorMismatch,
/// A published operation is not permitted for the caller.
PermissionDenied,
/// A bounded protocol resource limit was exceeded.
ResourceLimitExceeded,
/// The host encountered an internal failure. /// The host encountered an internal failure.
Internal, Internal,
} }
/// Transport-independent protocol failure returned by a host runtime. /// Transport-independent protocol failure.
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProtocolError { pub struct ProtocolError {
/// Stable programmatic category. /// Stable programmatic code. Consumers must branch on this field.
pub category: ProtocolErrorCategory, pub code: ProtocolErrorCode,
/// Human-readable diagnostic. Consumers must not branch on this text. /// Bounded human-readable diagnostic. Consumers must not branch on it.
pub message: String, pub message: String,
} }
+50 -13
View File
@@ -1,9 +1,32 @@
//! Transport-independent logical types for the Web Interface Protocol (WIP). //! Canonical transport-independent logical types for the Web Interface Protocol.
//! //!
//! This crate deliberately models interface declarations, published objects, //! This crate follows the Core Model and `3-protocol.md` without assigning HTTP
//! operation calls, and operation results without assigning them HTTP routes, //! routes, methods, headers, URL encodings, or JSON envelopes. The correspondence
//! methods, headers, or JSON envelopes. A transport can encode these values, //! between canonical logical concepts and this crate is direct:
//! but the encoding is outside this crate's contract. //!
//! | Canonical protocol concept | Rust API |
//! | --- | --- |
//! | Object projection and ordered interface set | [`Object`] |
//! | Optional object identity / state precondition | `Object::ref` / [`Object::validator`] |
//! | Interface Descriptor | [`InterfaceDescriptor`] |
//! | Named declaration and all schema expressions | [`TypeDeclaration`] / [`TypeExpr`] |
//! | Schema-neutral primitive/composite value | [`Value`] |
//! | `fetch` | [`FetchRequest`] / [`FetchResponse`] |
//! | `fetch_tree` | [`FetchTreeRequest`] / [`FetchTreeResponse`] |
//! | `fetch_interface` | [`FetchInterfaceRequest`] / [`FetchInterfaceResponse`] |
//! | Object and interface call preconditions | [`Target`] / [`InterfaceTarget`] |
//! | `call_operation` | [`CallOperationRequest`] / [`CallOperationResponse`] |
//! | Stable failure taxonomy | [`ProtocolErrorCode`] / [`ProtocolError`] |
//!
//! Paths, object refs, interface refs, and validators remain separate concepts.
//! They are represented by ordinary protocol strings/bytes in the corresponding
//! fields. In particular, object refs are optional, operation targeting uses a
//! path, and this crate defines no interaction that fetches an object by ref.
//!
//! [`Value`] contains only the canonical raw value kinds. A descriptor interprets
//! enum values as strings, entries as canonical-path strings, and unions as
//! records with a `$case` string plus an optional `value` payload. JSON uses the
//! same value tree (`Unit` is JSON null) and rejects bytes.
#![deny(missing_docs)] #![deny(missing_docs)]
@@ -12,19 +35,33 @@ mod model;
mod validation; mod validation;
pub use error::{ pub use error::{
NameNamespace, PathSegment, ProtocolError, ProtocolErrorCategory, ValidationError, NameNamespace, PathSegment, ProtocolError, ProtocolErrorCode, ValidationError,
ValidationErrorKind, ValueKind, ValidationErrorKind, ValueKind,
}; };
pub use model::{ pub use model::{
Documentation, EnumDeclaration, EnumVariantDeclaration, FieldDeclaration, InterfaceDescriptor, CallOperationRequest, CallOperationResponse, Documentation, EnumCase, FetchInterfaceRequest,
InterfaceObservation, JsonMember, JsonValue, NamedValue, Object, ObjectId, OperationCall, FetchInterfaceResponse, FetchRequest, FetchResponse, FetchTreeRequest, FetchTreeResponse,
OperationDeclaration, OperationOutcome, OperationResult, OperationTarget, ParameterDeclaration, FieldDeclaration, InterfaceDescriptor, InterfaceTarget, Object, ObjectTreeNode,
RecordDeclaration, ReturnDeclaration, TypeDeclaration, TypeExpr, UnionDeclaration, OperationDeclaration, ParameterDeclaration, ProtocolResult, ReturnDeclaration, Target,
UnionVariantDeclaration, Value, TypeDeclaration, TypeExpr, UnionCase, Value,
}; };
/// Largest integer that is exactly interoperable with a JavaScript `Number`. /// Validates a canonical absolute Worldspace path.
pub fn validate_path(path: &str) -> Result<(), ValidationError> {
validation::validate_path(path)
}
/// Canonical format identifier for the first Interface Descriptor format.
pub const INTERFACE_FORMAT_V1: &str = "wip-interface/1";
/// Discriminator field used by a union's raw record value.
pub const UNION_CASE_FIELD: &str = "$case";
/// Optional payload field used by a union's raw record value.
pub const UNION_VALUE_FIELD: &str = "value";
/// Largest integer exactly interoperable with a JavaScript `Number`.
pub const MAX_SAFE_INTEGER: i64 = 9_007_199_254_740_991; pub const MAX_SAFE_INTEGER: i64 = 9_007_199_254_740_991;
/// Smallest integer that is exactly interoperable with a JavaScript `Number`. /// Smallest integer exactly interoperable with a JavaScript `Number`.
pub const MIN_SAFE_INTEGER: i64 = -MAX_SAFE_INTEGER; pub const MIN_SAFE_INTEGER: i64 = -MAX_SAFE_INTEGER;
+293 -252
View File
@@ -1,6 +1,8 @@
use std::collections::BTreeMap;
use crate::{ProtocolError, ValidationError}; use crate::{ProtocolError, ValidationError};
/// Human-readable documentation attached to a protocol declaration. /// Human-readable plain-text documentation attached to a protocol declaration.
#[derive(Debug, Clone, PartialEq, Eq, Default)] #[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct Documentation { pub struct Documentation {
/// A short, self-contained description. /// A short, self-contained description.
@@ -9,242 +11,194 @@ pub struct Documentation {
pub details: Option<String>, pub details: Option<String>,
} }
/// Stable logical identity of a published object. /// The protocol projection currently published at one Worldspace path.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ObjectId(pub String);
/// A published object and the ordered set of interfaces it implements.
/// ///
/// The vector order is significant. Call [`Object::validate`] before /// `name` is a path segment, `interfaces` is an ordered set of opaque interface
/// publication to reject duplicate interface names. /// references, `r#ref` is optional object identity, and `validator` is an
/// independent optional observation precondition. In particular, neither the
/// path nor `r#ref` is a mandatory object identifier.
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct Object { pub struct Object {
/// Logical object identity. /// Path-segment name. The root object's name is empty.
pub id: ObjectId, pub name: String,
/// Interface names in publication order. /// Short public description of the object.
pub description: Option<String>,
/// Opaque interface references in host publication order.
pub interfaces: Vec<String>, pub interfaces: Vec<String>,
} /// Optional opaque identity within one client-facing Worldspace.
pub r#ref: Option<String>,
/// A complete logical observation of an object and its interface descriptors. /// Optional opaque validator for the object's observed state.
#[derive(Debug, Clone, PartialEq)] pub validator: Option<Vec<u8>>,
pub struct InterfaceObservation {
/// Observed object.
pub object: Object,
/// Descriptors corresponding to `object.interfaces`.
pub descriptors: Vec<InterfaceDescriptor>,
} }
/// Transport-independent description of one interface. /// Transport-independent description of one interface.
#[derive(Debug, Clone, PartialEq)] ///
/// Interface identity is supplied by interaction fields such as
/// [`FetchInterfaceResponse::interface`], not embedded in the descriptor.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InterfaceDescriptor { pub struct InterfaceDescriptor {
/// Interface name. /// Descriptor format version understood by clients and hosts.
pub name: String, pub format: String,
/// Optional interface documentation. /// Optional interface documentation.
pub documentation: Option<Documentation>, pub documentation: Option<Documentation>,
/// Named type declarations local to this descriptor. /// Named declarations local to this descriptor.
pub declarations: Vec<TypeDeclaration>, pub types: Vec<TypeDeclaration>,
/// Operations exposed by this interface. /// Operations exposed by this interface.
pub operations: Vec<OperationDeclaration>, pub operations: Vec<OperationDeclaration>,
} }
/// A named type declaration local to an interface descriptor. /// A descriptor-local named type declaration.
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub enum TypeDeclaration { pub struct TypeDeclaration {
/// A record declaration.
Record(RecordDeclaration),
/// An enumeration declaration.
Enum(EnumDeclaration),
/// A tagged union declaration.
Union(UnionDeclaration),
}
impl TypeDeclaration {
/// Returns the descriptor-local declaration name.
#[must_use]
pub fn name(&self) -> &str {
match self {
Self::Record(declaration) => &declaration.name,
Self::Enum(declaration) => &declaration.name,
Self::Union(declaration) => &declaration.name,
}
}
}
/// Declaration of a record with named fields.
#[derive(Debug, Clone, PartialEq)]
pub struct RecordDeclaration {
/// Descriptor-local type name. /// Descriptor-local type name.
pub name: String, pub name: String,
/// Optional record documentation. /// Optional type documentation.
pub documentation: Option<Documentation>, pub documentation: Option<Documentation>,
/// Definition associated with `name`.
pub definition: TypeExpr,
}
/// A type expression used by declarations, fields, parameters, and returns.
///
/// [`TypeExpr::Named`] references are resolved only within the containing
/// descriptor. Recursive references, including recursion through lists,
/// records, and unions, are invalid.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TypeExpr {
/// A value carrying no data.
Unit,
/// A boolean.
Boolean,
/// A JavaScript-lossless signed integer.
Integer,
/// A finite IEEE-754 binary64 number.
Number,
/// A Unicode string.
String,
/// An opaque byte sequence.
Bytes,
/// A schema-opaque JSON-compatible value tree.
Json,
/// A canonical absolute Worldspace path represented by a string value.
Entry,
/// A descriptor-local named declaration.
Named {
/// Referenced declaration name.
name: String,
},
/// A record with named fields.
Record {
/// Fields in declaration order. /// Fields in declaration order.
pub fields: Vec<FieldDeclaration>, fields: Vec<FieldDeclaration>,
},
/// An ordered list.
List {
/// Type of every list item.
items: Box<TypeExpr>,
},
/// A closed set of payload-free cases represented by strings.
Enum {
/// Cases in declaration order.
cases: Vec<EnumCase>,
},
/// A discriminated union represented by a record.
Union {
/// Cases in declaration order.
cases: Vec<UnionCase>,
},
} }
/// Declaration of a record field. /// Declaration of a record field.
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct FieldDeclaration { pub struct FieldDeclaration {
/// Field name. /// Field name.
pub name: String, pub name: String,
/// Whether the field must be present.
pub required: bool,
/// Optional field documentation. /// Optional field documentation.
pub documentation: Option<Documentation>, pub documentation: Option<Documentation>,
/// Field value type. /// Field value type.
pub ty: TypeExpr, pub r#type: TypeExpr,
/// Whether the field may be omitted.
///
/// Optionality represents absence, not a `null` value.
pub optional: bool,
} }
/// Declaration of an enumeration. /// One payload-free enum case.
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct EnumDeclaration { pub struct EnumCase {
/// Descriptor-local type name. /// Case name.
pub name: String, pub name: String,
/// Optional enum documentation. /// Optional case documentation.
pub documentation: Option<Documentation>, pub documentation: Option<Documentation>,
/// Allowed variants in declaration order.
pub variants: Vec<EnumVariantDeclaration>,
} }
/// Declaration of one enumeration variant. /// One union case with zero or one payload.
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct EnumVariantDeclaration { pub struct UnionCase {
/// Variant name. /// Case name.
pub name: String, pub name: String,
/// Optional variant documentation. /// Optional case documentation.
pub documentation: Option<Documentation>, pub documentation: Option<Documentation>,
} /// Optional payload type.
pub payload: Option<TypeExpr>,
/// Declaration of a tagged union.
#[derive(Debug, Clone, PartialEq)]
pub struct UnionDeclaration {
/// Descriptor-local type name.
pub name: String,
/// Optional union documentation.
pub documentation: Option<Documentation>,
/// Allowed tagged variants in declaration order.
pub variants: Vec<UnionVariantDeclaration>,
}
/// Declaration of one tagged-union variant.
#[derive(Debug, Clone, PartialEq)]
pub struct UnionVariantDeclaration {
/// Variant tag.
pub name: String,
/// Optional variant documentation.
pub documentation: Option<Documentation>,
/// Payload type.
pub ty: TypeExpr,
}
/// A type expression used by fields, parameters, returns, and union payloads.
///
/// `Named` references are resolved only within the containing descriptor.
/// Optional values are represented by the containing field or parameter and
/// therefore are intentionally not a type expression.
#[derive(Debug, Clone, PartialEq)]
pub enum TypeExpr {
/// A UTF-8 string.
String,
/// A boolean.
Boolean,
/// A JavaScript-lossless integer.
Integer,
/// A finite IEEE-754 double precision number.
Number,
/// An opaque byte sequence.
Bytes,
/// An ordered sequence of values.
List(Box<TypeExpr>),
/// A key/value entry.
Entry {
/// Entry key type.
key: Box<TypeExpr>,
/// Entry value type.
value: Box<TypeExpr>,
},
/// A logical JSON value.
Json,
/// A descriptor-local named declaration.
Named(String),
} }
/// Declaration of one interface operation. /// Declaration of one interface operation.
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct OperationDeclaration { pub struct OperationDeclaration {
/// Operation name, unique within its interface. /// Operation name, unique within its descriptor.
pub name: String, pub name: String,
/// Optional operation documentation. /// Optional operation documentation.
pub documentation: Option<Documentation>, pub documentation: Option<Documentation>,
/// Parameters in declaration order. /// Parameters in declaration order.
pub parameters: Vec<ParameterDeclaration>, pub parameters: Vec<ParameterDeclaration>,
/// Return declaration, or `None` for an operation with no return value. /// Return declaration. No-data operations return [`TypeExpr::Unit`].
pub returns: Option<ReturnDeclaration>, pub returns: ReturnDeclaration,
} }
/// Declaration of one operation parameter. /// Declaration of one operation parameter.
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParameterDeclaration { pub struct ParameterDeclaration {
/// Parameter name. /// Parameter name.
pub name: String, pub name: String,
/// Whether the parameter must be present.
pub required: bool,
/// Optional parameter documentation. /// Optional parameter documentation.
pub documentation: Option<Documentation>, pub documentation: Option<Documentation>,
/// Parameter value type. /// Parameter value type.
pub ty: TypeExpr, pub r#type: TypeExpr,
/// Whether the parameter may be omitted.
///
/// Optionality represents absence, not a `null` value.
pub optional: bool,
} }
/// Declaration of an operation return value. /// Declaration of an operation return value.
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReturnDeclaration { pub struct ReturnDeclaration {
/// Optional return-value documentation. /// Optional return-value documentation.
pub documentation: Option<Documentation>, pub documentation: Option<Documentation>,
/// Return value type. /// Return value type.
pub ty: TypeExpr, pub r#type: TypeExpr,
} }
/// A logical protocol value. /// A schema-neutral logical protocol value.
/// ///
/// There is no general `Null` variant: optional record fields and operation /// Enum cases, union cases, entries, and JSON are interpretations imposed by a
/// parameters are omitted. JSON null remains available inside [`Value::Json`]. /// descriptor. They are deliberately not variants of this enum. Record key
/// ordering is not protocol-significant.
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub enum Value { pub enum Value {
/// A UTF-8 string. /// A value carrying no data; it also represents JSON null under `Json`.
String(String), Unit,
/// A boolean. /// A boolean.
Boolean(bool), Boolean(bool),
/// An integer, validated against the JavaScript safe integer range. /// A JavaScript-lossless integer.
Integer(i64), Integer(i64),
/// A number, validated to be finite. /// A finite IEEE-754 binary64 number.
Number(f64), Number(f64),
/// A Unicode string.
String(String),
/// An opaque byte sequence. /// An opaque byte sequence.
Bytes(Vec<u8>), Bytes(Vec<u8>),
/// String-keyed fields with order-insensitive semantics.
Record(BTreeMap<String, Value>),
/// An ordered sequence. /// An ordered sequence.
List(Vec<Value>), List(Vec<Value>),
/// A key/value entry.
Entry {
/// Entry key.
key: Box<Value>,
/// Entry value.
value: Box<Value>,
},
/// Record fields. Duplicate names are invalid.
Record(Vec<NamedValue>),
/// An enum variant name.
Enum(String),
/// A tagged-union value.
Union {
/// Selected variant tag.
variant: String,
/// Variant payload.
value: Box<Value>,
},
/// A logical JSON value.
Json(JsonValue),
} }
impl Value { impl Value {
@@ -252,132 +206,219 @@ impl Value {
#[must_use] #[must_use]
pub fn kind(&self) -> crate::ValueKind { pub fn kind(&self) -> crate::ValueKind {
match self { match self {
Self::String(_) => crate::ValueKind::String, Self::Unit => crate::ValueKind::Unit,
Self::Boolean(_) => crate::ValueKind::Boolean, Self::Boolean(_) => crate::ValueKind::Boolean,
Self::Integer(_) => crate::ValueKind::Integer, Self::Integer(_) => crate::ValueKind::Integer,
Self::Number(_) => crate::ValueKind::Number, Self::Number(_) => crate::ValueKind::Number,
Self::String(_) => crate::ValueKind::String,
Self::Bytes(_) => crate::ValueKind::Bytes, Self::Bytes(_) => crate::ValueKind::Bytes,
Self::List(_) => crate::ValueKind::List,
Self::Entry { .. } => crate::ValueKind::Entry,
Self::Record(_) => crate::ValueKind::Record, Self::Record(_) => crate::ValueKind::Record,
Self::Enum(_) => crate::ValueKind::Enum, Self::List(_) => crate::ValueKind::List,
Self::Union { .. } => crate::ValueKind::Union,
Self::Json(_) => crate::ValueKind::Json,
} }
} }
} }
/// A named value used by records and operation arguments. /// Request for the object currently published at one path.
#[derive(Debug, Clone, PartialEq)]
pub struct NamedValue {
/// Field or parameter name.
pub name: String,
/// Associated value.
pub value: Value,
}
/// A transport-independent JSON value.
#[derive(Debug, Clone, PartialEq)]
pub enum JsonValue {
/// JSON null.
Null,
/// JSON boolean.
Boolean(bool),
/// JSON integer, validated against the JavaScript safe integer range.
Integer(i64),
/// JSON number, validated to be finite.
Number(f64),
/// JSON string.
String(String),
/// JSON array.
Array(Vec<JsonValue>),
/// JSON object. Duplicate member names are invalid.
Object(Vec<JsonMember>),
}
/// A member of a logical JSON object.
#[derive(Debug, Clone, PartialEq)]
pub struct JsonMember {
/// Member name.
pub name: String,
/// Member value.
pub value: JsonValue,
}
/// Logical destination of an operation call.
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct OperationTarget { pub struct FetchRequest {
/// Target object. /// Canonical absolute Worldspace path.
pub object: ObjectId, pub path: String,
/// Target interface name. }
/// Successful `fetch` response.
pub type FetchResponse = Object;
/// Request for a bounded breadth-first object-tree observation.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FetchTreeRequest {
/// Canonical absolute path of the tree root.
pub path: String,
/// Maximum depth, where the requested object has depth zero.
pub depth: u32,
/// Maximum total node count, including the root. Must be nonzero.
pub limit: u32,
}
/// One node in a tree observation.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ObjectTreeNode {
/// Object observed at this tree position.
pub object: Object,
/// Direct children in host-defined traversal order.
pub children: Vec<ObjectTreeNode>,
}
/// Successful `fetch_tree` response.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FetchTreeResponse {
/// Observed tree rooted at the requested path.
pub root: ObjectTreeNode,
/// Whether public indexable children were omitted by depth or limit.
pub truncated: bool,
}
/// Request for the current descriptor of one opaque interface reference.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FetchInterfaceRequest {
/// Opaque interface reference obtained from an object.
pub interface: String, pub interface: String,
/// Target operation name. }
/// Successful `fetch_interface` response.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FetchInterfaceResponse {
/// Opaque interface reference corresponding to the request.
pub interface: String,
/// Current validated descriptor.
pub descriptor: InterfaceDescriptor,
/// Optional opaque validator for this descriptor observation.
pub validator: Option<Vec<u8>>,
}
/// Object portion of an operation target.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Target {
/// Canonical absolute path of the target object.
pub path: String,
/// Optional validator copied from the object observation.
pub validator: Option<Vec<u8>>,
}
/// Interface portion of an operation target.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InterfaceTarget {
/// Opaque interface reference selected from the object's interface set.
pub reference: String,
/// Optional validator copied from the interface observation.
pub validator: Option<Vec<u8>>,
}
/// Transport-independent request to execute one operation.
#[derive(Debug, Clone, PartialEq)]
pub struct CallOperationRequest {
/// Path and optional object-state precondition.
pub target: Target,
/// Interface reference and its independent optional precondition.
pub interface: InterfaceTarget,
/// Operation name within the selected descriptor.
pub operation: String, pub operation: String,
/// Named arguments; map iteration order has no protocol meaning.
pub arguments: BTreeMap<String, Value>,
} }
/// A transport-independent operation call. /// Successful operation response.
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub struct OperationCall { pub struct CallOperationResponse {
/// Logical call target. /// Application result validated against the operation return declaration.
pub target: OperationTarget, pub result: Value,
/// Named arguments. Optional arguments are represented by omission. /// Optional validator for the object state observed after execution.
pub arguments: Vec<NamedValue>, pub validator: Option<Vec<u8>>,
} }
/// Successful or failed logical outcome of an operation. /// Logical outcome of any core interaction.
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub enum OperationOutcome { pub enum ProtocolResult<T> {
/// Operation returned. `None` represents a declared no-return operation. /// The interaction succeeded.
Returned(Option<Value>), Success(T),
/// Operation failed with a protocol-level error. /// The interaction failed at the protocol boundary.
Failed(ProtocolError), Failure(ProtocolError),
}
/// A transport-independent operation result.
#[derive(Debug, Clone, PartialEq)]
pub struct OperationResult {
/// Target corresponding to the result.
pub target: OperationTarget,
/// Logical outcome.
pub outcome: OperationOutcome,
} }
impl Object { impl Object {
/// Validates ordered-set interface semantics. /// Validates the object-independent name and ordered-interface-set rules.
pub fn validate(&self) -> Result<(), ValidationError> { pub fn validate(&self) -> Result<(), ValidationError> {
crate::validation::validate_object(self) crate::validation::validate_object(self)
} }
}
impl InterfaceObservation { /// Validates this object as the response observed at `path`.
/// Validates the object, descriptor structure, and their correspondence. pub fn validate_at_path(&self, path: &str) -> Result<(), ValidationError> {
pub fn validate(&self) -> Result<(), ValidationError> { crate::validation::validate_object_at_path(self, path)
crate::validation::validate_observation(self) }
/// Validates equal representations for a shared ref and present validator.
pub fn validate_consistency_with(&self, other: &Self) -> Result<(), ValidationError> {
crate::validation::validate_object_consistency(self, other)
} }
} }
impl InterfaceDescriptor { impl InterfaceDescriptor {
/// Validates declaration structure, references, and absence of recursion. /// Validates declaration structure, named references, and absence of cycles.
pub fn validate(&self) -> Result<(), ValidationError> { pub fn validate(&self) -> Result<(), ValidationError> {
crate::validation::validate_descriptor(self) crate::validation::validate_descriptor(self)
} }
/// Validates a value against a type expression in this descriptor. /// Validates a raw value against a type expression in this descriptor.
pub fn validate_value(&self, ty: &TypeExpr, value: &Value) -> Result<(), ValidationError> { pub fn validate_value(&self, r#type: &TypeExpr, value: &Value) -> Result<(), ValidationError> {
crate::validation::validate_root_value(self, ty, value) crate::validation::validate_root_value(self, r#type, value)
} }
/// Validates a call target and arguments against this descriptor. /// Validates named arguments for one declared operation.
pub fn validate_call(&self, call: &OperationCall) -> Result<(), ValidationError> { pub fn validate_arguments(
crate::validation::validate_call(self, call)
}
/// Validates an operation's returned value against this descriptor.
pub fn validate_return(
&self, &self,
operation: &str, operation: &str,
value: Option<&Value>, arguments: &BTreeMap<String, Value>,
) -> Result<(), ValidationError> { ) -> Result<(), ValidationError> {
crate::validation::validate_return(self, operation, value) crate::validation::validate_arguments(self, operation, arguments)
}
/// Validates one operation result against its return declaration.
pub fn validate_result(&self, operation: &str, result: &Value) -> Result<(), ValidationError> {
crate::validation::validate_result(self, operation, result)
}
/// Validates the path, operation, and arguments in a call request.
pub fn validate_call(&self, request: &CallOperationRequest) -> Result<(), ValidationError> {
crate::validation::validate_call(self, request)
}
}
impl FetchRequest {
/// Validates the request path.
pub fn validate(&self) -> Result<(), ValidationError> {
crate::validation::validate_path(&self.path)
}
/// Validates an object returned for this request.
pub fn validate_response(&self, response: &FetchResponse) -> Result<(), ValidationError> {
response.validate_at_path(&self.path)
}
}
impl FetchTreeRequest {
/// Validates the request path and nonzero node limit.
pub fn validate(&self) -> Result<(), ValidationError> {
crate::validation::validate_fetch_tree_request(self)
}
}
impl FetchTreeResponse {
/// Validates tree shape, names, depth, and node limit for `request`.
pub fn validate_for(&self, request: &FetchTreeRequest) -> Result<(), ValidationError> {
crate::validation::validate_fetch_tree_response(self, request)
}
}
impl FetchInterfaceResponse {
/// Validates request correspondence and descriptor semantics.
pub fn validate_for(&self, request: &FetchInterfaceRequest) -> Result<(), ValidationError> {
crate::validation::validate_fetch_interface_response(self, request)
}
}
impl CallOperationRequest {
/// Validates this call against the descriptor selected by `interface`.
pub fn validate_with(&self, descriptor: &InterfaceDescriptor) -> Result<(), ValidationError> {
descriptor.validate_call(self)
}
}
impl CallOperationResponse {
/// Validates this result against the named operation in `descriptor`.
pub fn validate_for(
&self,
descriptor: &InterfaceDescriptor,
operation: &str,
) -> Result<(), ValidationError> {
descriptor.validate_result(operation, &self.result)
} }
} }
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff