mod validator;
use std::collections::{BTreeMap, BTreeSet};
pub use validator::{validate_document, validate_document_with_limits};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SchemaDefinitionError {
message: String,
}
impl SchemaDefinitionError {
fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
}
}
}
impl std::fmt::Display for SchemaDefinitionError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str(&self.message)
}
}
impl std::error::Error for SchemaDefinitionError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Cardinality {
min: usize,
max: Option<usize>,
}
impl Cardinality {
pub fn new(min: usize, max: Option<usize>) -> Result<Self, SchemaDefinitionError> {
if max.is_some_and(|max| max < min) {
return Err(SchemaDefinitionError::new(
"schema cardinality maximum must be greater than or equal to its minimum",
));
}
Ok(Self { min, max })
}
pub const fn exactly(count: usize) -> Self {
Self {
min: count,
max: Some(count),
}
}
pub const fn at_least(min: usize) -> Self {
Self { min, max: None }
}
pub const fn min(self) -> usize {
self.min
}
pub const fn max(self) -> Option<usize> {
self.max
}
pub(crate) fn contains(self, count: usize) -> bool {
count >= self.min && self.max.is_none_or(|max| count <= max)
}
pub(crate) fn describe(self, singular: &str, plural: &str) -> String {
match self.max {
Some(max) if max == self.min => {
let noun = if self.min == 1 { singular } else { plural };
format!("exactly {} {noun}", self.min)
}
Some(max) => format!("between {} and {max} {plural}", self.min),
None => {
let noun = if self.min == 1 { singular } else { plural };
format!("at least {} {noun}", self.min)
}
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Schema {
attributes: BTreeMap<String, AttributeSchema>,
blocks: BTreeMap<String, BlockSchema>,
allow_unknown_attributes: bool,
allow_unknown_blocks: bool,
}
impl Schema {
pub fn new() -> Self {
Self::default()
}
pub fn attribute(mut self, name: impl Into<String>, attribute: AttributeSchema) -> Self {
self.attributes.insert(name.into(), attribute);
self
}
pub fn block(mut self, name: impl Into<String>, block: BlockSchema) -> Self {
self.blocks.insert(name.into(), block);
self
}
pub fn allow_unknown_attributes(mut self, allow: bool) -> Self {
self.allow_unknown_attributes = allow;
self
}
pub fn allow_unknown_blocks(mut self, allow: bool) -> Self {
self.allow_unknown_blocks = allow;
self
}
pub(crate) fn block_rule(&self, name: &str) -> Option<&BlockSchema> {
self.blocks.get(name)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AttributeSchema {
required: bool,
value: ValueSchema,
}
impl AttributeSchema {
pub fn required(value: ValueSchema) -> Self {
Self {
required: true,
value,
}
}
pub fn optional(value: ValueSchema) -> Self {
Self {
required: false,
value,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BlockSchema {
body: Schema,
occurrences: Cardinality,
labels: Cardinality,
unordered: bool,
}
impl BlockSchema {
pub fn new(body: Schema) -> Self {
Self {
body,
occurrences: Cardinality::at_least(0),
labels: Cardinality::exactly(0),
unordered: false,
}
}
pub fn occurrences(mut self, occurrences: Cardinality) -> Self {
self.occurrences = occurrences;
self
}
pub fn labels(mut self, labels: Cardinality) -> Self {
self.labels = labels;
self
}
pub fn unordered(mut self, unordered: bool) -> Self {
self.unordered = unordered;
self
}
pub(crate) fn body_schema(&self) -> &Schema {
&self.body
}
pub(crate) fn is_unordered(&self) -> bool {
self.unordered
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ObjectSchema {
fields: BTreeMap<String, AttributeSchema>,
allow_unknown_fields: bool,
}
impl ObjectSchema {
pub fn new() -> Self {
Self::default()
}
pub fn field(mut self, name: impl Into<String>, field: AttributeSchema) -> Self {
self.fields.insert(name.into(), field);
self
}
pub fn allow_unknown_fields(mut self, allow: bool) -> Self {
self.allow_unknown_fields = allow;
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CallSchema {
allowed_names: BTreeSet<String>,
arguments: Cardinality,
argument: Box<ValueSchema>,
}
impl CallSchema {
pub fn new() -> Self {
Self {
allowed_names: BTreeSet::new(),
arguments: Cardinality::at_least(0),
argument: Box::new(ValueSchema::any()),
}
}
pub fn allowed_name(mut self, name: impl Into<String>) -> Self {
self.allowed_names.insert(name.into());
self
}
pub fn arguments(mut self, arguments: Cardinality, argument: ValueSchema) -> Self {
self.arguments = arguments;
self.argument = Box::new(argument);
self
}
}
impl Default for CallSchema {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ValueSchema {
kind: ValueSchemaKind,
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum ValueSchemaKind {
Any,
String,
Number,
Bool,
Null,
List(Box<ValueSchema>),
Object(ObjectSchema),
Call(CallSchema),
OneOf(Vec<ValueSchema>),
}
impl ValueSchema {
pub fn any() -> Self {
Self {
kind: ValueSchemaKind::Any,
}
}
pub fn string() -> Self {
Self {
kind: ValueSchemaKind::String,
}
}
pub fn number() -> Self {
Self {
kind: ValueSchemaKind::Number,
}
}
pub fn bool() -> Self {
Self {
kind: ValueSchemaKind::Bool,
}
}
pub fn null() -> Self {
Self {
kind: ValueSchemaKind::Null,
}
}
pub fn list(item: ValueSchema) -> Self {
Self {
kind: ValueSchemaKind::List(Box::new(item)),
}
}
pub fn object(object: ObjectSchema) -> Self {
Self {
kind: ValueSchemaKind::Object(object),
}
}
pub fn call(call: CallSchema) -> Self {
Self {
kind: ValueSchemaKind::Call(call),
}
}
pub fn one_of(variants: Vec<ValueSchema>) -> Result<Self, SchemaDefinitionError> {
if variants.is_empty() {
return Err(SchemaDefinitionError::new(
"schema union must contain at least one variant",
));
}
Ok(Self {
kind: ValueSchemaKind::OneOf(variants),
})
}
fn kind(&self) -> &ValueSchemaKind {
&self.kind
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SchemaDiagnosticCode {
UnknownAttribute,
DuplicateAttribute,
MissingAttribute,
UnknownBlock,
BlockCount,
LabelCount,
ValueType,
UnknownObjectField,
DuplicateObjectField,
MissingObjectField,
CallName,
CallArgumentCount,
}
impl SchemaDiagnosticCode {
pub const fn as_str(self) -> &'static str {
match self {
Self::UnknownAttribute => "acl.schema.unknown_attribute",
Self::DuplicateAttribute => "acl.schema.duplicate_attribute",
Self::MissingAttribute => "acl.schema.missing_attribute",
Self::UnknownBlock => "acl.schema.unknown_block",
Self::BlockCount => "acl.schema.block_count",
Self::LabelCount => "acl.schema.label_count",
Self::ValueType => "acl.schema.value_type",
Self::UnknownObjectField => "acl.schema.unknown_object_field",
Self::DuplicateObjectField => "acl.schema.duplicate_object_field",
Self::MissingObjectField => "acl.schema.missing_object_field",
Self::CallName => "acl.schema.call_name",
Self::CallArgumentCount => "acl.schema.call_argument_count",
}
}
}
impl std::fmt::Display for SchemaDiagnosticCode {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str(self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SchemaDiagnostic {
pub code: SchemaDiagnosticCode,
pub message: String,
pub path: String,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct SchemaReport {
pub diagnostics: Vec<SchemaDiagnostic>,
pub truncated: bool,
}
impl SchemaReport {
pub fn is_empty(&self) -> bool {
self.diagnostics.is_empty() && !self.truncated
}
}