368 lines
12 KiB
Rust
368 lines
12 KiB
Rust
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<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(skip_serializing_if = "Option::is_none")]
|
|
pub description: Option<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub title: Option<String>,
|
|
#[serde(default)] // Allow missing type
|
|
#[serde(rename = "type")]
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub type_: Option<SchemaTypeOrArray>, // Handles string or array of strings
|
|
|
|
// Object Keywords
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub properties: Option<BTreeMap<String, Arc<Schema>>>,
|
|
#[serde(rename = "patternProperties")]
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub pattern_properties: Option<BTreeMap<String, Arc<Schema>>>,
|
|
#[serde(rename = "additionalProperties")]
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub additional_properties: Option<Arc<Schema>>,
|
|
#[serde(rename = "$family")]
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub family: Option<String>,
|
|
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub required: Option<Vec<String>>,
|
|
|
|
// dependencies can be schema dependencies or property dependencies
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub dependencies: Option<BTreeMap<String, Dependency>>,
|
|
|
|
// Array Keywords
|
|
#[serde(rename = "items")]
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub items: Option<Arc<Schema>>,
|
|
#[serde(rename = "prefixItems")]
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub prefix_items: Option<Vec<Arc<Schema>>>,
|
|
|
|
// String Validation
|
|
#[serde(rename = "minLength")]
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub min_length: Option<f64>,
|
|
#[serde(rename = "maxLength")]
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub max_length: Option<f64>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub pattern: Option<String>,
|
|
|
|
// Array Validation
|
|
#[serde(rename = "minItems")]
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub min_items: Option<f64>,
|
|
#[serde(rename = "maxItems")]
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub max_items: Option<f64>,
|
|
#[serde(rename = "uniqueItems")]
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub unique_items: Option<bool>,
|
|
#[serde(rename = "contains")]
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub contains: Option<Arc<Schema>>,
|
|
#[serde(rename = "minContains")]
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub min_contains: Option<f64>,
|
|
#[serde(rename = "maxContains")]
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub max_contains: Option<f64>,
|
|
|
|
// Object Validation
|
|
#[serde(rename = "minProperties")]
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub min_properties: Option<f64>,
|
|
#[serde(rename = "maxProperties")]
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub max_properties: Option<f64>,
|
|
#[serde(rename = "propertyNames")]
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub property_names: Option<Arc<Schema>>,
|
|
|
|
// Numeric Validation
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub format: Option<String>,
|
|
#[serde(rename = "enum")]
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub enum_: Option<Vec<Value>>, // `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<Value>,
|
|
|
|
// Numeric Validation
|
|
#[serde(rename = "multipleOf")]
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub multiple_of: Option<f64>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub minimum: Option<f64>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub maximum: Option<f64>,
|
|
#[serde(rename = "exclusiveMinimum")]
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub exclusive_minimum: Option<f64>,
|
|
#[serde(rename = "exclusiveMaximum")]
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub exclusive_maximum: Option<f64>,
|
|
|
|
// Combining Keywords
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
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>>,
|
|
|
|
// Custom Vocabularies
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub form: Option<Vec<String>>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub display: Option<Vec<String>>,
|
|
#[serde(rename = "enumNames")]
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub enum_names: Option<Vec<String>>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub control: Option<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub actions: Option<BTreeMap<String, Action>>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub computer: Option<String>,
|
|
#[serde(default)]
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub extensible: Option<bool>,
|
|
|
|
#[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<Vec<String>>,
|
|
|
|
#[serde(skip)]
|
|
pub compiled_properties: OnceLock<BTreeMap<String, Arc<Schema>>>,
|
|
|
|
#[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<String>,
|
|
|
|
#[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<BTreeMap<String, String>>,
|
|
|
|
#[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<BTreeMap<String, crate::database::edge::Edge>>,
|
|
|
|
#[serde(skip)]
|
|
pub compiled_format: OnceLock<CompiledFormat>,
|
|
#[serde(skip)]
|
|
pub compiled_pattern: OnceLock<CompiledRegex>,
|
|
#[serde(skip)]
|
|
pub compiled_pattern_properties: OnceLock<Vec<(CompiledRegex, Arc<Schema>)>>,
|
|
}
|
|
|
|
/// Represents a compiled format validator
|
|
#[derive(Clone)]
|
|
pub enum CompiledFormat {
|
|
Func(fn(&serde_json::Value) -> Result<(), Box<dyn std::error::Error + Send + Sync>>),
|
|
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<String>),
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct Action {
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub navigate: Option<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub punc: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
#[serde(untagged)]
|
|
pub enum Dependency {
|
|
Props(Vec<String>),
|
|
Schema(Arc<Schema>),
|
|
}
|
|
|
|
pub fn serialize_once_lock<T: serde::Serialize, S: serde::Serializer>(
|
|
lock: &OnceLock<T>,
|
|
serializer: S,
|
|
) -> Result<S::Ok, S::Error> {
|
|
if let Some(val) = lock.get() {
|
|
val.serialize(serializer)
|
|
} else {
|
|
serializer.serialize_none()
|
|
}
|
|
}
|
|
|
|
pub fn is_once_lock_map_empty<K, V>(lock: &OnceLock<std::collections::BTreeMap<K, V>>) -> bool {
|
|
lock.get().map_or(true, |m| m.is_empty())
|
|
}
|
|
|
|
pub fn is_once_lock_vec_empty<T>(lock: &OnceLock<Vec<T>>) -> bool {
|
|
lock.get().map_or(true, |v| v.is_empty())
|
|
}
|
|
|
|
pub fn is_once_lock_string_empty(lock: &OnceLock<String>) -> 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<Option<Value>, 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<String> {
|
|
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<String> {
|
|
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::<Vec<_>>())
|
|
);
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|