use serde::{Deserialize, Serialize}; use serde_json::Value; use std::collections::BTreeMap; use std::sync::Arc; use std::sync::OnceLock; use crate::database::schema::Schema; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Case { #[serde(skip_serializing_if = "Option::is_none")] pub when: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub then: Option>, #[serde(rename = "else")] #[serde(skip_serializing_if = "Option::is_none")] pub else_: Option>, } #[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, #[serde(skip_serializing_if = "Option::is_none")] pub description: Option, #[serde(skip_serializing_if = "Option::is_none")] pub title: Option, #[serde(default)] // Allow missing type #[serde(rename = "type")] #[serde(skip_serializing_if = "Option::is_none")] pub type_: Option, // Handles string or array of strings // Object Keywords #[serde(skip_serializing_if = "Option::is_none")] pub properties: Option>>, #[serde(rename = "patternProperties")] #[serde(skip_serializing_if = "Option::is_none")] pub pattern_properties: Option>>, #[serde(rename = "additionalProperties")] #[serde(skip_serializing_if = "Option::is_none")] pub additional_properties: Option>, #[serde(rename = "$family")] #[serde(skip_serializing_if = "Option::is_none")] pub family: Option, #[serde(skip_serializing_if = "Option::is_none")] pub required: Option>, // dependencies can be schema dependencies or property dependencies #[serde(skip_serializing_if = "Option::is_none")] pub dependencies: Option>, // Array Keywords #[serde(rename = "items")] #[serde(skip_serializing_if = "Option::is_none")] pub items: Option>, #[serde(rename = "prefixItems")] #[serde(skip_serializing_if = "Option::is_none")] pub prefix_items: Option>>, // String Validation #[serde(rename = "minLength")] #[serde(skip_serializing_if = "Option::is_none")] pub min_length: Option, #[serde(rename = "maxLength")] #[serde(skip_serializing_if = "Option::is_none")] pub max_length: Option, #[serde(skip_serializing_if = "Option::is_none")] pub pattern: Option, // Array Validation #[serde(rename = "minItems")] #[serde(skip_serializing_if = "Option::is_none")] pub min_items: Option, #[serde(rename = "maxItems")] #[serde(skip_serializing_if = "Option::is_none")] pub max_items: Option, #[serde(rename = "uniqueItems")] #[serde(skip_serializing_if = "Option::is_none")] pub unique_items: Option, #[serde(rename = "contains")] #[serde(skip_serializing_if = "Option::is_none")] pub contains: Option>, #[serde(rename = "minContains")] #[serde(skip_serializing_if = "Option::is_none")] pub min_contains: Option, #[serde(rename = "maxContains")] #[serde(skip_serializing_if = "Option::is_none")] pub max_contains: Option, // Object Validation #[serde(rename = "minProperties")] #[serde(skip_serializing_if = "Option::is_none")] pub min_properties: Option, #[serde(rename = "maxProperties")] #[serde(skip_serializing_if = "Option::is_none")] pub max_properties: Option, #[serde(rename = "propertyNames")] #[serde(skip_serializing_if = "Option::is_none")] pub property_names: Option>, // Numeric Validation #[serde(skip_serializing_if = "Option::is_none")] pub format: Option, #[serde(rename = "enum")] #[serde(skip_serializing_if = "Option::is_none")] pub enum_: Option>, // `enum` is a reserved keyword in Rust #[serde( default, rename = "const", deserialize_with = "crate::database::object::deserialize_some" )] #[serde(skip_serializing_if = "Option::is_none")] pub const_: Option, // Numeric Validation #[serde(rename = "multipleOf")] #[serde(skip_serializing_if = "Option::is_none")] pub multiple_of: Option, #[serde(skip_serializing_if = "Option::is_none")] pub minimum: Option, #[serde(skip_serializing_if = "Option::is_none")] pub maximum: Option, #[serde(rename = "exclusiveMinimum")] #[serde(skip_serializing_if = "Option::is_none")] pub exclusive_minimum: Option, #[serde(rename = "exclusiveMaximum")] #[serde(skip_serializing_if = "Option::is_none")] pub exclusive_maximum: Option, // Combining Keywords #[serde(skip_serializing_if = "Option::is_none")] pub cases: Option>, #[serde(rename = "oneOf")] #[serde(skip_serializing_if = "Option::is_none")] pub one_of: Option>>, #[serde(rename = "not")] #[serde(skip_serializing_if = "Option::is_none")] pub not: Option>, // Custom Vocabularies #[serde(skip_serializing_if = "Option::is_none")] pub form: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub display: Option>, #[serde(rename = "enumNames")] #[serde(skip_serializing_if = "Option::is_none")] pub enum_names: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub control: Option, #[serde(skip_serializing_if = "Option::is_none")] pub actions: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub computer: Option, #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] pub extensible: Option, #[serde(rename = "compiledProperties")] #[serde(skip_deserializing)] #[serde(skip_serializing_if = "crate::database::object::is_once_lock_vec_empty")] #[serde(serialize_with = "crate::database::object::serialize_once_lock")] pub compiled_property_names: OnceLock>, #[serde(skip)] pub compiled_properties: OnceLock>>, #[serde(rename = "compiledDiscriminator")] #[serde(skip_deserializing)] #[serde(skip_serializing_if = "crate::database::object::is_once_lock_string_empty")] #[serde(serialize_with = "crate::database::object::serialize_once_lock")] pub compiled_discriminator: OnceLock, #[serde(rename = "compiledOptions")] #[serde(skip_deserializing)] #[serde(skip_serializing_if = "crate::database::object::is_once_lock_map_empty")] #[serde(serialize_with = "crate::database::object::serialize_once_lock")] pub compiled_options: OnceLock>, #[serde(rename = "compiledEdges")] #[serde(skip_deserializing)] #[serde(skip_serializing_if = "crate::database::object::is_once_lock_map_empty")] #[serde(serialize_with = "crate::database::object::serialize_once_lock")] pub compiled_edges: OnceLock>, #[serde(skip)] pub compiled_format: OnceLock, #[serde(skip)] pub compiled_pattern: OnceLock, #[serde(skip)] pub compiled_pattern_properties: OnceLock)>>, } /// Represents a compiled format validator #[derive(Clone)] pub enum CompiledFormat { Func(fn(&serde_json::Value) -> Result<(), Box>), Regex(regex::Regex), } impl std::fmt::Debug for CompiledFormat { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { CompiledFormat::Func(_) => write!(f, "CompiledFormat::Func(...)"), CompiledFormat::Regex(r) => write!(f, "CompiledFormat::Regex({:?})", r), } } } /// A wrapper for compiled regex patterns #[derive(Debug, Clone)] pub struct CompiledRegex(pub regex::Regex); #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] pub enum SchemaTypeOrArray { Single(String), Multiple(Vec), } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Action { #[serde(skip_serializing_if = "Option::is_none")] pub navigate: Option, #[serde(skip_serializing_if = "Option::is_none")] pub punc: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] pub enum Dependency { Props(Vec), Schema(Arc), } pub fn serialize_once_lock( lock: &OnceLock, serializer: S, ) -> Result { if let Some(val) = lock.get() { val.serialize(serializer) } else { serializer.serialize_none() } } pub fn is_once_lock_map_empty(lock: &OnceLock>) -> bool { lock.get().map_or(true, |m| m.is_empty()) } pub fn is_once_lock_vec_empty(lock: &OnceLock>) -> bool { lock.get().map_or(true, |v| v.is_empty()) } pub fn is_once_lock_string_empty(lock: &OnceLock) -> bool { lock.get().map_or(true, |s| s.is_empty()) } // Schema mirrors the Go Punc Generator's schema struct for consistency. // It is an order-preserving representation of a JSON Schema. pub fn deserialize_some<'de, D>(deserializer: D) -> Result, D::Error> where D: serde::Deserializer<'de>, { let v = Value::deserialize(deserializer)?; Ok(Some(v)) } pub fn is_primitive_type(t: &str) -> bool { matches!( t, "string" | "number" | "integer" | "boolean" | "object" | "array" | "null" ) } impl SchemaObject { pub fn identifier(&self) -> Option { 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 } pub fn get_discriminator_value(&self, dim: &str) -> Option { let is_split = self .compiled_properties .get() .map_or(false, |p| p.contains_key("kind")); if let Some(id) = &self.id { if id.contains("light.person") || id.contains("light.organization") { println!( "[DEBUG SPLIT] ID: {}, dim: {}, is_split: {:?}, props: {:?}", id, dim, is_split, self .compiled_properties .get() .map(|p| p.keys().cloned().collect::>()) ); } } if let Some(props) = self.compiled_properties.get() { if let Some(prop_schema) = props.get(dim) { if let Some(c) = &prop_schema.obj.const_ { if let Some(s) = c.as_str() { return Some(s.to_string()); } } if let Some(e) = &prop_schema.obj.enum_ { if e.len() == 1 { if let Some(s) = e[0].as_str() { return Some(s.to_string()); } } } } } if dim == "kind" { if let Some(id) = &self.id { let base = id.split('/').last().unwrap_or(id); if let Some(idx) = base.rfind('.') { return Some(base[..idx].to_string()); } } if let Some(SchemaTypeOrArray::Single(t)) = &self.type_ { if !is_primitive_type(t) { let base = t.split('/').last().unwrap_or(t); if let Some(idx) = base.rfind('.') { return Some(base[..idx].to_string()); } } } } if dim == "type" { if let Some(id) = &self.id { let base = id.split('/').last().unwrap_or(id); if is_split { return Some(base.split('.').next_back().unwrap_or(base).to_string()); } else { return Some(base.to_string()); } } if let Some(SchemaTypeOrArray::Single(t)) = &self.type_ { if !is_primitive_type(t) { let base = t.split('/').last().unwrap_or(t); if is_split { return Some(base.split('.').next_back().unwrap_or(base).to_string()); } else { return Some(base.to_string()); } } } } None } }