386 lines
12 KiB
Rust
386 lines
12 KiB
Rust
use serde::{Deserialize, Serialize};
|
|
use serde_json::Value;
|
|
use std::collections::BTreeMap;
|
|
use std::sync::Arc;
|
|
|
|
// 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))
|
|
}
|
|
|
|
#[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")]
|
|
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::schema::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(rename = "allOf")]
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub all_of: Option<Vec<Arc<Schema>>>,
|
|
#[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")]
|
|
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(skip)]
|
|
pub compiled_format: Option<CompiledFormat>,
|
|
#[serde(skip)]
|
|
pub compiled_pattern: Option<CompiledRegex>,
|
|
#[serde(skip)]
|
|
pub compiled_pattern_properties: Option<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, Default)]
|
|
pub struct Schema {
|
|
#[serde(flatten)]
|
|
pub obj: SchemaObject,
|
|
#[serde(skip)]
|
|
pub always_fail: bool,
|
|
}
|
|
|
|
impl std::ops::Deref for Schema {
|
|
type Target = SchemaObject;
|
|
fn deref(&self) -> &Self::Target {
|
|
&self.obj
|
|
}
|
|
}
|
|
impl std::ops::DerefMut for Schema {
|
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
|
&mut self.obj
|
|
}
|
|
}
|
|
|
|
impl Schema {
|
|
pub fn compile_internals(&mut self) {
|
|
self.map_children(|child| child.compile_internals());
|
|
|
|
if let Some(format_str) = &self.obj.format
|
|
&& let Some(fmt) = crate::database::formats::FORMATS.get(format_str.as_str())
|
|
{
|
|
self.obj.compiled_format = Some(crate::database::schema::CompiledFormat::Func(fmt.func));
|
|
}
|
|
|
|
if let Some(pattern_str) = &self.obj.pattern
|
|
&& let Ok(re) = regex::Regex::new(pattern_str)
|
|
{
|
|
self.obj.compiled_pattern = Some(crate::database::schema::CompiledRegex(re));
|
|
}
|
|
|
|
if let Some(pattern_props) = &self.obj.pattern_properties {
|
|
let mut compiled = Vec::new();
|
|
for (k, v) in pattern_props {
|
|
if let Ok(re) = regex::Regex::new(k) {
|
|
compiled.push((crate::database::schema::CompiledRegex(re), v.clone()));
|
|
}
|
|
}
|
|
if !compiled.is_empty() {
|
|
self.obj.compiled_pattern_properties = Some(compiled);
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn harvest(&mut self, to_insert: &mut Vec<(String, Schema)>) {
|
|
if let Some(id) = &self.obj.id {
|
|
to_insert.push((id.clone(), self.clone()));
|
|
}
|
|
self.map_children(|child| child.harvest(to_insert));
|
|
}
|
|
|
|
pub fn map_children<F>(&mut self, mut f: F)
|
|
where
|
|
F: FnMut(&mut Schema),
|
|
{
|
|
if let Some(props) = &mut self.obj.properties {
|
|
for v in props.values_mut() {
|
|
let mut inner = (**v).clone();
|
|
f(&mut inner);
|
|
*v = Arc::new(inner);
|
|
}
|
|
}
|
|
|
|
if let Some(pattern_props) = &mut self.obj.pattern_properties {
|
|
for v in pattern_props.values_mut() {
|
|
let mut inner = (**v).clone();
|
|
f(&mut inner);
|
|
*v = Arc::new(inner);
|
|
}
|
|
}
|
|
|
|
let mut map_arr = |arr: &mut Vec<Arc<Schema>>| {
|
|
for v in arr.iter_mut() {
|
|
let mut inner = (**v).clone();
|
|
f(&mut inner);
|
|
*v = Arc::new(inner);
|
|
}
|
|
};
|
|
|
|
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);
|
|
}
|
|
|
|
let mut map_opt = |opt: &mut Option<Arc<Schema>>| {
|
|
if let Some(v) = opt {
|
|
let mut inner = (**v).clone();
|
|
f(&mut inner);
|
|
*v = Arc::new(inner);
|
|
}
|
|
};
|
|
|
|
map_opt(&mut self.obj.additional_properties);
|
|
map_opt(&mut self.obj.items);
|
|
map_opt(&mut self.obj.contains);
|
|
map_opt(&mut self.obj.property_names);
|
|
map_opt(&mut self.obj.not);
|
|
map_opt(&mut self.obj.if_);
|
|
map_opt(&mut self.obj.then_);
|
|
map_opt(&mut self.obj.else_);
|
|
}
|
|
}
|
|
|
|
impl<'de> Deserialize<'de> for Schema {
|
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
where
|
|
D: serde::Deserializer<'de>,
|
|
{
|
|
let v: Value = Deserialize::deserialize(deserializer)?;
|
|
|
|
if let Some(b) = v.as_bool() {
|
|
let mut obj = SchemaObject::default();
|
|
if b {
|
|
obj.extensible = Some(true);
|
|
}
|
|
return Ok(Schema {
|
|
obj,
|
|
always_fail: !b,
|
|
});
|
|
}
|
|
let mut obj: SchemaObject =
|
|
serde_json::from_value(v.clone()).map_err(serde::de::Error::custom)?;
|
|
|
|
// If a schema is effectively empty (except for potentially carrying an ID),
|
|
// it functions as a boolean `true` schema in Draft2020 which means it should not
|
|
// restrict additional properties natively
|
|
let is_empty = obj.type_.is_none()
|
|
&& obj.properties.is_none()
|
|
&& obj.pattern_properties.is_none()
|
|
&& obj.additional_properties.is_none()
|
|
&& obj.required.is_none()
|
|
&& obj.dependencies.is_none()
|
|
&& obj.items.is_none()
|
|
&& obj.prefix_items.is_none()
|
|
&& obj.contains.is_none()
|
|
&& obj.format.is_none()
|
|
&& obj.enum_.is_none()
|
|
&& obj.const_.is_none()
|
|
&& obj.all_of.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() {
|
|
obj.extensible = Some(true);
|
|
}
|
|
|
|
Ok(Schema {
|
|
obj,
|
|
always_fail: false,
|
|
})
|
|
}
|
|
}
|
|
|
|
#[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>),
|
|
}
|