Files
jspg/src/database/compile/collection.rs
2026-04-17 00:38:54 -04:00

164 lines
4.9 KiB
Rust

use crate::database::schema::Schema;
use std::sync::Arc;
impl Schema {
#[allow(unused_variables)]
pub(crate) fn validate_identifier(
id: &str,
field_name: &str,
root_id: &str,
path: &str,
errors: &mut Vec<crate::drop::Error>,
) {
#[cfg(not(test))]
for c in id.chars() {
if !c.is_ascii_lowercase() && !c.is_ascii_digit() && c != '_' && c != '.' {
errors.push(crate::drop::Error {
code: "INVALID_IDENTIFIER".to_string(),
message: format!(
"Invalid character '{}' in JSON Schema '{}' property: '{}'. Identifiers must exclusively contain [a-z0-9_.]",
c, field_name, id
),
details: crate::drop::ErrorDetails {
path: Some(path.to_string()),
schema: Some(root_id.to_string()),
..Default::default()
},
});
return;
}
}
}
pub fn collect_schemas(
schema_arc: &Arc<Schema>,
root_id: &str,
path: String,
to_insert: &mut Vec<(String, Arc<Schema>)>,
errors: &mut Vec<crate::drop::Error>,
) {
if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = &schema_arc.obj.type_ {
if t == "array" {
if let Some(items) = &schema_arc.obj.items {
if let Some(crate::database::object::SchemaTypeOrArray::Single(it)) = &items.obj.type_ {
if !crate::database::object::is_primitive_type(it) {
if items.obj.properties.is_some() || items.obj.cases.is_some() {
to_insert.push((path.clone(), Arc::clone(schema_arc)));
}
}
}
}
} else if !crate::database::object::is_primitive_type(t) {
Self::validate_identifier(t, "type", root_id, &path, errors);
// Is this an explicit inline ad-hoc composition?
if schema_arc.obj.properties.is_some() || schema_arc.obj.cases.is_some() {
to_insert.push((path.clone(), Arc::clone(schema_arc)));
}
}
}
if let Some(family) = &schema_arc.obj.family {
Self::validate_identifier(family, "family", root_id, &path, errors);
}
Self::collect_child_schemas(schema_arc, root_id, path, to_insert, errors);
}
pub fn collect_child_schemas(
schema_arc: &Arc<Schema>,
root_id: &str,
path: String,
to_insert: &mut Vec<(String, Arc<Schema>)>,
errors: &mut Vec<crate::drop::Error>,
) {
if let Some(props) = &schema_arc.obj.properties {
for (k, v) in props.iter() {
let next_path = format!("{}/{}", path, k);
Self::collect_schemas(v, root_id, next_path, to_insert, errors);
}
}
if let Some(pattern_props) = &schema_arc.obj.pattern_properties {
for (k, v) in pattern_props.iter() {
let next_path = format!("{}/{}", path, k);
Self::collect_schemas(v, root_id, next_path, to_insert, errors);
}
}
let mut map_arr = |arr: &Vec<Arc<Schema>>, sub: &str| {
for (i, v) in arr.iter().enumerate() {
Self::collect_schemas(
v,
root_id,
format!("{}/{}/{}", path, sub, i),
to_insert,
errors,
);
}
};
if let Some(arr) = &schema_arc.obj.prefix_items {
map_arr(arr, "prefixItems");
}
if let Some(arr) = &schema_arc.obj.one_of {
map_arr(arr, "oneOf");
}
let mut map_opt = |opt: &Option<Arc<Schema>>, pass_path: bool, sub: &str| {
if let Some(v) = opt {
if pass_path {
// Arrays explicitly push their wrapper natively.
// 'items' becomes a transparent conduit, bypassing self-promotion and skipping the '/items' suffix.
Self::collect_child_schemas(v, root_id, path.clone(), to_insert, errors);
} else {
Self::collect_child_schemas(v, root_id, format!("{}/{}", path, sub), to_insert, errors);
}
}
};
map_opt(
&schema_arc.obj.additional_properties,
false,
"additionalProperties",
);
map_opt(&schema_arc.obj.items, true, "items");
map_opt(&schema_arc.obj.not, false, "not");
map_opt(&schema_arc.obj.contains, false, "contains");
map_opt(&schema_arc.obj.property_names, false, "propertyNames");
if let Some(cases) = &schema_arc.obj.cases {
for (i, c) in cases.iter().enumerate() {
if let Some(when) = &c.when {
Self::collect_schemas(
when,
root_id,
format!("{}/cases/{}/when", path, i),
to_insert,
errors,
);
}
if let Some(then) = &c.then {
Self::collect_schemas(
then,
root_id,
format!("{}/cases/{}/then", path, i),
to_insert,
errors,
);
}
if let Some(else_) = &c.else_ {
Self::collect_schemas(
else_,
root_id,
format!("{}/cases/{}/else", path, i),
to_insert,
errors,
);
}
}
}
}
}