use crate::document::{RawNode, Scalar as DocScalar};
use crate::error::MaterializeError;
use crate::schema::{ErrorCode, FieldType, Resolved, ScalarKind, Schema, ValidationResult};
use num_traits::{FromPrimitive, ToPrimitive};
pub fn materialize(node: &RawNode, schema: Option<&Schema>) -> Result<RawNode, MaterializeError> {
let Some(schema) = schema else {
return Ok(node.clone());
};
let mut res = ValidationResult::new();
let root_ty = FieldType::Ref(schema.root().clone());
let mut path = String::from("$");
let out = materialize_type(node, schema, &root_ty, &mut path, &mut res);
if !res.ok() {
return Err(MaterializeError(res));
}
Ok(out)
}
fn materialize_type(
node: &RawNode,
schema: &Schema,
ty: &FieldType,
path: &mut String,
res: &mut ValidationResult,
) -> RawNode {
match schema.resolve(ty) {
Resolved::Any => node.clone(),
Resolved::Scalar(s) => materialize_scalar(node, s.kind(), s.is_nullable(), path, res),
Resolved::Record(rec) => materialize_record(node, schema, rec, path, res),
}
}
fn materialize_record(
node: &RawNode,
schema: &Schema,
rec: &crate::schema::Record,
path: &mut String,
res: &mut ValidationResult,
) -> RawNode {
let RawNode::Edges(edges) = node else {
res.add(
path.as_str(),
"expected an object, got a value",
ErrorCode::ShapeMismatch,
);
return node.clone();
};
let mut out: Vec<(String, RawNode)> = Vec::with_capacity(edges.len());
let mut counts: indexmap::IndexMap<&str, usize> = indexmap::IndexMap::new();
for (label, child) in edges {
let i = *counts.entry(label.as_str()).or_insert(0);
counts.insert(label.as_str(), i + 1);
let base = path.len();
crate::report::push_child_path(path, label, i);
match rec.field(label) {
None => {
res.add(
path.as_str(),
"unexpected field",
ErrorCode::UnexpectedField,
);
out.push((label.clone(), child.clone()));
}
Some(f) => {
let m = materialize_type(child, schema, &f.ty, path, res);
out.push((label.clone(), m));
}
}
path.truncate(base);
}
for f in rec.fields() {
let c = counts.get(f.label.as_str()).copied().unwrap_or(0);
if c < f.min || f.max.is_some_and(|max| c > max) {
res.add(
path.as_str(),
format!(
"field {:?} occurs {} time(s), expected {}",
f.label,
c,
f.cardinality_str()
),
ErrorCode::Cardinality,
);
}
}
RawNode::Edges(out)
}
fn materialize_scalar(
node: &RawNode,
kind: ScalarKind,
nullable: bool,
path: &str,
res: &mut ValidationResult,
) -> RawNode {
let value = match node {
RawNode::Leaf(v) => v,
RawNode::Edges(_) => {
res.add(
path,
format!("expected a {} value, got an object", kind.as_str()),
ErrorCode::ShapeMismatch,
);
return node.clone();
}
};
if matches!(value, DocScalar::Null) {
if !nullable {
res.add(path, "null not allowed here", ErrorCode::NullNotAllowed);
}
return RawNode::Leaf(DocScalar::Null);
}
if let Some(upgraded) = try_upgrade(value, kind) {
return RawNode::Leaf(upgraded);
}
res.add(
path,
format!(
"{value} cannot be read as {} (not a value-exact conversion)",
kind.as_str()
),
ErrorCode::TypeMismatch,
);
node.clone()
}
fn try_upgrade(value: &DocScalar, kind: ScalarKind) -> Option<DocScalar> {
match (kind, value) {
(ScalarKind::String, DocScalar::Str(_)) => Some(value.clone()),
(ScalarKind::Boolean, DocScalar::Bool(_)) => Some(value.clone()),
(ScalarKind::Integer, DocScalar::Int(_)) => Some(value.clone()),
(ScalarKind::Integer, DocScalar::Float(f)) => {
if f.is_finite() && f.fract() == 0.0 {
num_bigint::BigInt::from_f64(*f).map(DocScalar::Int)
} else {
None
}
}
(ScalarKind::Number, DocScalar::Int(i)) => {
if let Some(f) = i.to_f64()
&& f.is_finite()
&& let Some(round_tripped) = num_bigint::BigInt::from_f64(f)
&& &round_tripped == i
{
return Some(DocScalar::Float(f));
}
None
}
(ScalarKind::Number, DocScalar::Float(_)) => Some(value.clone()),
(ScalarKind::Date, DocScalar::Str(s)) if crate::schema::is_iso_date(s) => {
Some(DocScalar::Date(s.clone()))
}
(ScalarKind::Time, DocScalar::Str(s)) if crate::schema::is_iso_time(s) => {
Some(DocScalar::Time(crate::schema::canonicalize_iso_time(s)))
}
(ScalarKind::Datetime, DocScalar::Str(s))
if crate::schema::is_iso_datetime(s) && !crate::schema::is_iso_date(s) =>
{
Some(DocScalar::Datetime(
crate::schema::canonicalize_iso_datetime(s),
))
}
(ScalarKind::Date, DocScalar::Date(_))
| (ScalarKind::Time, DocScalar::Time(_))
| (ScalarKind::Datetime, DocScalar::Datetime(_)) => Some(value.clone()),
_ => None,
}
}
#[cfg(test)]
mod tests;