use alloc::collections::BTreeMap;
use combine_structs::combine_fields;
use indexmap::IndexMap;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::{Number, Value};
use url::Url;
use crate::extensions::IntellijSchemaExt;
use crate::extensions::LintelSchemaExt;
use crate::extensions::TaploInfoSchemaExt;
use crate::extensions::TaploSchemaExt;
use crate::extensions::TombiSchemaExt;
mod add;
mod navigate;
pub mod vocabularies;
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests;
pub use navigate::{navigate_pointer, ref_name, resolve_ref};
#[allow(clippy::trivially_copy_pass_by_ref)] pub(crate) fn is_false(v: &bool) -> bool {
!v
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(untagged)]
pub enum SchemaValue {
Bool(bool),
Schema(Box<Schema>),
Other(Value),
}
#[derive(
Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema, strum::Display,
)]
#[serde(rename_all = "camelCase")]
#[strum(serialize_all = "lowercase")]
pub enum SimpleType {
Array,
Boolean,
Integer,
Null,
Number,
Object,
String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(untagged)]
pub enum TypeValue {
Single(SimpleType),
Union(Vec<SimpleType>),
}
#[combine_fields(
CoreVocabulary,
ApplicatorVocabulary,
UnevaluatedVocabulary,
ValidationVocabulary,
MetaDataVocabulary,
FormatAnnotationVocabulary,
ContentVocabulary
)]
#[allow(clippy::struct_excessive_bools)] #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct Schema {
#[serde(
rename = "markdownDescription",
skip_serializing_if = "Option::is_none"
)]
pub markdown_description: Option<String>,
#[serde(
rename = "markdownEnumDescriptions",
skip_serializing_if = "Option::is_none"
)]
pub markdown_enum_descriptions: Option<Vec<Option<String>>>,
#[serde(rename = "x-lintel", skip_serializing_if = "Option::is_none")]
pub x_lintel: Option<LintelSchemaExt>,
#[serde(rename = "x-taplo", skip_serializing_if = "Option::is_none")]
pub x_taplo: Option<TaploSchemaExt>,
#[serde(rename = "x-taplo-info", skip_serializing_if = "Option::is_none")]
pub x_taplo_info: Option<TaploInfoSchemaExt>,
#[serde(flatten)]
pub x_tombi: TombiSchemaExt,
#[serde(flatten)]
pub x_intellij: IntellijSchemaExt,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
impl SchemaValue {
pub fn as_schema(&self) -> Option<&Schema> {
match self {
Self::Schema(s) => Some(s),
Self::Bool(_) | Self::Other(_) => None,
}
}
}
impl Schema {
pub fn from_value(value: Value) -> Result<Self, serde_json::Error> {
serde_json::from_value(value)
}
pub fn description(&self) -> Option<&str> {
self.markdown_description
.as_deref()
.or(self.description.as_deref())
}
pub fn required_set(&self) -> &[String] {
self.required.as_deref().unwrap_or_default()
}
pub fn is_deprecated(&self) -> bool {
self.deprecated
}
pub fn type_str(&self) -> Option<String> {
schema_type_str(self)
}
pub fn validate(&self) -> Vec<crate::validate::SchemaError> {
crate::validate::validate(self)
}
#[must_use]
pub fn absolute(&self) -> Schema {
crate::absolute::make_absolute(self)
}
#[must_use]
pub fn flatten(&self, root: &SchemaValue) -> Schema {
crate::flatten::flatten_all_of(self, root)
}
pub fn get_keyword(&self, key: &str) -> Option<&SchemaValue> {
match key {
"items" => self.items.as_deref(),
"contains" => self.contains.as_deref(),
"additionalProperties" => self.additional_properties.as_deref(),
"propertyNames" => self.property_names.as_deref(),
"unevaluatedProperties" => self.unevaluated_properties.as_deref(),
"unevaluatedItems" => self.unevaluated_items.as_deref(),
"not" => self.not.as_deref(),
"if" => self.if_.as_deref(),
"then" => self.then_.as_deref(),
"else" => self.else_.as_deref(),
"contentSchema" => self.content_schema.as_deref(),
_ => None,
}
}
pub fn get_map_entry(&self, keyword: &str, key: &str) -> Option<&SchemaValue> {
match keyword {
"properties" => self.properties.get(key),
"patternProperties" => self.pattern_properties.get(key),
"$defs" => self.defs.as_ref()?.get(key),
"dependentSchemas" => self.dependent_schemas.get(key),
_ => None,
}
}
pub fn get_array_entry(&self, keyword: &str, index: usize) -> Option<&SchemaValue> {
match keyword {
"allOf" => self.all_of.as_ref()?.get(index),
"anyOf" => self.any_of.as_ref()?.get(index),
"oneOf" => self.one_of.as_ref()?.get(index),
"prefixItems" => self.prefix_items.as_ref()?.get(index),
_ => None,
}
}
fn get_map_entry_by_pointer_segment(&self, segment: &str) -> Option<&SchemaValue> {
self.properties
.get(segment)
.or_else(|| self.pattern_properties.get(segment))
.or_else(|| self.defs.as_ref().and_then(|m| m.get(segment)))
.or_else(|| self.dependent_schemas.get(segment))
}
}
fn schema_type_str(schema: &Schema) -> Option<String> {
if let Some(ref ty) = schema.type_ {
return match ty {
TypeValue::Single(s) if *s == SimpleType::Array => {
let item_ty = schema
.items
.as_ref()
.and_then(|sv| sv.as_schema())
.and_then(schema_type_str);
match item_ty {
Some(item_ty) => Some(format!("{item_ty}[]")),
None => Some("array".to_string()),
}
}
TypeValue::Single(s) => Some(s.to_string()),
TypeValue::Union(arr) => Some(
arr.iter()
.map(SimpleType::to_string)
.collect::<Vec<_>>()
.join(" | "),
),
};
}
if let Some(ref r) = schema.ref_ {
return Some(ref_name(r).to_string());
}
for variants in [&schema.one_of, &schema.any_of].into_iter().flatten() {
let mut types: Vec<String> = variants
.iter()
.filter_map(|v| match v {
SchemaValue::Schema(s) => {
schema_type_str(s).or_else(|| s.ref_.as_ref().map(|r| ref_name(r).to_string()))
}
SchemaValue::Bool(_) | SchemaValue::Other(_) => None,
})
.collect();
types.dedup();
if !types.is_empty() {
return Some(types.join(" | "));
}
}
if let Some(ref c) = schema.const_ {
return Some(format!("const: {c}"));
}
if let Some(ref values) = schema.enum_ {
if values.len() == 1 {
let val = &values[0];
return Some(
val.as_str()
.map_or_else(|| val.to_string(), |s| format!("\"{s}\"")),
);
}
return Some("enum".to_string());
}
None
}