feat(protocol): add logical model and validation

This commit is contained in:
2026-09-24 05:29:20 +09:00
parent ba4ccf1e67
commit 742442f174
8 changed files with 1818 additions and 0 deletions
Generated
+7
View File
@@ -2,6 +2,13 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 4 version = 4
[[package]]
name = "wip-protocol"
version = "0.1.0"
[[package]] [[package]]
name = "wip-rs" name = "wip-rs"
version = "0.1.0" version = "0.1.0"
dependencies = [
"wip-protocol",
]
+5
View File
@@ -1,6 +1,11 @@
[workspace]
members = [".", "crates/wip-protocol"]
resolver = "3"
[package] [package]
name = "wip-rs" name = "wip-rs"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
wip-protocol = { path = "crates/wip-protocol" }
+8
View File
@@ -0,0 +1,8 @@
[package]
name = "wip-protocol"
version = "0.1.0"
edition = "2024"
description = "Transport-independent logical model and validation for WIP"
license = "MIT OR Apache-2.0"
[dependencies]
+240
View File
@@ -0,0 +1,240 @@
use std::error::Error;
use std::fmt::{self, Display, Formatter};
/// Namespace in which a declaration name must be unique.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NameNamespace {
/// Named types in an interface descriptor.
TypeDeclaration,
/// Operations in an interface descriptor.
Operation,
/// Fields in a record declaration.
Field,
/// Parameters in an operation declaration.
Parameter,
/// Variants in an enum declaration.
EnumVariant,
/// Variants in a union declaration.
UnionVariant,
/// Fields supplied in a record value.
RecordValueField,
/// 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.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ValueKind {
/// String value.
String,
/// Boolean value.
Boolean,
/// Integer value.
Integer,
/// Floating-point number value.
Number,
/// Byte sequence.
Bytes,
/// List value.
List,
/// Key/value entry.
Entry,
/// Record value.
Record,
/// Enum value.
Enum,
/// Tagged-union value.
Union,
/// Logical JSON value.
Json,
}
/// A typed segment identifying a declaration or value location.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PathSegment {
/// Interface descriptor.
Interface(String),
/// Named type declaration.
Declaration(String),
/// Operation declaration or call.
Operation(String),
/// Record field declaration or value.
Field(String),
/// Operation parameter declaration or argument.
Parameter(String),
/// Enum or union variant.
Variant(String),
/// List or JSON array index.
Index(usize),
/// Entry key.
EntryKey,
/// Entry value.
EntryValue,
/// Operation return value.
Return,
/// JSON object member.
JsonMember(String),
}
/// Machine-readable reason why protocol validation failed.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ValidationErrorKind {
/// A name occurred more than once in one namespace.
DuplicateName {
/// Namespace containing the duplicate.
namespace: NameNamespace,
/// Duplicate name.
name: String,
},
/// A named type was not declared in the descriptor.
UnknownType {
/// Missing descriptor-local type name.
name: String,
},
/// Named declarations form a recursive cycle.
RecursiveType {
/// Cycle in traversal order, with the starting name repeated last.
cycle: Vec<String>,
},
/// A value has the wrong structural kind.
TypeMismatch {
/// Expected kind.
expected: ValueKind,
/// Actual kind.
actual: ValueKind,
},
/// A record value contains an undeclared field.
UnknownField {
/// Unknown field name.
name: String,
},
/// A record value omitted a required field.
MissingField {
/// Missing field name.
name: String,
},
/// An enum or union value selected an undeclared variant.
UnknownVariant {
/// Unknown variant name.
name: String,
},
/// An operation call contains an undeclared argument.
UnknownParameter {
/// Unknown parameter name.
name: String,
},
/// An operation call omitted a required argument.
MissingParameter {
/// Missing parameter name.
name: String,
},
/// An integer is outside the JavaScript-lossless range.
IntegerOutOfRange {
/// Rejected integer.
value: i64,
},
/// A numeric value is NaN or infinite.
NonFiniteNumber,
/// An object's ordered interface set contains a duplicate.
DuplicateInterface {
/// Duplicate interface name.
name: String,
},
/// An observation omits a descriptor named by its object.
MissingInterfaceDescriptor {
/// Missing interface name.
name: String,
},
/// An observation contains a descriptor not named by its object.
UnexpectedInterfaceDescriptor {
/// Unexpected interface name.
name: String,
},
/// A call targets an interface other than the descriptor being used.
InterfaceMismatch {
/// Descriptor interface name.
expected: String,
/// Call target interface name.
actual: String,
},
/// An operation is not declared by the interface.
UnknownOperation {
/// Unknown operation name.
name: 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.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ValidationError {
/// Path from the validation root to the failure.
pub path: Vec<PathSegment>,
/// Machine-readable failure reason.
pub kind: ValidationErrorKind,
}
impl ValidationError {
pub(crate) fn new(kind: ValidationErrorKind) -> Self {
Self {
path: Vec::new(),
kind,
}
}
pub(crate) fn prepend(mut self, segment: PathSegment) -> Self {
self.path.insert(0, segment);
self
}
}
impl Display for ValidationError {
fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
if self.path.is_empty() {
write!(formatter, "protocol validation failed: {:?}", self.kind)
} else {
write!(
formatter,
"protocol validation failed at {:?}: {:?}",
self.path, self.kind
)
}
}
}
impl Error for ValidationError {}
/// Stable category of an error returned by a protocol host runtime.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProtocolErrorCategory {
/// The request is structurally invalid.
InvalidRequest,
/// The target object is not published.
ObjectNotFound,
/// The target interface is not implemented by the object.
InterfaceNotFound,
/// The target operation is not declared by the interface.
OperationNotFound,
/// Supplied operation arguments are invalid.
InvalidArguments,
/// The implementation rejected the call for a domain reason.
OperationFailed,
/// The host encountered an internal failure.
Internal,
}
/// Transport-independent protocol failure returned by a host runtime.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProtocolError {
/// Stable programmatic category.
pub category: ProtocolErrorCategory,
/// Human-readable diagnostic. Consumers must not branch on this text.
pub message: String,
}
+30
View File
@@ -0,0 +1,30 @@
//! Transport-independent logical types for the Web Interface Protocol (WIP).
//!
//! This crate deliberately models interface declarations, published objects,
//! operation calls, and operation results without assigning them HTTP routes,
//! methods, headers, or JSON envelopes. A transport can encode these values,
//! but the encoding is outside this crate's contract.
#![deny(missing_docs)]
mod error;
mod model;
mod validation;
pub use error::{
NameNamespace, PathSegment, ProtocolError, ProtocolErrorCategory, ValidationError,
ValidationErrorKind, ValueKind,
};
pub use model::{
Documentation, EnumDeclaration, EnumVariantDeclaration, FieldDeclaration, InterfaceDescriptor,
InterfaceObservation, JsonMember, JsonValue, NamedValue, Object, ObjectId, OperationCall,
OperationDeclaration, OperationOutcome, OperationResult, OperationTarget, ParameterDeclaration,
RecordDeclaration, ReturnDeclaration, TypeDeclaration, TypeExpr, UnionDeclaration,
UnionVariantDeclaration, Value,
};
/// Largest integer that is exactly interoperable with a JavaScript `Number`.
pub const MAX_SAFE_INTEGER: i64 = 9_007_199_254_740_991;
/// Smallest integer that is exactly interoperable with a JavaScript `Number`.
pub const MIN_SAFE_INTEGER: i64 = -MAX_SAFE_INTEGER;
+383
View File
@@ -0,0 +1,383 @@
use crate::{ProtocolError, ValidationError};
/// Human-readable documentation attached to a protocol declaration.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct Documentation {
/// A short, self-contained description.
pub summary: String,
/// Optional extended documentation.
pub details: Option<String>,
}
/// Stable logical identity of a published object.
#[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
/// publication to reject duplicate interface names.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Object {
/// Logical object identity.
pub id: ObjectId,
/// Interface names in publication order.
pub interfaces: Vec<String>,
}
/// A complete logical observation of an object and its interface descriptors.
#[derive(Debug, Clone, PartialEq)]
pub struct InterfaceObservation {
/// Observed object.
pub object: Object,
/// Descriptors corresponding to `object.interfaces`.
pub descriptors: Vec<InterfaceDescriptor>,
}
/// Transport-independent description of one interface.
#[derive(Debug, Clone, PartialEq)]
pub struct InterfaceDescriptor {
/// Interface name.
pub name: String,
/// Optional interface documentation.
pub documentation: Option<Documentation>,
/// Named type declarations local to this descriptor.
pub declarations: Vec<TypeDeclaration>,
/// Operations exposed by this interface.
pub operations: Vec<OperationDeclaration>,
}
/// A named type declaration local to an interface descriptor.
#[derive(Debug, Clone, PartialEq)]
pub enum 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.
pub name: String,
/// Optional record documentation.
pub documentation: Option<Documentation>,
/// Fields in declaration order.
pub fields: Vec<FieldDeclaration>,
}
/// Declaration of a record field.
#[derive(Debug, Clone, PartialEq)]
pub struct FieldDeclaration {
/// Field name.
pub name: String,
/// Optional field documentation.
pub documentation: Option<Documentation>,
/// Field value type.
pub ty: TypeExpr,
/// Whether the field may be omitted.
///
/// Optionality represents absence, not a `null` value.
pub optional: bool,
}
/// Declaration of an enumeration.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EnumDeclaration {
/// Descriptor-local type name.
pub name: String,
/// Optional enum documentation.
pub documentation: Option<Documentation>,
/// Allowed variants in declaration order.
pub variants: Vec<EnumVariantDeclaration>,
}
/// Declaration of one enumeration variant.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EnumVariantDeclaration {
/// Variant name.
pub name: String,
/// Optional variant documentation.
pub documentation: Option<Documentation>,
}
/// 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.
#[derive(Debug, Clone, PartialEq)]
pub struct OperationDeclaration {
/// Operation name, unique within its interface.
pub name: String,
/// Optional operation documentation.
pub documentation: Option<Documentation>,
/// Parameters in declaration order.
pub parameters: Vec<ParameterDeclaration>,
/// Return declaration, or `None` for an operation with no return value.
pub returns: Option<ReturnDeclaration>,
}
/// Declaration of one operation parameter.
#[derive(Debug, Clone, PartialEq)]
pub struct ParameterDeclaration {
/// Parameter name.
pub name: String,
/// Optional parameter documentation.
pub documentation: Option<Documentation>,
/// Parameter value type.
pub ty: TypeExpr,
/// Whether the parameter may be omitted.
///
/// Optionality represents absence, not a `null` value.
pub optional: bool,
}
/// Declaration of an operation return value.
#[derive(Debug, Clone, PartialEq)]
pub struct ReturnDeclaration {
/// Optional return-value documentation.
pub documentation: Option<Documentation>,
/// Return value type.
pub ty: TypeExpr,
}
/// A logical protocol value.
///
/// There is no general `Null` variant: optional record fields and operation
/// parameters are omitted. JSON null remains available inside [`Value::Json`].
#[derive(Debug, Clone, PartialEq)]
pub enum Value {
/// A UTF-8 string.
String(String),
/// A boolean.
Boolean(bool),
/// An integer, validated against the JavaScript safe integer range.
Integer(i64),
/// A number, validated to be finite.
Number(f64),
/// An opaque byte sequence.
Bytes(Vec<u8>),
/// An ordered sequence.
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 {
/// Returns the structural kind used by validation diagnostics.
#[must_use]
pub fn kind(&self) -> crate::ValueKind {
match self {
Self::String(_) => crate::ValueKind::String,
Self::Boolean(_) => crate::ValueKind::Boolean,
Self::Integer(_) => crate::ValueKind::Integer,
Self::Number(_) => crate::ValueKind::Number,
Self::Bytes(_) => crate::ValueKind::Bytes,
Self::List(_) => crate::ValueKind::List,
Self::Entry { .. } => crate::ValueKind::Entry,
Self::Record(_) => crate::ValueKind::Record,
Self::Enum(_) => crate::ValueKind::Enum,
Self::Union { .. } => crate::ValueKind::Union,
Self::Json(_) => crate::ValueKind::Json,
}
}
}
/// A named value used by records and operation arguments.
#[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)]
pub struct OperationTarget {
/// Target object.
pub object: ObjectId,
/// Target interface name.
pub interface: String,
/// Target operation name.
pub operation: String,
}
/// A transport-independent operation call.
#[derive(Debug, Clone, PartialEq)]
pub struct OperationCall {
/// Logical call target.
pub target: OperationTarget,
/// Named arguments. Optional arguments are represented by omission.
pub arguments: Vec<NamedValue>,
}
/// Successful or failed logical outcome of an operation.
#[derive(Debug, Clone, PartialEq)]
pub enum OperationOutcome {
/// Operation returned. `None` represents a declared no-return operation.
Returned(Option<Value>),
/// Operation failed with a protocol-level error.
Failed(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 {
/// Validates ordered-set interface semantics.
pub fn validate(&self) -> Result<(), ValidationError> {
crate::validation::validate_object(self)
}
}
impl InterfaceObservation {
/// Validates the object, descriptor structure, and their correspondence.
pub fn validate(&self) -> Result<(), ValidationError> {
crate::validation::validate_observation(self)
}
}
impl InterfaceDescriptor {
/// Validates declaration structure, references, and absence of recursion.
pub fn validate(&self) -> Result<(), ValidationError> {
crate::validation::validate_descriptor(self)
}
/// Validates a value against a type expression in this descriptor.
pub fn validate_value(&self, ty: &TypeExpr, value: &Value) -> Result<(), ValidationError> {
crate::validation::validate_root_value(self, ty, value)
}
/// Validates a call target and arguments against this descriptor.
pub fn validate_call(&self, call: &OperationCall) -> Result<(), ValidationError> {
crate::validation::validate_call(self, call)
}
/// Validates an operation's returned value against this descriptor.
pub fn validate_return(
&self,
operation: &str,
value: Option<&Value>,
) -> Result<(), ValidationError> {
crate::validation::validate_return(self, operation, value)
}
}
+620
View File
@@ -0,0 +1,620 @@
use std::collections::{HashMap, HashSet};
use crate::{
InterfaceDescriptor, InterfaceObservation, JsonValue, MAX_SAFE_INTEGER, MIN_SAFE_INTEGER,
NameNamespace, Object, OperationCall, PathSegment, TypeDeclaration, TypeExpr, ValidationError,
ValidationErrorKind, Value, ValueKind,
};
pub(crate) fn validate_object(object: &Object) -> Result<(), ValidationError> {
let mut seen = HashSet::new();
for interface in &object.interfaces {
if !seen.insert(interface.as_str()) {
return Err(
ValidationError::new(ValidationErrorKind::DuplicateInterface {
name: interface.clone(),
})
.prepend(PathSegment::Interface(interface.clone())),
);
}
}
Ok(())
}
pub(crate) fn validate_observation(
observation: &InterfaceObservation,
) -> Result<(), ValidationError> {
observation.object.validate()?;
let mut descriptors = HashMap::new();
for descriptor in &observation.descriptors {
if descriptors
.insert(descriptor.name.as_str(), descriptor)
.is_some()
{
return Err(ValidationError::new(ValidationErrorKind::DuplicateName {
namespace: NameNamespace::InterfaceDescriptor,
name: descriptor.name.clone(),
})
.prepend(PathSegment::Interface(descriptor.name.clone())));
}
descriptor.validate()?;
}
for interface in &observation.object.interfaces {
if !descriptors.contains_key(interface.as_str()) {
return Err(
ValidationError::new(ValidationErrorKind::MissingInterfaceDescriptor {
name: interface.clone(),
})
.prepend(PathSegment::Interface(interface.clone())),
);
}
}
let published: HashSet<&str> = observation
.object
.interfaces
.iter()
.map(String::as_str)
.collect();
for descriptor in &observation.descriptors {
if !published.contains(descriptor.name.as_str()) {
return Err(
ValidationError::new(ValidationErrorKind::UnexpectedInterfaceDescriptor {
name: descriptor.name.clone(),
})
.prepend(PathSegment::Interface(descriptor.name.clone())),
);
}
}
Ok(())
}
pub(crate) fn validate_descriptor(descriptor: &InterfaceDescriptor) -> Result<(), ValidationError> {
let declarations = declaration_map(descriptor)?;
for declaration in &descriptor.declarations {
validate_declaration(declaration, &declarations).map_err(|error| {
error
.prepend(PathSegment::Declaration(declaration.name().to_owned()))
.prepend(PathSegment::Interface(descriptor.name.clone()))
})?;
}
let mut operation_names = HashSet::new();
for operation in &descriptor.operations {
if !operation_names.insert(operation.name.as_str()) {
return Err(ValidationError::new(ValidationErrorKind::DuplicateName {
namespace: NameNamespace::Operation,
name: operation.name.clone(),
})
.prepend(PathSegment::Operation(operation.name.clone()))
.prepend(PathSegment::Interface(descriptor.name.clone())));
}
let mut parameter_names = HashSet::new();
for parameter in &operation.parameters {
if !parameter_names.insert(parameter.name.as_str()) {
return Err(ValidationError::new(ValidationErrorKind::DuplicateName {
namespace: NameNamespace::Parameter,
name: parameter.name.clone(),
})
.prepend(PathSegment::Parameter(parameter.name.clone()))
.prepend(PathSegment::Operation(operation.name.clone()))
.prepend(PathSegment::Interface(descriptor.name.clone())));
}
validate_type_expr(&parameter.ty, &declarations).map_err(|error| {
error
.prepend(PathSegment::Parameter(parameter.name.clone()))
.prepend(PathSegment::Operation(operation.name.clone()))
.prepend(PathSegment::Interface(descriptor.name.clone()))
})?;
}
if let Some(returns) = &operation.returns {
validate_type_expr(&returns.ty, &declarations).map_err(|error| {
error
.prepend(PathSegment::Return)
.prepend(PathSegment::Operation(operation.name.clone()))
.prepend(PathSegment::Interface(descriptor.name.clone()))
})?;
}
}
validate_no_cycles(descriptor, &declarations)
}
fn declaration_map(
descriptor: &InterfaceDescriptor,
) -> Result<HashMap<&str, &TypeDeclaration>, ValidationError> {
let mut declarations = HashMap::new();
for declaration in &descriptor.declarations {
if declarations
.insert(declaration.name(), declaration)
.is_some()
{
return Err(ValidationError::new(ValidationErrorKind::DuplicateName {
namespace: NameNamespace::TypeDeclaration,
name: declaration.name().to_owned(),
})
.prepend(PathSegment::Declaration(declaration.name().to_owned()))
.prepend(PathSegment::Interface(descriptor.name.clone())));
}
}
Ok(declarations)
}
fn validate_declaration(
declaration: &TypeDeclaration,
declarations: &HashMap<&str, &TypeDeclaration>,
) -> Result<(), ValidationError> {
match declaration {
TypeDeclaration::Record(record) => {
let mut names = HashSet::new();
for field in &record.fields {
if !names.insert(field.name.as_str()) {
return Err(ValidationError::new(ValidationErrorKind::DuplicateName {
namespace: NameNamespace::Field,
name: field.name.clone(),
})
.prepend(PathSegment::Field(field.name.clone())));
}
validate_type_expr(&field.ty, declarations)
.map_err(|error| error.prepend(PathSegment::Field(field.name.clone())))?;
}
}
TypeDeclaration::Enum(enumeration) => {
let mut names = HashSet::new();
for variant in &enumeration.variants {
if !names.insert(variant.name.as_str()) {
return Err(ValidationError::new(ValidationErrorKind::DuplicateName {
namespace: NameNamespace::EnumVariant,
name: variant.name.clone(),
})
.prepend(PathSegment::Variant(variant.name.clone())));
}
}
}
TypeDeclaration::Union(union) => {
let mut names = HashSet::new();
for variant in &union.variants {
if !names.insert(variant.name.as_str()) {
return Err(ValidationError::new(ValidationErrorKind::DuplicateName {
namespace: NameNamespace::UnionVariant,
name: variant.name.clone(),
})
.prepend(PathSegment::Variant(variant.name.clone())));
}
validate_type_expr(&variant.ty, declarations)
.map_err(|error| error.prepend(PathSegment::Variant(variant.name.clone())))?;
}
}
}
Ok(())
}
fn validate_type_expr(
ty: &TypeExpr,
declarations: &HashMap<&str, &TypeDeclaration>,
) -> Result<(), ValidationError> {
match ty {
TypeExpr::List(item) => validate_type_expr(item, declarations),
TypeExpr::Entry { key, value } => {
validate_type_expr(key, declarations)
.map_err(|error| error.prepend(PathSegment::EntryKey))?;
validate_type_expr(value, declarations)
.map_err(|error| error.prepend(PathSegment::EntryValue))
}
TypeExpr::Named(name) if !declarations.contains_key(name.as_str()) => {
Err(ValidationError::new(ValidationErrorKind::UnknownType {
name: name.clone(),
}))
}
_ => Ok(()),
}
}
fn validate_no_cycles(
descriptor: &InterfaceDescriptor,
declarations: &HashMap<&str, &TypeDeclaration>,
) -> Result<(), ValidationError> {
let mut state: HashMap<&str, VisitState> = HashMap::new();
let mut stack = Vec::new();
for declaration in &descriptor.declarations {
visit_declaration(
declaration.name(),
descriptor,
declarations,
&mut state,
&mut stack,
)?;
}
Ok(())
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum VisitState {
Visiting,
Visited,
}
fn visit_declaration<'a>(
name: &'a str,
descriptor: &InterfaceDescriptor,
declarations: &HashMap<&'a str, &'a TypeDeclaration>,
state: &mut HashMap<&'a str, VisitState>,
stack: &mut Vec<&'a str>,
) -> Result<(), ValidationError> {
match state.get(name) {
Some(VisitState::Visited) => return Ok(()),
Some(VisitState::Visiting) => {
let start = stack.iter().position(|entry| *entry == name).unwrap_or(0);
let mut cycle: Vec<String> = stack[start..]
.iter()
.map(|entry| (*entry).to_owned())
.collect();
cycle.push(name.to_owned());
return Err(
ValidationError::new(ValidationErrorKind::RecursiveType { cycle })
.prepend(PathSegment::Declaration(name.to_owned()))
.prepend(PathSegment::Interface(descriptor.name.clone())),
);
}
None => {}
}
state.insert(name, VisitState::Visiting);
stack.push(name);
let mut references = Vec::new();
collect_declaration_references(declarations[name], &mut references);
for reference in references {
visit_declaration(reference, descriptor, declarations, state, stack)?;
}
stack.pop();
state.insert(name, VisitState::Visited);
Ok(())
}
fn collect_declaration_references<'a>(
declaration: &'a TypeDeclaration,
references: &mut Vec<&'a str>,
) {
match declaration {
TypeDeclaration::Record(record) => {
for field in &record.fields {
collect_type_references(&field.ty, references);
}
}
TypeDeclaration::Enum(_) => {}
TypeDeclaration::Union(union) => {
for variant in &union.variants {
collect_type_references(&variant.ty, references);
}
}
}
}
fn collect_type_references<'a>(ty: &'a TypeExpr, references: &mut Vec<&'a str>) {
match ty {
TypeExpr::List(item) => collect_type_references(item, references),
TypeExpr::Entry { key, value } => {
collect_type_references(key, references);
collect_type_references(value, references);
}
TypeExpr::Named(name) => references.push(name),
_ => {}
}
}
pub(crate) fn validate_root_value(
descriptor: &InterfaceDescriptor,
ty: &TypeExpr,
value: &Value,
) -> Result<(), ValidationError> {
descriptor.validate()?;
let declarations = declaration_map(descriptor)?;
validate_value(ty, value, &declarations)
.map_err(|error| error.prepend(PathSegment::Interface(descriptor.name.clone())))
}
fn validate_value(
ty: &TypeExpr,
value: &Value,
declarations: &HashMap<&str, &TypeDeclaration>,
) -> Result<(), ValidationError> {
match ty {
TypeExpr::String => expect_kind(value, ValueKind::String),
TypeExpr::Boolean => expect_kind(value, ValueKind::Boolean),
TypeExpr::Integer => match value {
Value::Integer(integer) if (MIN_SAFE_INTEGER..=MAX_SAFE_INTEGER).contains(integer) => {
Ok(())
}
Value::Integer(integer) => Err(ValidationError::new(
ValidationErrorKind::IntegerOutOfRange { value: *integer },
)),
_ => expect_kind(value, ValueKind::Integer),
},
TypeExpr::Number => match value {
Value::Number(number) if number.is_finite() => Ok(()),
Value::Number(_) => Err(ValidationError::new(ValidationErrorKind::NonFiniteNumber)),
_ => expect_kind(value, ValueKind::Number),
},
TypeExpr::Bytes => expect_kind(value, ValueKind::Bytes),
TypeExpr::List(item_type) => match value {
Value::List(items) => {
for (index, item) in items.iter().enumerate() {
validate_value(item_type, item, declarations)
.map_err(|error| error.prepend(PathSegment::Index(index)))?;
}
Ok(())
}
_ => expect_kind(value, ValueKind::List),
},
TypeExpr::Entry {
key: key_type,
value: value_type,
} => match value {
Value::Entry { key, value } => {
validate_value(key_type, key, declarations)
.map_err(|error| error.prepend(PathSegment::EntryKey))?;
validate_value(value_type, value, declarations)
.map_err(|error| error.prepend(PathSegment::EntryValue))
}
_ => expect_kind(value, ValueKind::Entry),
},
TypeExpr::Json => match value {
Value::Json(json) => validate_json(json),
_ => expect_kind(value, ValueKind::Json),
},
TypeExpr::Named(name) => {
validate_declared_value(declarations[name.as_str()], value, declarations)
}
}
}
fn validate_declared_value(
declaration: &TypeDeclaration,
value: &Value,
declarations: &HashMap<&str, &TypeDeclaration>,
) -> Result<(), ValidationError> {
match declaration {
TypeDeclaration::Record(record) => match value {
Value::Record(fields) => {
let mut supplied = HashMap::new();
for field in fields {
if supplied.insert(field.name.as_str(), &field.value).is_some() {
return Err(ValidationError::new(ValidationErrorKind::DuplicateName {
namespace: NameNamespace::RecordValueField,
name: field.name.clone(),
})
.prepend(PathSegment::Field(field.name.clone())));
}
}
for field in fields {
let Some(declared) = record
.fields
.iter()
.find(|declared| declared.name == field.name)
else {
return Err(ValidationError::new(ValidationErrorKind::UnknownField {
name: field.name.clone(),
})
.prepend(PathSegment::Field(field.name.clone())));
};
validate_value(&declared.ty, &field.value, declarations)
.map_err(|error| error.prepend(PathSegment::Field(field.name.clone())))?;
}
for declared in &record.fields {
if !declared.optional && !supplied.contains_key(declared.name.as_str()) {
return Err(ValidationError::new(ValidationErrorKind::MissingField {
name: declared.name.clone(),
})
.prepend(PathSegment::Field(declared.name.clone())));
}
}
Ok(())
}
_ => expect_kind(value, ValueKind::Record),
},
TypeDeclaration::Enum(enumeration) => match value {
Value::Enum(variant)
if enumeration
.variants
.iter()
.any(|declared| declared.name == *variant) =>
{
Ok(())
}
Value::Enum(variant) => {
Err(ValidationError::new(ValidationErrorKind::UnknownVariant {
name: variant.clone(),
})
.prepend(PathSegment::Variant(variant.clone())))
}
_ => expect_kind(value, ValueKind::Enum),
},
TypeDeclaration::Union(union) => match value {
Value::Union { variant, value } => {
let Some(declared) = union
.variants
.iter()
.find(|declared| declared.name == *variant)
else {
return Err(ValidationError::new(ValidationErrorKind::UnknownVariant {
name: variant.clone(),
})
.prepend(PathSegment::Variant(variant.clone())));
};
validate_value(&declared.ty, value, declarations)
.map_err(|error| error.prepend(PathSegment::Variant(variant.clone())))
}
_ => expect_kind(value, ValueKind::Union),
},
}
}
fn expect_kind(value: &Value, expected: ValueKind) -> Result<(), ValidationError> {
if value.kind() == expected {
Ok(())
} else {
Err(ValidationError::new(ValidationErrorKind::TypeMismatch {
expected,
actual: value.kind(),
}))
}
}
fn validate_json(value: &JsonValue) -> Result<(), ValidationError> {
match value {
JsonValue::Integer(integer) if !(MIN_SAFE_INTEGER..=MAX_SAFE_INTEGER).contains(integer) => {
Err(ValidationError::new(
ValidationErrorKind::IntegerOutOfRange { value: *integer },
))
}
JsonValue::Number(number) if !number.is_finite() => {
Err(ValidationError::new(ValidationErrorKind::NonFiniteNumber))
}
JsonValue::Array(items) => {
for (index, item) in items.iter().enumerate() {
validate_json(item).map_err(|error| error.prepend(PathSegment::Index(index)))?;
}
Ok(())
}
JsonValue::Object(members) => {
let mut names = HashSet::new();
for member in members {
if !names.insert(member.name.as_str()) {
return Err(ValidationError::new(ValidationErrorKind::DuplicateName {
namespace: NameNamespace::JsonMember,
name: member.name.clone(),
})
.prepend(PathSegment::JsonMember(member.name.clone())));
}
validate_json(&member.value)
.map_err(|error| error.prepend(PathSegment::JsonMember(member.name.clone())))?;
}
Ok(())
}
_ => Ok(()),
}
}
pub(crate) fn validate_call(
descriptor: &InterfaceDescriptor,
call: &OperationCall,
) -> Result<(), ValidationError> {
descriptor.validate()?;
if call.target.interface != descriptor.name {
return Err(
ValidationError::new(ValidationErrorKind::InterfaceMismatch {
expected: descriptor.name.clone(),
actual: call.target.interface.clone(),
})
.prepend(PathSegment::Interface(call.target.interface.clone())),
);
}
let Some(operation) = descriptor
.operations
.iter()
.find(|operation| operation.name == call.target.operation)
else {
return Err(ValidationError::new(ValidationErrorKind::UnknownOperation {
name: call.target.operation.clone(),
})
.prepend(PathSegment::Operation(call.target.operation.clone()))
.prepend(PathSegment::Interface(descriptor.name.clone())));
};
let mut supplied = HashMap::new();
for argument in &call.arguments {
if supplied
.insert(argument.name.as_str(), &argument.value)
.is_some()
{
return Err(ValidationError::new(ValidationErrorKind::DuplicateName {
namespace: NameNamespace::Argument,
name: argument.name.clone(),
})
.prepend(PathSegment::Parameter(argument.name.clone()))
.prepend(PathSegment::Operation(operation.name.clone()))
.prepend(PathSegment::Interface(descriptor.name.clone())));
}
}
let declarations = declaration_map(descriptor)?;
for argument in &call.arguments {
let Some(parameter) = operation
.parameters
.iter()
.find(|parameter| parameter.name == argument.name)
else {
return Err(ValidationError::new(ValidationErrorKind::UnknownParameter {
name: argument.name.clone(),
})
.prepend(PathSegment::Parameter(argument.name.clone()))
.prepend(PathSegment::Operation(operation.name.clone()))
.prepend(PathSegment::Interface(descriptor.name.clone())));
};
validate_value(&parameter.ty, &argument.value, &declarations).map_err(|error| {
error
.prepend(PathSegment::Parameter(argument.name.clone()))
.prepend(PathSegment::Operation(operation.name.clone()))
.prepend(PathSegment::Interface(descriptor.name.clone()))
})?;
}
for parameter in &operation.parameters {
if !parameter.optional && !supplied.contains_key(parameter.name.as_str()) {
return Err(ValidationError::new(ValidationErrorKind::MissingParameter {
name: parameter.name.clone(),
})
.prepend(PathSegment::Parameter(parameter.name.clone()))
.prepend(PathSegment::Operation(operation.name.clone()))
.prepend(PathSegment::Interface(descriptor.name.clone())));
}
}
Ok(())
}
pub(crate) fn validate_return(
descriptor: &InterfaceDescriptor,
operation_name: &str,
value: Option<&Value>,
) -> Result<(), ValidationError> {
descriptor.validate()?;
let Some(operation) = descriptor
.operations
.iter()
.find(|operation| operation.name == operation_name)
else {
return Err(ValidationError::new(ValidationErrorKind::UnknownOperation {
name: operation_name.to_owned(),
})
.prepend(PathSegment::Operation(operation_name.to_owned()))
.prepend(PathSegment::Interface(descriptor.name.clone())));
};
match (&operation.returns, value) {
(None, None) => Ok(()),
(None, Some(_)) => Err(ValidationError::new(
ValidationErrorKind::UnexpectedReturnValue,
)),
(Some(_), None) => Err(ValidationError::new(
ValidationErrorKind::MissingReturnValue,
)),
(Some(returns), Some(value)) => {
let declarations = declaration_map(descriptor)?;
validate_value(&returns.ty, value, &declarations)
}
}
.map_err(|error| {
error
.prepend(PathSegment::Return)
.prepend(PathSegment::Operation(operation.name.clone()))
.prepend(PathSegment::Interface(descriptor.name.clone()))
})
}
+525
View File
@@ -0,0 +1,525 @@
use wip_protocol::{
EnumDeclaration, EnumVariantDeclaration, FieldDeclaration, InterfaceDescriptor,
InterfaceObservation, JsonMember, JsonValue, MAX_SAFE_INTEGER, NameNamespace, NamedValue,
Object, ObjectId, OperationCall, OperationDeclaration, OperationTarget, ParameterDeclaration,
PathSegment, RecordDeclaration, ReturnDeclaration, TypeDeclaration, TypeExpr, UnionDeclaration,
UnionVariantDeclaration, ValidationErrorKind, Value, ValueKind,
};
fn field(name: &str, ty: TypeExpr, optional: bool) -> FieldDeclaration {
FieldDeclaration {
name: name.into(),
documentation: None,
ty,
optional,
}
}
fn descriptor(name: &str) -> InterfaceDescriptor {
InterfaceDescriptor {
name: name.into(),
documentation: None,
declarations: vec![
TypeDeclaration::Enum(EnumDeclaration {
name: "Color".into(),
documentation: None,
variants: vec![
EnumVariantDeclaration {
name: "red".into(),
documentation: None,
},
EnumVariantDeclaration {
name: "blue".into(),
documentation: None,
},
],
}),
TypeDeclaration::Record(RecordDeclaration {
name: "Payload".into(),
documentation: None,
fields: vec![
field("title", TypeExpr::String, false),
field("color", TypeExpr::Named("Color".into()), true),
field(
"metadata",
TypeExpr::Entry {
key: Box::new(TypeExpr::String),
value: Box::new(TypeExpr::Json),
},
true,
),
],
}),
TypeDeclaration::Union(UnionDeclaration {
name: "Selection".into(),
documentation: None,
variants: vec![
UnionVariantDeclaration {
name: "by_name".into(),
documentation: None,
ty: TypeExpr::String,
},
UnionVariantDeclaration {
name: "by_id".into(),
documentation: None,
ty: TypeExpr::Integer,
},
],
}),
],
operations: vec![OperationDeclaration {
name: "publish".into(),
documentation: None,
parameters: vec![
ParameterDeclaration {
name: "payload".into(),
documentation: None,
ty: TypeExpr::Named("Payload".into()),
optional: false,
},
ParameterDeclaration {
name: "selection".into(),
documentation: None,
ty: TypeExpr::Named("Selection".into()),
optional: true,
},
],
returns: Some(ReturnDeclaration {
documentation: None,
ty: TypeExpr::Boolean,
}),
}],
}
}
fn named(name: &str, value: Value) -> NamedValue {
NamedValue {
name: name.into(),
value,
}
}
#[test]
fn valid_descriptor_values_call_and_return_are_accepted() {
let descriptor = descriptor("catalog");
descriptor.validate().unwrap();
let payload = Value::Record(vec![
named("title", Value::String("hello".into())),
named("color", Value::Enum("red".into())),
named(
"metadata",
Value::Entry {
key: Box::new(Value::String("source".into())),
value: Box::new(Value::Json(JsonValue::Object(vec![JsonMember {
name: "trusted".into(),
value: JsonValue::Boolean(true),
}]))),
},
),
]);
descriptor
.validate_value(&TypeExpr::Named("Payload".into()), &payload)
.unwrap();
let call = OperationCall {
target: OperationTarget {
object: ObjectId("object-1".into()),
interface: "catalog".into(),
operation: "publish".into(),
},
arguments: vec![named("payload", payload)],
};
descriptor.validate_call(&call).unwrap();
descriptor
.validate_return("publish", Some(&Value::Boolean(true)))
.unwrap();
}
#[test]
fn duplicate_declaration_names_are_structured_errors() {
let mut descriptor = descriptor("catalog");
descriptor
.declarations
.push(TypeDeclaration::Record(RecordDeclaration {
name: "Color".into(),
documentation: None,
fields: vec![],
}));
let error = descriptor.validate().unwrap_err();
assert_eq!(
error.kind,
ValidationErrorKind::DuplicateName {
namespace: NameNamespace::TypeDeclaration,
name: "Color".into(),
}
);
assert_eq!(
error.path,
vec![
PathSegment::Interface("catalog".into()),
PathSegment::Declaration("Color".into())
]
);
}
#[test]
fn member_and_operation_names_are_unique_in_their_own_scopes() {
let mut duplicate_field = descriptor("one");
let TypeDeclaration::Record(record) = &mut duplicate_field.declarations[1] else {
unreachable!()
};
record.fields.push(field("title", TypeExpr::String, false));
assert!(matches!(
duplicate_field.validate().unwrap_err().kind,
ValidationErrorKind::DuplicateName {
namespace: NameNamespace::Field,
..
}
));
let mut duplicate_operation = descriptor("one");
duplicate_operation
.operations
.push(duplicate_operation.operations[0].clone());
assert!(matches!(
duplicate_operation.validate().unwrap_err().kind,
ValidationErrorKind::DuplicateName {
namespace: NameNamespace::Operation,
..
}
));
let mut duplicate_parameter = descriptor("one");
let repeated_parameter = duplicate_parameter.operations[0].parameters[0].clone();
duplicate_parameter.operations[0]
.parameters
.push(repeated_parameter);
assert!(matches!(
duplicate_parameter.validate().unwrap_err().kind,
ValidationErrorKind::DuplicateName {
namespace: NameNamespace::Parameter,
..
}
));
// Operation names are local to an interface, so another descriptor may
// independently declare the same operation name.
descriptor("one").validate().unwrap();
descriptor("two").validate().unwrap();
}
#[test]
fn named_references_must_exist_and_must_not_cycle() {
let mut unknown = descriptor("catalog");
let TypeDeclaration::Record(record) = &mut unknown.declarations[1] else {
unreachable!()
};
record.fields[0].ty = TypeExpr::Named("Elsewhere".into());
let error = unknown.validate().unwrap_err();
assert_eq!(
error.kind,
ValidationErrorKind::UnknownType {
name: "Elsewhere".into()
}
);
assert_eq!(
error.path,
vec![
PathSegment::Interface("catalog".into()),
PathSegment::Declaration("Payload".into()),
PathSegment::Field("title".into()),
]
);
let cyclic = InterfaceDescriptor {
name: "cyclic".into(),
documentation: None,
declarations: vec![
TypeDeclaration::Record(RecordDeclaration {
name: "A".into(),
documentation: None,
fields: vec![field("b", TypeExpr::Named("B".into()), false)],
}),
TypeDeclaration::Union(UnionDeclaration {
name: "B".into(),
documentation: None,
variants: vec![UnionVariantDeclaration {
name: "a".into(),
documentation: None,
ty: TypeExpr::List(Box::new(TypeExpr::Named("A".into()))),
}],
}),
],
operations: vec![],
};
assert_eq!(
cyclic.validate().unwrap_err().kind,
ValidationErrorKind::RecursiveType {
cycle: vec!["A".into(), "B".into(), "A".into()]
}
);
}
#[test]
fn records_reject_unknown_missing_duplicate_and_mistyped_fields() {
let descriptor = descriptor("catalog");
let ty = TypeExpr::Named("Payload".into());
let error = descriptor
.validate_value(
&ty,
&Value::Record(vec![
named("title", Value::String("ok".into())),
named("extra", Value::Boolean(true)),
]),
)
.unwrap_err();
assert_eq!(
error.kind,
ValidationErrorKind::UnknownField {
name: "extra".into()
}
);
let error = descriptor
.validate_value(&ty, &Value::Record(vec![]))
.unwrap_err();
assert_eq!(
error.kind,
ValidationErrorKind::MissingField {
name: "title".into()
}
);
let error = descriptor
.validate_value(
&ty,
&Value::Record(vec![
named("title", Value::String("one".into())),
named("title", Value::String("two".into())),
]),
)
.unwrap_err();
assert!(matches!(
error.kind,
ValidationErrorKind::DuplicateName {
namespace: NameNamespace::RecordValueField,
..
}
));
let error = descriptor
.validate_value(&ty, &Value::Record(vec![named("title", Value::Integer(1))]))
.unwrap_err();
assert_eq!(
error.kind,
ValidationErrorKind::TypeMismatch {
expected: ValueKind::String,
actual: ValueKind::Integer,
}
);
// Optional means omission: there is no general-purpose Value::Null.
descriptor
.validate_value(
&ty,
&Value::Record(vec![named("title", Value::String("ok".into()))]),
)
.unwrap();
}
#[test]
fn integers_numbers_enums_unions_entries_and_json_are_validated() {
let descriptor = descriptor("catalog");
descriptor
.validate_value(&TypeExpr::Integer, &Value::Integer(MAX_SAFE_INTEGER))
.unwrap();
assert_eq!(
descriptor
.validate_value(&TypeExpr::Integer, &Value::Integer(MAX_SAFE_INTEGER + 1))
.unwrap_err()
.kind,
ValidationErrorKind::IntegerOutOfRange {
value: MAX_SAFE_INTEGER + 1
}
);
assert_eq!(
descriptor
.validate_value(&TypeExpr::Number, &Value::Number(f64::INFINITY))
.unwrap_err()
.kind,
ValidationErrorKind::NonFiniteNumber
);
assert!(matches!(
descriptor
.validate_value(
&TypeExpr::Named("Color".into()),
&Value::Enum("green".into())
)
.unwrap_err()
.kind,
ValidationErrorKind::UnknownVariant { .. }
));
descriptor
.validate_value(
&TypeExpr::Named("Selection".into()),
&Value::Union {
variant: "by_id".into(),
value: Box::new(Value::Integer(7)),
},
)
.unwrap();
assert!(matches!(
descriptor
.validate_value(
&TypeExpr::Named("Selection".into()),
&Value::Union {
variant: "by_id".into(),
value: Box::new(Value::String("wrong".into())),
},
)
.unwrap_err()
.kind,
ValidationErrorKind::TypeMismatch { .. }
));
let entry_ty = TypeExpr::Entry {
key: Box::new(TypeExpr::String),
value: Box::new(TypeExpr::Integer),
};
let error = descriptor
.validate_value(
&entry_ty,
&Value::Entry {
key: Box::new(Value::String("key".into())),
value: Box::new(Value::Boolean(false)),
},
)
.unwrap_err();
assert_eq!(error.path.last(), Some(&PathSegment::EntryValue));
let json = Value::Json(JsonValue::Object(vec![
JsonMember {
name: "same".into(),
value: JsonValue::Null,
},
JsonMember {
name: "same".into(),
value: JsonValue::Number(f64::NAN),
},
]));
assert!(matches!(
descriptor
.validate_value(&TypeExpr::Json, &json)
.unwrap_err()
.kind,
ValidationErrorKind::DuplicateName {
namespace: NameNamespace::JsonMember,
..
}
));
}
#[test]
fn object_interfaces_are_an_ordered_set_and_observations_correspond() {
let object = Object {
id: ObjectId("object-1".into()),
interfaces: vec!["catalog".into(), "search".into()],
};
object.validate().unwrap();
assert_eq!(object.interfaces, vec!["catalog", "search"]);
let duplicate = Object {
id: ObjectId("object-2".into()),
interfaces: vec!["catalog".into(), "catalog".into()],
};
assert_eq!(
duplicate.validate().unwrap_err().kind,
ValidationErrorKind::DuplicateInterface {
name: "catalog".into()
}
);
let observation = InterfaceObservation {
object: object.clone(),
descriptors: vec![descriptor("catalog"), descriptor("search")],
};
observation.validate().unwrap();
let missing = InterfaceObservation {
object,
descriptors: vec![descriptor("catalog")],
};
assert_eq!(
missing.validate().unwrap_err().kind,
ValidationErrorKind::MissingInterfaceDescriptor {
name: "search".into()
}
);
}
#[test]
fn calls_reject_unknown_missing_duplicate_and_bad_arguments_and_returns() {
let catalog = descriptor("catalog");
let target = OperationTarget {
object: ObjectId("object-1".into()),
interface: "catalog".into(),
operation: "publish".into(),
};
let missing = OperationCall {
target: target.clone(),
arguments: vec![],
};
assert_eq!(
catalog.validate_call(&missing).unwrap_err().kind,
ValidationErrorKind::MissingParameter {
name: "payload".into()
}
);
let unknown = OperationCall {
target: target.clone(),
arguments: vec![named("other", Value::Boolean(true))],
};
assert_eq!(
catalog.validate_call(&unknown).unwrap_err().kind,
ValidationErrorKind::UnknownParameter {
name: "other".into()
}
);
let duplicate = OperationCall {
target,
arguments: vec![
named("payload", Value::Record(vec![])),
named("payload", Value::Record(vec![])),
],
};
assert!(matches!(
catalog.validate_call(&duplicate).unwrap_err().kind,
ValidationErrorKind::DuplicateName {
namespace: NameNamespace::Argument,
..
}
));
assert_eq!(
catalog.validate_return("publish", None).unwrap_err().kind,
ValidationErrorKind::MissingReturnValue
);
let mut no_return = descriptor("catalog");
no_return.operations[0].returns = None;
assert_eq!(
no_return
.validate_return("publish", Some(&Value::Boolean(true)))
.unwrap_err()
.kind,
ValidationErrorKind::UnexpectedReturnValue
);
}