use indexmap::IndexMap;
use crate::graph::schema::SchemaPattern;
use crate::graph::schema::defaults::CreateContext;
use crate::value::temporal::{Temporal, TemporalKind};
use crate::value::{ElementProperty, Number, Vector, VectorElement, VectorMetric};
#[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(format) => format.engine_name(),
None => "string",
},
JsonType::Array => "array",
JsonType::Object => "object",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum SchemaFormat {
Uuid,
DateTime,
Date,
LocalDateTime,
Time,
Duration,
}
impl SchemaFormat {
pub const ALL: [SchemaFormat; 6] = [
SchemaFormat::Uuid,
SchemaFormat::DateTime,
SchemaFormat::Date,
SchemaFormat::LocalDateTime,
SchemaFormat::Time,
SchemaFormat::Duration,
];
pub fn as_str(self) -> &'static str {
match self {
SchemaFormat::Uuid => "uuid",
SchemaFormat::DateTime => "date-time",
SchemaFormat::Date => "date",
SchemaFormat::LocalDateTime => "local-date-time",
SchemaFormat::Time => "time",
SchemaFormat::Duration => "duration",
}
}
pub fn from_name(name: &str) -> Option<Self> {
SchemaFormat::ALL
.into_iter()
.find(|format| format.as_str() == name)
}
pub fn temporal_kind(self) -> Option<TemporalKind> {
match self {
SchemaFormat::Uuid => None,
SchemaFormat::DateTime => Some(TemporalKind::DateTime),
SchemaFormat::Date => Some(TemporalKind::Date),
SchemaFormat::LocalDateTime => Some(TemporalKind::LocalDateTime),
SchemaFormat::Time => Some(TemporalKind::Time),
SchemaFormat::Duration => Some(TemporalKind::Duration),
}
}
pub fn of_temporal(kind: TemporalKind) -> Self {
match kind {
TemporalKind::DateTime => SchemaFormat::DateTime,
TemporalKind::Date => SchemaFormat::Date,
TemporalKind::LocalDateTime => SchemaFormat::LocalDateTime,
TemporalKind::Time => SchemaFormat::Time,
TemporalKind::Duration => SchemaFormat::Duration,
}
}
pub(crate) fn engine_name(self) -> &'static str {
match self.temporal_kind() {
Some(kind) => kind.name(),
None => "uuid",
}
}
pub(crate) fn admits(self, value: &ElementProperty) -> bool {
match (self, value) {
(SchemaFormat::Uuid, ElementProperty::Uuid(_)) => true,
(_, ElementProperty::Temporal(temporal)) => {
self.temporal_kind() == Some(temporal.kind())
}
_ => false,
}
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct VectorSchema {
pub element: VectorElement,
pub dimension: Option<u32>,
pub metric: Option<VectorMetric>,
pub embedder: Option<String>,
}
impl VectorSchema {
pub fn new(element: VectorElement) -> Self {
VectorSchema {
element,
dimension: None,
metric: None,
embedder: None,
}
}
pub fn with_embedder(mut self, model: impl Into<String>) -> Self {
self.embedder = Some(model.into());
self
}
pub fn with_dimension(mut self, dimension: u32) -> Self {
self.dimension = Some(dimension);
self
}
pub fn with_metric(mut self, metric: VectorMetric) -> Self {
self.metric = Some(metric);
self
}
pub(crate) fn admits(&self, vector: &Vector) -> bool {
vector.element() == self.element
&& self
.dimension
.is_none_or(|dimension| vector.dimension() == dimension as usize)
}
pub(crate) fn type_name(&self) -> String {
match self.dimension {
Some(dimension) => format!("vector<{}, {dimension}>", self.element),
None => format!("vector<{}>", self.element),
}
}
}
#[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>,
pub format_minimum: Option<Temporal>,
pub format_maximum: Option<Temporal>,
pub format_exclusive_minimum: Option<Temporal>,
pub format_exclusive_maximum: Option<Temporal>,
}
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>,
pub default_function: Option<DefaultFunction>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum DefaultFunction {
Uuidv4,
Uuidv7,
Now,
Today,
}
#[derive(Debug, Clone, Copy)]
pub(crate) enum CreateDefault<'a> {
Value(&'a ElementProperty),
Function(DefaultFunction),
}
impl CreateDefault<'_> {
pub(crate) fn value(self, context: &CreateContext) -> ElementProperty {
match self {
CreateDefault::Value(value) => value.clone(),
CreateDefault::Function(function) => function.generate(context),
}
}
}
impl DefaultFunction {
pub fn as_str(self) -> &'static str {
match self {
DefaultFunction::Uuidv4 => "uuidv4",
DefaultFunction::Uuidv7 => "uuidv7",
DefaultFunction::Now => "now",
DefaultFunction::Today => "today",
}
}
pub fn from_name(name: &str) -> Option<Self> {
match name {
"uuidv4" => Some(DefaultFunction::Uuidv4),
"uuidv7" => Some(DefaultFunction::Uuidv7),
"now" => Some(DefaultFunction::Now),
"today" => Some(DefaultFunction::Today),
_ => None,
}
}
pub(crate) fn generate(self, context: &CreateContext) -> ElementProperty {
match self {
DefaultFunction::Uuidv4 => ElementProperty::Uuid(crate::value::uuid::new_v4()),
DefaultFunction::Uuidv7 => ElementProperty::Uuid(crate::value::uuid::new_v7()),
DefaultFunction::Now => ElementProperty::Temporal(Temporal::DateTime(context.now())),
DefaultFunction::Today => ElementProperty::Temporal(Temporal::Date(context.today())),
}
}
pub(crate) fn temporal_format(self) -> Option<SchemaFormat> {
match self {
DefaultFunction::Uuidv4 | DefaultFunction::Uuidv7 => None,
DefaultFunction::Now => Some(SchemaFormat::DateTime),
DefaultFunction::Today => Some(SchemaFormat::Date),
}
}
}
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 vector: Option<VectorSchema>,
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 with_string_format(format: SchemaFormat) -> Self {
Self {
format: Some(format),
..Self::of(JsonType::String)
}
}
pub fn datetime() -> Self {
Self::with_string_format(SchemaFormat::DateTime)
}
pub fn date() -> Self {
Self::with_string_format(SchemaFormat::Date)
}
pub fn local_datetime() -> Self {
Self::with_string_format(SchemaFormat::LocalDateTime)
}
pub fn time() -> Self {
Self::with_string_format(SchemaFormat::Time)
}
pub fn duration() -> Self {
Self::with_string_format(SchemaFormat::Duration)
}
pub fn temporal(kind: TemporalKind) -> Self {
Self::with_string_format(SchemaFormat::of_temporal(kind))
}
pub fn vector(element: VectorElement, dimension: Option<u32>) -> Self {
Self {
vector: Some(VectorSchema {
element,
dimension,
metric: None,
embedder: None,
}),
..Self::of(JsonType::Array)
}
}
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 with_default(self, value: ElementProperty) -> Self {
self.with_annotation(|a| a.default = Some(value))
}
pub fn with_default_function(self, function: DefaultFunction) -> Self {
self.with_annotation(|a| a.default_function = Some(function))
}
pub(crate) fn create_default(&self) -> Option<CreateDefault<'_>> {
let annotations = self.annotations.as_deref()?;
if let Some(function) = annotations.default_function {
return Some(CreateDefault::Function(function));
}
match &annotations.default {
None | Some(ElementProperty::Null) => None,
Some(value) => Some(CreateDefault::Value(value)),
}
}
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, Option<SchemaFormat>)>> {
if let Some(types) = &self.types {
return Some(
types
.iter()
.map(|t| {
(
*t,
if *t == JsonType::String {
self.format
} else {
None
},
)
})
.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 string_format(&self) -> Option<SchemaFormat> {
self.format.filter(|_| {
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.vector, &self.items) {
(Some(vector), _) => vector.type_name(),
(None, Some(items)) => format!("array<{}>", items.type_name()),
(None, 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)
}
}