massively improves the jspg validator by removing mathmatical functions like allOf, anyOf, ref, etc to effectively use discriminators and OOP with types to determine valid pathing an nno intersections, unions, or guesswork; added cases to replace the former conditionals
This commit is contained in:
@ -242,12 +242,14 @@ impl Database {
|
||||
if !visited.insert(current_id.clone()) {
|
||||
break; // Cycle detected
|
||||
}
|
||||
if let Some(ref_str) = &schema.obj.r#ref {
|
||||
current_id = ref_str.clone();
|
||||
depth += 1;
|
||||
} else {
|
||||
break;
|
||||
if let Some(crate::database::schema::SchemaTypeOrArray::Single(t)) = &schema.obj.type_ {
|
||||
if !crate::database::schema::is_primitive_type(t) {
|
||||
current_id = t.clone();
|
||||
depth += 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
depths.insert(id, depth);
|
||||
}
|
||||
@ -257,11 +259,13 @@ impl Database {
|
||||
fn collect_descendants(&mut self) {
|
||||
let mut direct_refs: HashMap<String, Vec<String>> = HashMap::new();
|
||||
for (id, schema) in &self.schemas {
|
||||
if let Some(ref_str) = &schema.obj.r#ref {
|
||||
direct_refs
|
||||
.entry(ref_str.clone())
|
||||
.or_default()
|
||||
.push(id.clone());
|
||||
if let Some(crate::database::schema::SchemaTypeOrArray::Single(t)) = &schema.obj.type_ {
|
||||
if !crate::database::schema::is_primitive_type(t) {
|
||||
direct_refs
|
||||
.entry(t.clone())
|
||||
.or_default()
|
||||
.push(id.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -33,15 +33,30 @@ where
|
||||
Ok(Some(v))
|
||||
}
|
||||
|
||||
pub fn is_primitive_type(t: &str) -> bool {
|
||||
matches!(
|
||||
t,
|
||||
"string" | "number" | "integer" | "boolean" | "object" | "array" | "null"
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Case {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub when: Option<Arc<Schema>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub then: Option<Arc<Schema>>,
|
||||
#[serde(rename = "else")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub else_: Option<Arc<Schema>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct SchemaObject {
|
||||
// Core Schema Keywords
|
||||
#[serde(rename = "$id")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub id: Option<String>,
|
||||
#[serde(rename = "$ref")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub r#ref: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@ -150,24 +165,14 @@ pub struct SchemaObject {
|
||||
pub exclusive_maximum: Option<f64>,
|
||||
|
||||
// Combining Keywords
|
||||
#[serde(rename = "allOf")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub all_of: Option<Vec<Arc<Schema>>>,
|
||||
pub cases: Option<Vec<Case>>,
|
||||
#[serde(rename = "oneOf")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub one_of: Option<Vec<Arc<Schema>>>,
|
||||
#[serde(rename = "not")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub not: Option<Arc<Schema>>,
|
||||
#[serde(rename = "if")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub if_: Option<Arc<Schema>>,
|
||||
#[serde(rename = "then")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub then_: Option<Arc<Schema>>,
|
||||
#[serde(rename = "else")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub else_: Option<Arc<Schema>>,
|
||||
|
||||
// Custom Vocabularies
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@ -300,35 +305,45 @@ impl Schema {
|
||||
let mut props = std::collections::BTreeMap::new();
|
||||
|
||||
// 1. Resolve INHERITANCE dependencies first
|
||||
if let Some(ref_id) = &self.obj.r#ref {
|
||||
if let Some(parent) = db.schemas.get(ref_id) {
|
||||
parent.compile(db, visited, errors);
|
||||
if let Some(p_props) = parent.obj.compiled_properties.get() {
|
||||
props.extend(p_props.clone());
|
||||
if let Some(crate::database::schema::SchemaTypeOrArray::Single(t)) = &self.obj.type_ {
|
||||
if !crate::database::schema::is_primitive_type(t) {
|
||||
if let Some(parent) = db.schemas.get(t) {
|
||||
parent.compile(db, visited, errors);
|
||||
if let Some(p_props) = parent.obj.compiled_properties.get() {
|
||||
props.extend(p_props.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(all_of) = &self.obj.all_of {
|
||||
for ao in all_of {
|
||||
ao.compile(db, visited, errors);
|
||||
if let Some(ao_props) = ao.obj.compiled_properties.get() {
|
||||
props.extend(ao_props.clone());
|
||||
if let Some(crate::database::schema::SchemaTypeOrArray::Multiple(types)) = &self.obj.type_ {
|
||||
let mut custom_type_count = 0;
|
||||
for t in types {
|
||||
if !crate::database::schema::is_primitive_type(t) {
|
||||
custom_type_count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(then_schema) = &self.obj.then_ {
|
||||
then_schema.compile(db, visited, errors);
|
||||
if let Some(t_props) = then_schema.obj.compiled_properties.get() {
|
||||
props.extend(t_props.clone());
|
||||
if custom_type_count > 1 {
|
||||
errors.push(crate::drop::Error {
|
||||
code: "MULTIPLE_INHERITANCE_PROHIBITED".to_string(),
|
||||
message: format!(
|
||||
"Schema '{}' attempts to extend multiple custom object pointers in its type array. Use 'oneOf' for polymorphism and tagged unions.",
|
||||
self.obj.identifier().unwrap_or("unknown".to_string())
|
||||
),
|
||||
details: crate::drop::ErrorDetails {
|
||||
path: self.obj.identifier().unwrap_or("unknown".to_string()),
|
||||
..Default::default()
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(else_schema) = &self.obj.else_ {
|
||||
else_schema.compile(db, visited, errors);
|
||||
if let Some(e_props) = else_schema.obj.compiled_properties.get() {
|
||||
props.extend(e_props.clone());
|
||||
for t in types {
|
||||
if !crate::database::schema::is_primitive_type(t) {
|
||||
if let Some(parent) = db.schemas.get(t) {
|
||||
parent.compile(db, visited, errors);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -382,11 +397,18 @@ impl Schema {
|
||||
if let Some(child) = &self.obj.contains {
|
||||
child.compile(db, visited, errors);
|
||||
}
|
||||
if let Some(child) = &self.obj.property_names {
|
||||
child.compile(db, visited, errors);
|
||||
}
|
||||
if let Some(child) = &self.obj.if_ {
|
||||
child.compile(db, visited, errors);
|
||||
if let Some(cases) = &self.obj.cases {
|
||||
for c in cases {
|
||||
if let Some(child) = &c.when {
|
||||
child.compile(db, visited, errors);
|
||||
}
|
||||
if let Some(child) = &c.then {
|
||||
child.compile(db, visited, errors);
|
||||
}
|
||||
if let Some(child) = &c.else_ {
|
||||
child.compile(db, visited, errors);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(id) = &self.obj.id {
|
||||
@ -422,8 +444,10 @@ impl Schema {
|
||||
Self::validate_identifier(id, "$id", errors);
|
||||
to_insert.push((id.clone(), self.clone()));
|
||||
}
|
||||
if let Some(r#ref) = &self.obj.r#ref {
|
||||
Self::validate_identifier(r#ref, "$ref", errors);
|
||||
if let Some(crate::database::schema::SchemaTypeOrArray::Single(t)) = &self.obj.type_ {
|
||||
if !crate::database::schema::is_primitive_type(t) {
|
||||
Self::validate_identifier(t, "type", errors);
|
||||
}
|
||||
}
|
||||
if let Some(family) = &self.obj.family {
|
||||
Self::validate_identifier(family, "$family", errors);
|
||||
@ -431,9 +455,13 @@ impl Schema {
|
||||
|
||||
// Is this schema an inline ad-hoc composition?
|
||||
// Meaning it has a tracking context, lacks an explicit $id, but extends an Entity ref with explicit properties!
|
||||
if self.obj.id.is_none() && self.obj.r#ref.is_some() && self.obj.properties.is_some() {
|
||||
if let Some(ref path) = tracking_path {
|
||||
to_insert.push((path.clone(), self.clone()));
|
||||
if self.obj.id.is_none() && self.obj.properties.is_some() {
|
||||
if let Some(crate::database::schema::SchemaTypeOrArray::Single(t)) = &self.obj.type_ {
|
||||
if !crate::database::schema::is_primitive_type(t) {
|
||||
if let Some(ref path) = tracking_path {
|
||||
to_insert.push((path.clone(), self.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -478,9 +506,7 @@ impl Schema {
|
||||
if let Some(arr) = &mut self.obj.prefix_items {
|
||||
map_arr(arr);
|
||||
}
|
||||
if let Some(arr) = &mut self.obj.all_of {
|
||||
map_arr(arr);
|
||||
}
|
||||
|
||||
if let Some(arr) = &mut self.obj.one_of {
|
||||
map_arr(arr);
|
||||
}
|
||||
@ -503,9 +529,25 @@ impl Schema {
|
||||
map_opt(&mut self.obj.not, false);
|
||||
map_opt(&mut self.obj.contains, false);
|
||||
map_opt(&mut self.obj.property_names, false);
|
||||
map_opt(&mut self.obj.if_, false);
|
||||
map_opt(&mut self.obj.then_, false);
|
||||
map_opt(&mut self.obj.else_, false);
|
||||
if let Some(cases) = &mut self.obj.cases {
|
||||
for c in cases.iter_mut() {
|
||||
if let Some(when) = &mut c.when {
|
||||
let mut inner = (**when).clone();
|
||||
inner.collect_schemas(origin_path.clone(), to_insert, errors);
|
||||
*when = Arc::new(inner);
|
||||
}
|
||||
if let Some(then) = &mut c.then {
|
||||
let mut inner = (**then).clone();
|
||||
inner.collect_schemas(origin_path.clone(), to_insert, errors);
|
||||
*then = Arc::new(inner);
|
||||
}
|
||||
if let Some(else_) = &mut c.else_ {
|
||||
let mut inner = (**else_).clone();
|
||||
inner.collect_schemas(origin_path.clone(), to_insert, errors);
|
||||
*else_ = Arc::new(inner);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Dynamically infers and compiles all structural database relationships between this Schema
|
||||
@ -823,13 +865,9 @@ impl<'de> Deserialize<'de> for Schema {
|
||||
&& obj.format.is_none()
|
||||
&& obj.enum_.is_none()
|
||||
&& obj.const_.is_none()
|
||||
&& obj.all_of.is_none()
|
||||
&& obj.cases.is_none()
|
||||
&& obj.one_of.is_none()
|
||||
&& obj.not.is_none()
|
||||
&& obj.if_.is_none()
|
||||
&& obj.then_.is_none()
|
||||
&& obj.else_.is_none()
|
||||
&& obj.r#ref.is_none()
|
||||
&& obj.family.is_none();
|
||||
|
||||
if is_empty && obj.extensible.is_none() {
|
||||
@ -845,11 +883,15 @@ impl<'de> Deserialize<'de> for Schema {
|
||||
|
||||
impl SchemaObject {
|
||||
pub fn identifier(&self) -> Option<String> {
|
||||
if let Some(lookup_key) = self.id.as_ref().or(self.r#ref.as_ref()) {
|
||||
Some(lookup_key.split('.').next_back().unwrap_or("").to_string())
|
||||
} else {
|
||||
None
|
||||
if let Some(id) = &self.id {
|
||||
return Some(id.split('.').next_back().unwrap_or("").to_string());
|
||||
}
|
||||
if let Some(SchemaTypeOrArray::Single(t)) = &self.type_ {
|
||||
if !is_primitive_type(t) {
|
||||
return Some(t.split('.').next_back().unwrap_or("").to_string());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user