use std::collections::HashSet;
use std::error::Error;
use std::fmt;
use indexmap::IndexMap;
use serde_json::{Number, Value};
use crate::compose::FlatNode;
use crate::model::{DataType, IfcxFile, IfcxNode, IfcxSchema, IfcxValueDescription};
pub const INTERNAL_ATTRIBUTE_PREFIX: &str = "__internal";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum JsonType {
Null,
Boolean,
Number,
String,
Array,
Object,
}
impl JsonType {
pub fn of(value: &Value) -> Self {
match value {
Value::Null => Self::Null,
Value::Bool(_) => Self::Boolean,
Value::Number(_) => Self::Number,
Value::String(_) => Self::String,
Value::Array(_) => Self::Array,
Value::Object(_) => Self::Object,
}
}
pub fn as_str(self) -> &'static str {
match self {
Self::Null => "null",
Self::Boolean => "boolean",
Self::Number => "number",
Self::String => "string",
Self::Array => "array",
Self::Object => "object",
}
}
}
impl fmt::Display for JsonType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum FailureKind {
MissingSchema,
UnknownInheritedSchema { id: String },
InheritanceCycle { id: String },
TypeMismatch { expected: DataType, found: JsonType },
NotAnInteger { value: Number },
NotAnOption { value: String, options: Vec<String> },
MissingKey { key: String },
TooFewElements { len: usize, min: Number },
TooManyElements { len: usize, max: Number },
MissingRestrictions { data_type: DataType },
UnknownDataType { name: String },
}
impl fmt::Display for FailureKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::MissingSchema => f.write_str("no schema with this id"),
Self::UnknownInheritedSchema { id } => {
write!(f, "inherits unknown schema \"{id}\"")
}
Self::InheritanceCycle { id } => {
write!(f, "schema \"{id}\" inherits from itself")
}
Self::TypeMismatch { expected, found } => {
write!(f, "expected {}, found {found}", expected.as_str())
}
Self::NotAnInteger { value } => write!(f, "expected Integer, found {value}"),
Self::NotAnOption { value, options } => {
write!(f, "\"{value}\" is not one of [{}]", options.join(", "))
}
Self::MissingKey { key } => write!(f, "missing key \"{key}\""),
Self::TooFewElements { len, min } => {
write!(f, "array has {len} elements, fewer than min {min}")
}
Self::TooManyElements { len, max } => {
write!(f, "array has {len} elements, more than max {max}")
}
Self::MissingRestrictions { data_type } => {
let field = match data_type {
DataType::Enum => "enumRestrictions",
DataType::Array => "arrayRestrictions",
_ => "restrictions",
};
write!(f, "{} schema has no {field}", data_type.as_str())
}
Self::UnknownDataType { name } => write!(f, "unknown dataType \"{name}\""),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ValidationFailure {
pub node: String,
pub attribute: String,
pub pointer: String,
pub kind: FailureKind,
}
impl fmt::Display for ValidationFailure {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "[\"{}\"].attributes[\"{}\"]", self.node, self.attribute)?;
if !self.pointer.is_empty() {
write!(f, " at {}", self.pointer)?;
}
write!(f, ": {}", self.kind)
}
}
impl Error for ValidationFailure {}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct ValidationReport {
pub failures: Vec<ValidationFailure>,
}
impl ValidationReport {
pub fn is_valid(&self) -> bool {
self.failures.is_empty()
}
pub fn into_result(self) -> Result<(), ValidationReport> {
if self.is_valid() {
Ok(())
} else {
Err(self)
}
}
}
impl fmt::Display for ValidationReport {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.failures.len() {
0 => return f.write_str("all attributes match their schemas"),
1 => f.write_str("1 attribute value does not match its schema")?,
n => write!(f, "{n} attribute values do not match their schemas")?,
}
for failure in &self.failures {
write!(f, "\n {failure}")?;
}
Ok(())
}
}
impl Error for ValidationReport {}
pub fn validate_attributes<'a, P, I>(
schemas: &IndexMap<String, IfcxSchema>,
nodes: I,
) -> ValidationReport
where
P: AsRef<str> + 'a,
I: IntoIterator<Item = (P, &'a IndexMap<String, Value>)>,
{
check_nodes(
schemas,
nodes.into_iter().map(|(path, attributes)| {
(
path,
attributes.iter().map(|(id, value)| (id.as_str(), value)),
)
}),
)
}
pub fn validate_flat(
schemas: &IndexMap<String, IfcxSchema>,
nodes: &IndexMap<String, FlatNode>,
) -> ValidationReport {
check_nodes(
schemas,
nodes.iter().map(|(path, node)| {
(
path,
node.attributes
.iter()
.map(|(id, value)| (id.as_str(), &**value)),
)
}),
)
}
fn check_nodes<'v, P, A, I>(schemas: &IndexMap<String, IfcxSchema>, nodes: I) -> ValidationReport
where
P: AsRef<str>,
A: IntoIterator<Item = (&'v str, &'v Value)>,
I: IntoIterator<Item = (P, A)>,
{
let mut report = ValidationReport::default();
for (path, attributes) in nodes {
let path = path.as_ref();
for (id, value) in attributes {
if id.starts_with(INTERNAL_ATTRIBUTE_PREFIX) {
continue;
}
let mut checker = Checker {
schemas,
node: path,
attribute: id,
pointer: String::new(),
inheriting: HashSet::new(),
failures: &mut report.failures,
};
match schemas.get_key_value(id) {
Some((key, schema)) => {
checker.inheriting.insert(key);
checker.check(&schema.value, value);
}
None => checker.fail(FailureKind::MissingSchema),
}
}
}
report
}
pub fn validate_nodes<'a, I>(schemas: &IndexMap<String, IfcxSchema>, nodes: I) -> ValidationReport
where
I: IntoIterator<Item = &'a IfcxNode>,
{
validate_attributes(
schemas,
nodes
.into_iter()
.filter_map(|node| Some((node.path.as_str(), node.attributes.as_ref()?))),
)
}
impl IfcxFile {
pub fn validate(&self) -> Result<(), ValidationReport> {
check_merged(&self.schemas, &self.data).into_result()
}
}
fn merge_attributes<'a>(
nodes: impl IntoIterator<Item = &'a IfcxNode>,
) -> IndexMap<&'a str, IndexMap<&'a str, &'a Value>> {
let mut merged: IndexMap<&str, IndexMap<&str, &Value>> = IndexMap::new();
for node in nodes {
let entry = merged.entry(node.path.as_str()).or_default();
for (id, value) in node.attributes.iter().flatten() {
entry.insert(id.as_str(), value);
}
}
merged
}
fn check_merged<'a>(
schemas: &IndexMap<String, IfcxSchema>,
nodes: impl IntoIterator<Item = &'a IfcxNode>,
) -> ValidationReport {
let merged = merge_attributes(nodes);
check_nodes(
schemas,
merged
.iter()
.map(|(path, attributes)| (*path, attributes.iter().map(|(id, value)| (*id, *value)))),
)
}
pub(crate) fn validate_files<'a>(
files: impl IntoIterator<Item = &'a IfcxFile>,
) -> ValidationReport {
let files: Vec<&IfcxFile> = files.into_iter().collect();
let mut schemas: IndexMap<String, IfcxSchema> = IndexMap::new();
for file in &files {
for (id, schema) in &file.schemas {
schemas.insert(id.clone(), schema.clone());
}
}
check_merged(&schemas, files.iter().flat_map(|file| &file.data))
}
struct Checker<'s, 'r> {
schemas: &'s IndexMap<String, IfcxSchema>,
node: &'r str,
attribute: &'r str,
pointer: String,
inheriting: HashSet<&'s str>,
failures: &'r mut Vec<ValidationFailure>,
}
impl<'s> Checker<'s, '_> {
fn fail(&mut self, kind: FailureKind) {
self.failures.push(ValidationFailure {
node: self.node.to_owned(),
attribute: self.attribute.to_owned(),
pointer: self.pointer.clone(),
kind,
});
}
fn mismatch(&mut self, expected: &DataType, value: &Value) {
self.fail(FailureKind::TypeMismatch {
expected: expected.clone(),
found: JsonType::of(value),
});
}
fn check_at(&mut self, token: &str, desc: &'s IfcxValueDescription, value: &Value) {
let len = self.pointer.len();
self.pointer.push('/');
for c in token.chars() {
match c {
'~' => self.pointer.push_str("~0"),
'/' => self.pointer.push_str("~1"),
c => self.pointer.push(c),
}
}
self.check(desc, value);
self.pointer.truncate(len);
}
fn check(&mut self, desc: &'s IfcxValueDescription, value: &Value) {
let mut done: HashSet<&'s str> = HashSet::new();
let mut stack: Vec<(&'s IfcxValueDescription, Option<&'s str>, usize)> =
vec![(desc, None, 0)];
while let Some((current, _, next)) = stack.last_mut() {
if let Some(id) = current.inherits.as_deref().and_then(|ids| ids.get(*next)) {
*next += 1;
let Some((key, schema)) = self.schemas.get_key_value(id) else {
self.fail(FailureKind::UnknownInheritedSchema { id: id.clone() });
continue;
};
if self.inheriting.contains(key.as_str()) {
self.fail(FailureKind::InheritanceCycle { id: id.clone() });
} else if !done.contains(key.as_str()) {
self.inheriting.insert(key);
stack.push((&schema.value, Some(key), 0));
}
continue;
}
let (current, key, _) = stack.pop().expect("stack is not empty");
self.check_own(current, value);
if let Some(key) = key {
self.inheriting.remove(key);
done.insert(key);
}
}
}
fn check_own(&mut self, desc: &'s IfcxValueDescription, value: &Value) {
let data_type = &desc.data_type;
match data_type {
DataType::Boolean => {
if !value.is_boolean() {
self.mismatch(data_type, value);
}
}
DataType::String | DataType::DateTime | DataType::Reference => {
if !value.is_string() {
self.mismatch(data_type, value);
}
}
DataType::Real => {
if !value.is_number() {
self.mismatch(data_type, value);
}
}
DataType::Integer => match value {
Value::Number(n) => {
let integral = n.is_i64()
|| n.is_u64()
|| n.as_f64()
.is_some_and(|f| f.is_finite() && f.fract() == 0.0);
if !integral {
self.fail(FailureKind::NotAnInteger { value: n.clone() });
}
}
_ => self.mismatch(data_type, value),
},
DataType::Enum => {
let Value::String(s) = value else {
return self.mismatch(data_type, value);
};
let Some(restrictions) = &desc.enum_restrictions else {
return self.fail(FailureKind::MissingRestrictions {
data_type: data_type.clone(),
});
};
if !restrictions.options.contains(s) {
self.fail(FailureKind::NotAnOption {
value: s.clone(),
options: restrictions.options.clone(),
});
}
}
DataType::Object => {
let Value::Object(object) = value else {
return self.mismatch(data_type, value);
};
let Some(restrictions) = &desc.object_restrictions else {
return;
};
for (key, key_desc) in &restrictions.values {
match object.get(key) {
Some(v) => self.check_at(key, key_desc, v),
None if key_desc.optional == Some(true) => {}
None => self.fail(FailureKind::MissingKey { key: key.clone() }),
}
}
}
DataType::Array => {
let Value::Array(elements) = value else {
return self.mismatch(data_type, value);
};
let Some(restrictions) = &desc.array_restrictions else {
return self.fail(FailureKind::MissingRestrictions {
data_type: data_type.clone(),
});
};
let len = elements.len();
if let Some(min) = &restrictions.min {
if (len as f64) < min.as_f64().unwrap_or(f64::NEG_INFINITY) {
self.fail(FailureKind::TooFewElements {
len,
min: min.clone(),
});
}
}
if let Some(max) = &restrictions.max {
if (len as f64) > max.as_f64().unwrap_or(f64::INFINITY) {
self.fail(FailureKind::TooManyElements {
len,
max: max.clone(),
});
}
}
for (i, element) in elements.iter().enumerate() {
self.check_at(&i.to_string(), &restrictions.value, element);
}
}
DataType::Blob => {}
DataType::Other(name) => self.fail(FailureKind::UnknownDataType { name: name.clone() }),
}
}
}