use ifc_model::Value;
use ifc_schema::{Schema, TypeKind};
use super::form::{aliases_to, form_of, select_lists, Form};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Verdict {
Fits,
WrongType,
WrongForm {
typed_required: bool,
},
}
pub(crate) fn describe_value(value: &Value) -> String {
match value {
Value::Null => "unset ($)".to_owned(),
Value::Derived => "derived (*)".to_owned(),
Value::Bool(_) => "a boolean".to_owned(),
Value::LogicalUnknown => "a logical unknown".to_owned(),
Value::Integer(_) => "an integer".to_owned(),
Value::Real(_) => "a real".to_owned(),
Value::Text(_) => "a string".to_owned(),
Value::Binary(_) => "a binary literal".to_owned(),
Value::Enum(_) => "an enumeration constant".to_owned(),
Value::Ref(_) => "an entity reference".to_owned(),
Value::List(_) => "an aggregate".to_owned(),
Value::Typed { type_name, .. } => format!("a {type_name} wrapper"),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Shape {
Number,
Text,
Logical,
Binary,
Reference,
Enumeration,
Unresolved,
}
fn shape_of(schema: &Schema, type_name: &str, depth: u8) -> Shape {
if depth == 0 {
return Shape::Unresolved;
}
match type_name.to_ascii_uppercase().as_str() {
"REAL" | "INTEGER" | "NUMBER" => return Shape::Number,
"STRING" => return Shape::Text,
"BOOLEAN" | "LOGICAL" => return Shape::Logical,
"BINARY" => return Shape::Binary,
_ => {}
}
if schema.entity(type_name).is_some() {
return Shape::Reference;
}
let Some(def) = schema.type_def(type_name) else {
return Shape::Unresolved;
};
match &def.kind {
TypeKind::Defined(alias) => shape_of(schema, alias, depth - 1),
TypeKind::Enumeration(_) => Shape::Enumeration,
TypeKind::Select(members) => {
let mut shapes = members.iter().map(|m| shape_of(schema, m, depth - 1));
let Some(first) = shapes.next() else {
return Shape::Unresolved;
};
if shapes.all(|s| s == first) {
first
} else {
Shape::Unresolved
}
}
}
}
pub(crate) fn judge_value(schema: &Schema, type_name: &str, value: &Value) -> Verdict {
if matches!(value, Value::Null | Value::Derived) {
return Verdict::Fits;
}
match (form_of(schema, type_name), value) {
(Form::Unresolved, _) => Verdict::Fits,
(
Form::Typed,
Value::Typed {
type_name: wrapper,
value: inner,
},
) => {
if select_lists(schema, type_name, wrapper) == Some(false) {
Verdict::WrongType
} else {
judge_value(schema, wrapper, inner)
}
}
(Form::Typed, Value::Ref(_) | Value::List(_)) => shape_verdict(schema, type_name, value),
(Form::Typed, _) => {
if shape_of(schema, type_name, 16) == Shape::Reference {
Verdict::WrongType
} else {
Verdict::WrongForm {
typed_required: true,
}
}
}
(
Form::Bare,
Value::Typed {
type_name: wrapper, ..
},
) => {
if aliases_to(schema, wrapper, type_name) {
Verdict::WrongForm {
typed_required: false,
}
} else {
Verdict::WrongType
}
}
(Form::Bare, _) => shape_verdict(schema, type_name, value),
}
}
fn shape_verdict(schema: &Schema, type_name: &str, value: &Value) -> Verdict {
let fits = match shape_of(schema, type_name, 16) {
Shape::Unresolved => true,
Shape::Number => matches!(value, Value::Integer(_) | Value::Real(_)),
Shape::Text => matches!(value, Value::Text(_)),
Shape::Logical => matches!(value, Value::Bool(_) | Value::LogicalUnknown),
Shape::Binary => matches!(value, Value::Binary(_)),
Shape::Reference => matches!(value, Value::Ref(_)),
Shape::Enumeration => matches!(value, Value::Enum(_)),
};
if fits {
Verdict::Fits
} else {
Verdict::WrongType
}
}
pub(crate) fn aggregate_element(schema: &Schema, type_name: &str) -> Option<String> {
let mut current = type_name.to_owned();
for _ in 0..16 {
let TypeKind::Defined(rhs) = &schema.type_def(¤t)?.kind else {
return None;
};
let rhs = rhs.trim();
let keyword = rhs
.split(|c: char| !c.is_ascii_alphabetic())
.next()
.unwrap_or("")
.to_ascii_uppercase();
if matches!(keyword.as_str(), "LIST" | "SET" | "ARRAY" | "BAG") {
let (_, element) = rhs.split_once(" OF ")?;
let element = element
.trim()
.trim_start_matches("UNIQUE ")
.trim_start_matches("OPTIONAL ")
.trim();
return Some(element.to_owned());
}
current = rhs.to_owned();
}
None
}