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
+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()))
})
}