feat: unify object observation API
This commit is contained in:
@@ -62,7 +62,7 @@ pub enum PathSegment {
|
||||
Return,
|
||||
/// Object at a Worldspace path.
|
||||
Object(String),
|
||||
/// Child object in a tree response.
|
||||
/// Child object in an observation response.
|
||||
Child(String),
|
||||
}
|
||||
|
||||
@@ -163,17 +163,15 @@ pub enum ValidationErrorKind {
|
||||
/// Shared opaque object reference.
|
||||
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,
|
||||
/// An observation omitted children before reaching the requested depth.
|
||||
ChildrenMissingBeforeDepth {
|
||||
/// Node depth where children were required.
|
||||
depth: u32,
|
||||
},
|
||||
/// A tree response exceeds the requested maximum node count.
|
||||
TreeLimitExceeded {
|
||||
/// Requested maximum node count.
|
||||
limit: u32,
|
||||
/// An observation included children at the requested depth boundary.
|
||||
ChildrenAtDepthBoundary {
|
||||
/// Boundary depth where children must be unobserved.
|
||||
depth: u32,
|
||||
},
|
||||
/// A response refers to a different interface than its request.
|
||||
InterfaceResponseMismatch {
|
||||
@@ -226,10 +224,8 @@ impl Error for ValidationError {}
|
||||
/// One core interaction whose failure codes form a closed set.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ProtocolInteraction {
|
||||
/// Fetch one object by its Worldspace path.
|
||||
Fetch,
|
||||
/// Fetch a bounded tree of objects.
|
||||
FetchTree,
|
||||
/// Observe one object and its indexable descendants to a requested depth.
|
||||
Observe,
|
||||
/// Fetch one interface descriptor by its opaque reference.
|
||||
FetchInterface,
|
||||
/// Invoke one operation on a path-targeted object.
|
||||
@@ -315,11 +311,11 @@ impl ProtocolErrorCode {
|
||||
OperationNotFound, OperationOutcomeUnknown, PermissionDenied, ResourceLimitExceeded,
|
||||
ValidatorMismatch, ValidatorRequired,
|
||||
};
|
||||
use ProtocolInteraction::{CallOperation, Fetch, FetchInterface, FetchTree};
|
||||
use ProtocolInteraction::{CallOperation, FetchInterface, Observe};
|
||||
|
||||
match (interaction, self) {
|
||||
(_, InvalidRequest | ResourceLimitExceeded | Internal) => true,
|
||||
(Fetch | FetchTree, NotFound) => true,
|
||||
(Observe, NotFound) => true,
|
||||
(FetchInterface, InterfaceNotFound) => true,
|
||||
(
|
||||
CallOperation,
|
||||
|
||||
@@ -11,8 +11,7 @@
|
||||
//! | Interface Descriptor | [`InterfaceDescriptor`] |
|
||||
//! | Named declaration and all schema expressions | [`TypeDeclaration`] / [`TypeExpr`] |
|
||||
//! | Schema-neutral primitive/composite value | [`Value`] |
|
||||
//! | `fetch` | [`FetchRequest`] / [`FetchResponse`] |
|
||||
//! | `fetch_tree` | [`FetchTreeRequest`] / [`FetchTreeResponse`] |
|
||||
//! | `observe` | [`ObserveRequest`] / [`ObserveResponse`] |
|
||||
//! | `fetch_interface` | [`FetchInterfaceRequest`] / [`FetchInterfaceResponse`] |
|
||||
//! | Object and interface call preconditions | [`Target`] / [`InterfaceTarget`] |
|
||||
//! | `call_operation` | [`CallOperationRequest`] / [`CallOperationResponse`] |
|
||||
@@ -53,10 +52,9 @@ pub use error::{
|
||||
};
|
||||
pub use model::{
|
||||
CallOperationRequest, CallOperationResponse, Documentation, EnumCase, FetchInterfaceRequest,
|
||||
FetchInterfaceResponse, FetchRequest, FetchResponse, FetchTreeRequest, FetchTreeResponse,
|
||||
FieldDeclaration, InterfaceDescriptor, InterfaceTarget, Object, ObjectTreeNode,
|
||||
OperationDeclaration, ParameterDeclaration, ProtocolResult, ReturnDeclaration, Target,
|
||||
TypeDeclaration, TypeExpr, UnionCase, Value,
|
||||
FetchInterfaceResponse, FieldDeclaration, InterfaceDescriptor, InterfaceTarget, Object,
|
||||
ObjectObservation, ObserveRequest, ObserveResponse, OperationDeclaration, ParameterDeclaration,
|
||||
ProtocolResult, ReturnDeclaration, Target, TypeDeclaration, TypeExpr, UnionCase, Value,
|
||||
};
|
||||
|
||||
/// Validates a canonical absolute Worldspace path.
|
||||
|
||||
@@ -218,44 +218,26 @@ impl Value {
|
||||
}
|
||||
}
|
||||
|
||||
/// Request for the object currently published at one path.
|
||||
/// Request for the object currently published at one path and its indexable descendants.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct FetchRequest {
|
||||
/// Canonical absolute Worldspace path.
|
||||
pub struct ObserveRequest {
|
||||
/// Canonical absolute path of the observation root.
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
/// 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.
|
||||
/// Maximum observed 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.
|
||||
/// One node in an object observation.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ObjectTreeNode {
|
||||
/// Object observed at this tree position.
|
||||
pub struct ObjectObservation {
|
||||
/// Object observed at this Worldspace position.
|
||||
pub object: Object,
|
||||
/// Direct children in host-defined traversal order.
|
||||
pub children: Vec<ObjectTreeNode>,
|
||||
/// Complete direct children when observed, or `None` at the requested depth boundary.
|
||||
pub children: Option<Vec<ObjectObservation>>,
|
||||
}
|
||||
|
||||
/// 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,
|
||||
}
|
||||
/// Successful `observe` response.
|
||||
pub type ObserveResponse = ObjectObservation;
|
||||
|
||||
/// Request for the current descriptor of one opaque interface reference.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@@ -372,35 +354,18 @@ impl InterfaceDescriptor {
|
||||
}
|
||||
}
|
||||
|
||||
impl FetchRequest {
|
||||
impl ObserveRequest {
|
||||
/// Validates the request path.
|
||||
pub fn validate(&self) -> Result<(), ValidationError> {
|
||||
crate::validation::validate_path(&self.path)
|
||||
}
|
||||
|
||||
/// Validates an object returned for this request.
|
||||
/// Validates observation shape, names, and requested depth.
|
||||
///
|
||||
/// Failure means the Client received an invalid success response; it is not a
|
||||
/// Host-returned [`crate::ProtocolErrorCode`].
|
||||
pub fn validate_response(&self, response: &FetchResponse) -> Result<(), ValidationError> {
|
||||
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`.
|
||||
///
|
||||
/// Failure means the Client received an invalid success response; it is not a
|
||||
/// Host-returned [`crate::ProtocolErrorCode`].
|
||||
pub fn validate_for(&self, request: &FetchTreeRequest) -> Result<(), ValidationError> {
|
||||
crate::validation::validate_fetch_tree_response(self, request)
|
||||
pub fn validate_response(&self, response: &ObserveResponse) -> Result<(), ValidationError> {
|
||||
crate::validation::validate_observe_response(response, self)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||
|
||||
use crate::{
|
||||
CallOperationRequest, FetchInterfaceRequest, FetchInterfaceResponse, FetchTreeRequest,
|
||||
FetchTreeResponse, InterfaceDescriptor, MAX_SAFE_INTEGER, MIN_SAFE_INTEGER, NameNamespace,
|
||||
Object, ObjectTreeNode, PathSegment, TypeDeclaration, TypeExpr, UNION_CASE_FIELD,
|
||||
UNION_VALUE_FIELD, ValidationError, ValidationErrorKind, Value, ValueKind,
|
||||
CallOperationRequest, FetchInterfaceRequest, FetchInterfaceResponse, InterfaceDescriptor,
|
||||
MAX_SAFE_INTEGER, MIN_SAFE_INTEGER, NameNamespace, Object, ObjectObservation, ObserveRequest,
|
||||
PathSegment, TypeDeclaration, TypeExpr, UNION_CASE_FIELD, UNION_VALUE_FIELD, ValidationError,
|
||||
ValidationErrorKind, Value, ValueKind,
|
||||
};
|
||||
|
||||
pub(crate) fn validate_object(object: &Object) -> Result<(), ValidationError> {
|
||||
@@ -604,60 +604,38 @@ fn find_operation<'a>(
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn validate_fetch_tree_request(
|
||||
request: &FetchTreeRequest,
|
||||
) -> Result<(), ValidationError> {
|
||||
validate_path(&request.path)?;
|
||||
if request.limit == 0 {
|
||||
Err(ValidationError::new(ValidationErrorKind::ZeroTreeLimit))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn validate_fetch_tree_response(
|
||||
response: &FetchTreeResponse,
|
||||
request: &FetchTreeRequest,
|
||||
pub(crate) fn validate_observe_response(
|
||||
response: &ObjectObservation,
|
||||
request: &ObserveRequest,
|
||||
) -> Result<(), ValidationError> {
|
||||
request.validate()?;
|
||||
let mut node_count = 0_u32;
|
||||
validate_tree_node(
|
||||
&response.root,
|
||||
&request.path,
|
||||
0,
|
||||
request.depth,
|
||||
request.limit,
|
||||
&mut node_count,
|
||||
)
|
||||
validate_observation_node(response, &request.path, 0, request.depth)
|
||||
}
|
||||
|
||||
fn validate_tree_node(
|
||||
node: &ObjectTreeNode,
|
||||
fn validate_observation_node(
|
||||
node: &ObjectObservation,
|
||||
path: &str,
|
||||
depth: u32,
|
||||
maximum_depth: u32,
|
||||
limit: u32,
|
||||
node_count: &mut u32,
|
||||
) -> Result<(), ValidationError> {
|
||||
if depth > maximum_depth {
|
||||
return Err(
|
||||
ValidationError::new(ValidationErrorKind::TreeDepthExceeded {
|
||||
maximum: maximum_depth,
|
||||
})
|
||||
.prepend(PathSegment::Object(path.to_owned())),
|
||||
);
|
||||
}
|
||||
*node_count = node_count.saturating_add(1);
|
||||
if *node_count > limit {
|
||||
return Err(
|
||||
ValidationError::new(ValidationErrorKind::TreeLimitExceeded { limit })
|
||||
.prepend(PathSegment::Object(path.to_owned())),
|
||||
);
|
||||
node.object.validate_at_path(path)?;
|
||||
|
||||
if depth == maximum_depth {
|
||||
if node.children.is_some() {
|
||||
return Err(
|
||||
ValidationError::new(ValidationErrorKind::ChildrenAtDepthBoundary { depth })
|
||||
.prepend(PathSegment::Object(path.to_owned())),
|
||||
);
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
node.object.validate_at_path(path)?;
|
||||
let children = node.children.as_ref().ok_or_else(|| {
|
||||
ValidationError::new(ValidationErrorKind::ChildrenMissingBeforeDepth { depth })
|
||||
.prepend(PathSegment::Object(path.to_owned()))
|
||||
})?;
|
||||
let mut names = HashSet::new();
|
||||
for child in &node.children {
|
||||
for child in children {
|
||||
validate_object_name(&child.object.name, false)
|
||||
.map_err(|error| error.prepend(PathSegment::Child(child.object.name.clone())))?;
|
||||
if !names.insert(child.object.name.as_str()) {
|
||||
@@ -673,14 +651,7 @@ fn validate_tree_node(
|
||||
} else {
|
||||
format!("{path}/{}", child.object.name)
|
||||
};
|
||||
validate_tree_node(
|
||||
child,
|
||||
&child_path,
|
||||
depth.saturating_add(1),
|
||||
maximum_depth,
|
||||
limit,
|
||||
node_count,
|
||||
)?;
|
||||
validate_observation_node(child, &child_path, depth + 1, maximum_depth)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user