use indexmap::IndexMap;
use crate::graph::schema::SchemaPattern;
use crate::value::{ElementProperty, Number};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum JsonType {
Null,
Boolean,
Integer,
Number,
String,
Array,
Object,
}
impl JsonType {
pub const ALL: [JsonType; 7] = [
JsonType::Null,
JsonType::Boolean,
JsonType::Integer,
JsonType::Number,
JsonType::String,
JsonType::Array,
JsonType::Object,
];
pub fn as_str(self) -> &'static str {
match self {
JsonType::Null => "null",
JsonType::Boolean => "boolean",
JsonType::Integer => "integer",
JsonType::Number => "number",
JsonType::String => "string",
JsonType::Array => "array",
JsonType::Object => "object",
}
}
pub fn from_name(name: &str) -> Option<Self> {
JsonType::ALL.into_iter().find(|ty| ty.as_str() == name)
}
pub(crate) fn engine_name(self, format: Option<SchemaFormat>) -> &'static str {
match self {
JsonType::Null => "null",
JsonType::Boolean => "boolean",
JsonType::Integer => "int64",
JsonType::Number => "float64",
JsonType::String => match format {
Some(SchemaFormat::Uuid) => "uuid",
None => "string",
},
JsonType::Array => "array",
JsonType::Object => "object",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum SchemaFormat {
Uuid,
}
impl SchemaFormat {
pub fn as_str(self) -> &'static str {
match self {
SchemaFormat::Uuid => "uuid",
}
}
}
#[derive(Debug, Clone, PartialEq, Default)]
#[non_exhaustive]
pub struct Constraints {
pub minimum: Option<Number>,
pub maximum: Option<Number>,
pub exclusive_minimum: Option<Number>,
pub exclusive_maximum: Option<Number>,
pub min_length: Option<u64>,
pub max_length: Option<u64>,
pub pattern: Option<SchemaPattern>,
pub min_items: Option<u64>,
pub max_items: Option<u64>,
pub enum_values: Option<Vec<ElementProperty>>,
pub const_value: Option<ElementProperty>,
}
impl Constraints {
pub fn is_empty(&self) -> bool {
*self == Constraints::default()
}
}
#[derive(Debug, Clone, PartialEq, Default)]
#[non_exhaustive]
pub struct Annotations {
pub title: Option<String>,
pub description: Option<String>,
pub comment: Option<String>,
pub examples: Option<Vec<ElementProperty>>,
pub deprecated: Option<bool>,
pub default: Option<ElementProperty>,
}
impl Annotations {
pub fn is_empty(&self) -> bool {
*self == Annotations::default()
}
}
#[derive(Debug, Clone, PartialEq, Default)]
#[non_exhaustive]
pub struct PropertySchema {
pub types: Option<Vec<JsonType>>,
pub format: Option<SchemaFormat>,
pub items: Option<Box<PropertySchema>>,
pub properties: IndexMap<String, PropertySchema>,
pub required: Vec<String>,
pub additional_properties: Option<bool>,
pub any_of: Option<Vec<PropertySchema>>,
pub constraints: Option<Box<Constraints>>,
pub annotations: Option<Box<Annotations>>,
}
impl PropertySchema {
pub fn any() -> Self {
Self::default()
}
pub fn of(ty: JsonType) -> Self {
Self {
types: Some(vec![ty]),
..Self::default()
}
}
pub fn of_types(types: &[JsonType]) -> Self {
Self {
types: Some(types.to_vec()),
..Self::default()
}
}
pub fn null() -> Self {
Self::of(JsonType::Null)
}
pub fn boolean() -> Self {
Self::of(JsonType::Boolean)
}
pub fn integer() -> Self {
Self::of(JsonType::Integer)
}
pub fn number() -> Self {
Self::of(JsonType::Number)
}
pub fn string() -> Self {
Self::of(JsonType::String)
}
pub fn uuid() -> Self {
Self {
format: Some(SchemaFormat::Uuid),
..Self::of(JsonType::String)
}
}
pub fn array(items: PropertySchema) -> Self {
Self {
items: Some(Box::new(items)),
..Self::of(JsonType::Array)
}
}
pub fn object() -> Self {
Self::of(JsonType::Object)
}
pub fn any_of(members: Vec<PropertySchema>) -> Self {
Self {
any_of: Some(members),
..Self::default()
}
}
pub fn nullable(mut self) -> Self {
if let Some(types) = &mut self.types {
if !types.contains(&JsonType::Null) {
types.push(JsonType::Null);
}
} else if let Some(members) = &mut self.any_of
&& !members.iter().any(PropertySchema::admits_null)
{
members.push(PropertySchema::null());
}
self
}
pub fn with_property(mut self, name: impl Into<String>, schema: PropertySchema) -> Self {
self.properties.insert(name.into(), schema);
self
}
pub fn with_required_property(
mut self,
name: impl Into<String>,
schema: PropertySchema,
) -> Self {
let name = name.into();
if !self.required.contains(&name) {
self.required.push(name.clone());
}
self.properties.insert(name, schema);
self
}
pub fn with_additional_properties(mut self, allowed: bool) -> Self {
self.additional_properties = Some(allowed);
self
}
pub fn with_constraints(mut self, constraints: Constraints) -> Self {
self.constraints = (!constraints.is_empty()).then(|| Box::new(constraints));
self
}
pub fn with_constraint(mut self, change: impl FnOnce(&mut Constraints)) -> Self {
let mut constraints = self.constraints.take().map(|c| *c).unwrap_or_default();
change(&mut constraints);
self.with_constraints(constraints)
}
pub fn with_annotation(mut self, change: impl FnOnce(&mut Annotations)) -> Self {
let mut annotations = self.annotations.take().map(|a| *a).unwrap_or_default();
change(&mut annotations);
self.annotations = (!annotations.is_empty()).then(|| Box::new(annotations));
self
}
pub fn with_description(self, description: impl Into<String>) -> Self {
let description = description.into();
self.with_annotation(|a| a.description = Some(description))
}
pub fn admits_null(&self) -> bool {
self.admits_kind(JsonType::Null)
}
pub(crate) fn admits_kind(&self, kind: JsonType) -> bool {
let by_type = self
.types
.as_ref()
.is_none_or(|types| types.contains(&kind));
let by_any_of = self
.any_of
.as_ref()
.is_none_or(|members| members.iter().any(|m| m.admits_kind(kind)));
by_type && by_any_of
}
pub(crate) fn declared_kinds(&self) -> Option<Vec<(JsonType, bool)>> {
if let Some(types) = &self.types {
return Some(
types
.iter()
.map(|t| {
(
*t,
*t == JsonType::String && self.format == Some(SchemaFormat::Uuid),
)
})
.collect(),
);
}
let members = self.any_of.as_ref()?;
let mut kinds = Vec::new();
for member in members {
kinds.extend(member.declared_kinds()?);
}
Some(kinds)
}
pub fn admits_additional_properties(&self, mode_default: bool) -> bool {
match self.additional_properties {
Some(explicit) => explicit,
None if self
.types
.as_ref()
.is_some_and(|types| types.contains(&JsonType::Object)) =>
{
mode_default
}
None => true,
}
}
pub(crate) fn is_uuid_string(&self) -> bool {
self.format == Some(SchemaFormat::Uuid)
&& self
.types
.as_ref()
.is_some_and(|types| types.contains(&JsonType::String))
}
pub fn type_name(&self) -> String {
let own: Option<Vec<String>> = self.types.as_ref().map(|types| {
types
.iter()
.map(|ty| match ty {
JsonType::Array => match &self.items {
Some(items) => format!("array<{}>", items.type_name()),
None => "array".to_string(),
},
other => other.engine_name(self.format).to_string(),
})
.collect()
});
let members: Option<Vec<String>> = self
.any_of
.as_ref()
.map(|members| members.iter().map(PropertySchema::type_name).collect());
let names = match (own, members) {
(Some(own), _) => own,
(None, Some(members)) => members,
(None, None) => return "any".to_string(),
};
if names.len() == 1 {
names.into_iter().next().unwrap_or_default()
} else {
format!("union<{}>", names.join("|"))
}
}
pub fn non_null_type_name(&self) -> String {
if let Some(types) = &self.types
&& types.len() > 1
&& types.contains(&JsonType::Null)
{
let mut copy = self.clone();
copy.types = Some(
types
.iter()
.copied()
.filter(|t| *t != JsonType::Null)
.collect(),
);
return copy.type_name();
}
if self.types.is_none()
&& let Some(members) = &self.any_of
{
let kept: Vec<&PropertySchema> = members
.iter()
.filter(|m| m.types.as_deref() != Some(&[JsonType::Null]))
.collect();
if kept.len() < members.len() && !kept.is_empty() {
return match kept.as_slice() {
[only] => only.type_name(),
many => format!(
"union<{}>",
many.iter()
.map(|m| m.type_name())
.collect::<Vec<_>>()
.join("|")
),
};
}
}
self.type_name()
}
pub fn annotations(&self) -> Option<&Annotations> {
self.annotations.as_deref()
}
pub fn constraints(&self) -> Option<&Constraints> {
self.constraints.as_deref()
}
pub fn is_required(&self, key: &str) -> bool {
self.required.iter().any(|name| name == key)
}
}