feat(protocol): add logical model and validation
This commit is contained in:
Generated
+7
@@ -2,6 +2,13 @@
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "wip-protocol"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "wip-rs"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"wip-protocol",
|
||||
]
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
[workspace]
|
||||
members = [".", "crates/wip-protocol"]
|
||||
resolver = "3"
|
||||
|
||||
[package]
|
||||
name = "wip-rs"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
wip-protocol = { path = "crates/wip-protocol" }
|
||||
|
||||
@@ -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]
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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(¶meter.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(¶meter.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()))
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user