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