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(())
|
||||
}
|
||||
|
||||
@@ -2,11 +2,11 @@ use std::collections::BTreeMap;
|
||||
|
||||
use wip_protocol::{
|
||||
CallOperationRequest, CallOperationResponse, EnumCase, FetchInterfaceRequest,
|
||||
FetchInterfaceResponse, FetchRequest, FetchTreeRequest, FetchTreeResponse, FieldDeclaration,
|
||||
INTERFACE_FORMAT_V1, InterfaceDescriptor, InterfaceTarget, MAX_SAFE_INTEGER, NameNamespace,
|
||||
Object, ObjectTreeNode, OperationDeclaration, ParameterDeclaration, PathSegment, ProtocolError,
|
||||
ProtocolErrorCode, ProtocolInteraction, ProtocolResult, ReturnDeclaration, Target,
|
||||
TypeDeclaration, TypeExpr, UnionCase, ValidationErrorKind, Value, ValueKind,
|
||||
FetchInterfaceResponse, FieldDeclaration, INTERFACE_FORMAT_V1, InterfaceDescriptor,
|
||||
InterfaceTarget, MAX_SAFE_INTEGER, NameNamespace, Object, ObjectObservation, ObserveRequest,
|
||||
OperationDeclaration, ParameterDeclaration, PathSegment, ProtocolError, ProtocolErrorCode,
|
||||
ProtocolInteraction, ProtocolResult, ReturnDeclaration, Target, TypeDeclaration, TypeExpr,
|
||||
UnionCase, ValidationErrorKind, Value, ValueKind,
|
||||
};
|
||||
|
||||
fn record(entries: impl IntoIterator<Item = (&'static str, Value)>) -> Value {
|
||||
@@ -617,13 +617,26 @@ fn calls_validate_canonical_paths_named_arguments_and_unit_results() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fetch_and_fetch_interface_dtos_validate_request_response_correspondence() {
|
||||
let fetch = FetchRequest {
|
||||
fn observe_and_fetch_interface_dtos_validate_request_response_correspondence() {
|
||||
let observe = ObserveRequest {
|
||||
path: "/articles/one".into(),
|
||||
depth: 0,
|
||||
};
|
||||
fetch.validate().unwrap();
|
||||
fetch.validate_response(&object("one")).unwrap();
|
||||
assert!(fetch.validate_response(&object("two")).is_err());
|
||||
observe.validate().unwrap();
|
||||
observe
|
||||
.validate_response(&ObjectObservation {
|
||||
object: object("one"),
|
||||
children: None,
|
||||
})
|
||||
.unwrap();
|
||||
assert!(
|
||||
observe
|
||||
.validate_response(&ObjectObservation {
|
||||
object: object("two"),
|
||||
children: None,
|
||||
})
|
||||
.is_err()
|
||||
);
|
||||
|
||||
for invalid in [
|
||||
"",
|
||||
@@ -634,8 +647,9 @@ fn fetch_and_fetch_interface_dtos_validate_request_response_correspondence() {
|
||||
"/a/../b",
|
||||
] {
|
||||
assert!(matches!(
|
||||
FetchRequest {
|
||||
path: invalid.into()
|
||||
ObserveRequest {
|
||||
path: invalid.into(),
|
||||
depth: 0,
|
||||
}
|
||||
.validate()
|
||||
.unwrap_err()
|
||||
@@ -666,61 +680,51 @@ fn fetch_and_fetch_interface_dtos_validate_request_response_correspondence() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fetch_tree_dtos_validate_names_depth_limit_and_siblings() {
|
||||
let request = FetchTreeRequest {
|
||||
fn observe_response_validates_complete_children_and_depth_boundary() {
|
||||
let request = ObserveRequest {
|
||||
path: "/articles".into(),
|
||||
depth: 1,
|
||||
limit: 3,
|
||||
};
|
||||
let leaf = |name: &str| ObjectTreeNode {
|
||||
let leaf = |name: &str| ObjectObservation {
|
||||
object: object(name),
|
||||
children: vec![],
|
||||
children: None,
|
||||
};
|
||||
let response = FetchTreeResponse {
|
||||
root: ObjectTreeNode {
|
||||
object: object("articles"),
|
||||
children: vec![leaf("one"), leaf("two")],
|
||||
},
|
||||
truncated: false,
|
||||
let response = ObjectObservation {
|
||||
object: object("articles"),
|
||||
children: Some(vec![leaf("one"), leaf("two")]),
|
||||
};
|
||||
response.validate_for(&request).unwrap();
|
||||
request.validate_response(&response).unwrap();
|
||||
|
||||
let zero = FetchTreeRequest {
|
||||
limit: 0,
|
||||
..request.clone()
|
||||
let empty = ObjectObservation {
|
||||
object: object("articles"),
|
||||
children: Some(vec![]),
|
||||
};
|
||||
assert_eq!(
|
||||
zero.validate().unwrap_err().kind,
|
||||
ValidationErrorKind::ZeroTreeLimit
|
||||
);
|
||||
request.validate_response(&empty).unwrap();
|
||||
|
||||
let shallow = FetchTreeRequest {
|
||||
let missing = ObjectObservation {
|
||||
object: object("articles"),
|
||||
children: None,
|
||||
};
|
||||
assert!(matches!(
|
||||
request.validate_response(&missing).unwrap_err().kind,
|
||||
ValidationErrorKind::ChildrenMissingBeforeDepth { depth: 0 }
|
||||
));
|
||||
|
||||
let boundary = ObserveRequest {
|
||||
depth: 0,
|
||||
..request.clone()
|
||||
};
|
||||
assert!(matches!(
|
||||
response.validate_for(&shallow).unwrap_err().kind,
|
||||
ValidationErrorKind::TreeDepthExceeded { .. }
|
||||
boundary.validate_response(&response).unwrap_err().kind,
|
||||
ValidationErrorKind::ChildrenAtDepthBoundary { depth: 0 }
|
||||
));
|
||||
|
||||
let small = FetchTreeRequest {
|
||||
limit: 2,
|
||||
..request.clone()
|
||||
let duplicate = ObjectObservation {
|
||||
object: object("articles"),
|
||||
children: Some(vec![leaf("one"), leaf("one")]),
|
||||
};
|
||||
assert!(matches!(
|
||||
response.validate_for(&small).unwrap_err().kind,
|
||||
ValidationErrorKind::TreeLimitExceeded { .. }
|
||||
));
|
||||
|
||||
let duplicate = FetchTreeResponse {
|
||||
root: ObjectTreeNode {
|
||||
object: object("articles"),
|
||||
children: vec![leaf("one"), leaf("one")],
|
||||
},
|
||||
truncated: false,
|
||||
};
|
||||
assert!(matches!(
|
||||
duplicate.validate_for(&request).unwrap_err().kind,
|
||||
request.validate_response(&duplicate).unwrap_err().kind,
|
||||
ValidationErrorKind::DuplicateName {
|
||||
namespace: NameNamespace::ChildObject,
|
||||
..
|
||||
@@ -778,22 +782,9 @@ fn assert_allowed_codes(interaction: ProtocolInteraction, expected: &[ProtocolEr
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fetch_error_codes_are_exhaustive() {
|
||||
fn observe_error_codes_are_exhaustive() {
|
||||
assert_allowed_codes(
|
||||
ProtocolInteraction::Fetch,
|
||||
&[
|
||||
ProtocolErrorCode::InvalidRequest,
|
||||
ProtocolErrorCode::NotFound,
|
||||
ProtocolErrorCode::ResourceLimitExceeded,
|
||||
ProtocolErrorCode::Internal,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fetch_tree_error_codes_are_exhaustive() {
|
||||
assert_allowed_codes(
|
||||
ProtocolInteraction::FetchTree,
|
||||
ProtocolInteraction::Observe,
|
||||
&[
|
||||
ProtocolErrorCode::InvalidRequest,
|
||||
ProtocolErrorCode::NotFound,
|
||||
@@ -840,11 +831,18 @@ fn call_operation_error_codes_are_exhaustive() {
|
||||
|
||||
#[test]
|
||||
fn invalid_success_payloads_remain_client_local_validation_failures() {
|
||||
let fetch = FetchRequest {
|
||||
let observe = ObserveRequest {
|
||||
path: "/articles/one".into(),
|
||||
depth: 0,
|
||||
};
|
||||
assert!(matches!(
|
||||
fetch.validate_response(&object("other")).unwrap_err().kind,
|
||||
observe
|
||||
.validate_response(&ObjectObservation {
|
||||
object: object("other"),
|
||||
children: None,
|
||||
})
|
||||
.unwrap_err()
|
||||
.kind,
|
||||
ValidationErrorKind::ObjectNameMismatch { .. }
|
||||
));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user