use crate::consts::meta_schemas;
use crate::Schema;
use crate::_alloc_prelude::*;
use crate::{transform::*, JsonSchema};
use alloc::collections::{BTreeMap, BTreeSet};
use core::{any::Any, fmt::Debug};
use dyn_clone::DynClone;
use serde::Serialize;
use serde_json::{Map as JsonMap, Value};
type CowStr = alloc::borrow::Cow<'static, str>;
#[derive(Debug, Clone)]
#[non_exhaustive]
#[allow(clippy::struct_excessive_bools)]
pub struct SchemaSettings {
pub definitions_path: CowStr,
pub meta_schema: Option<CowStr>,
pub transforms: Vec<Box<dyn GenTransform>>,
pub inline_subschemas: bool,
pub contract: Contract,
pub untagged_enum_variant_titles: bool,
}
impl Default for SchemaSettings {
fn default() -> SchemaSettings {
SchemaSettings::draft2020_12()
}
}
impl SchemaSettings {
#[must_use]
pub fn draft07() -> SchemaSettings {
SchemaSettings {
definitions_path: "/definitions".into(),
meta_schema: Some(meta_schemas::DRAFT07.into()),
transforms: vec![
Box::new(ReplaceUnevaluatedProperties),
Box::new(RemoveRefSiblings),
Box::new(ReplacePrefixItems),
],
inline_subschemas: false,
contract: Contract::Deserialize,
untagged_enum_variant_titles: false,
}
}
#[must_use]
pub fn draft2019_09() -> SchemaSettings {
SchemaSettings {
definitions_path: "/$defs".into(),
meta_schema: Some(meta_schemas::DRAFT2019_09.into()),
transforms: vec![Box::new(ReplacePrefixItems)],
inline_subschemas: false,
contract: Contract::Deserialize,
untagged_enum_variant_titles: false,
}
}
#[must_use]
pub fn draft2020_12() -> SchemaSettings {
SchemaSettings {
definitions_path: "/$defs".into(),
meta_schema: Some(meta_schemas::DRAFT2020_12.into()),
transforms: Vec::new(),
inline_subschemas: false,
contract: Contract::Deserialize,
untagged_enum_variant_titles: false,
}
}
#[must_use]
pub fn openapi3() -> SchemaSettings {
SchemaSettings {
definitions_path: "/components/schemas".into(),
meta_schema: Some(meta_schemas::OPENAPI3.into()),
transforms: vec![
Box::new(ReplaceUnevaluatedProperties),
Box::new(ReplaceBoolSchemas {
skip_additional_properties: true,
}),
Box::new(AddNullable::default()),
Box::new(RemoveRefSiblings),
Box::new(SetSingleExample),
Box::new(ReplaceConstValue),
Box::new(ReplacePrefixItems),
],
inline_subschemas: false,
contract: Contract::Deserialize,
untagged_enum_variant_titles: false,
}
}
#[must_use]
pub fn with(mut self, configure_fn: impl FnOnce(&mut Self)) -> Self {
configure_fn(&mut self);
self
}
#[must_use]
pub fn with_transform(mut self, transform: impl Transform + Clone + 'static + Send) -> Self {
self.transforms.push(Box::new(transform));
self
}
#[must_use]
pub fn into_generator(self) -> SchemaGenerator {
SchemaGenerator::new(self)
}
#[must_use]
pub fn for_deserialize(mut self) -> Self {
self.contract = Contract::Deserialize;
self
}
#[must_use]
pub fn for_serialize(mut self) -> Self {
self.contract = Contract::Serialize;
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
#[allow(missing_docs)]
#[non_exhaustive]
pub enum Contract {
Deserialize,
Serialize,
}
impl Contract {
#[must_use]
pub fn is_deserialize(&self) -> bool {
self == &Contract::Deserialize
}
#[must_use]
pub fn is_serialize(&self) -> bool {
self == &Contract::Serialize
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
struct SchemaUid(CowStr, Contract);
#[derive(Debug)]
pub struct SchemaGenerator {
settings: SchemaSettings,
definitions: JsonMap<String, Value>,
pending_schema_ids: BTreeSet<SchemaUid>,
schema_id_to_name: BTreeMap<SchemaUid, CowStr>,
used_schema_names: BTreeSet<CowStr>,
root_schema_id_stack: Vec<SchemaUid>,
}
impl Default for SchemaGenerator {
fn default() -> Self {
SchemaSettings::default().into_generator()
}
}
impl Clone for SchemaGenerator {
fn clone(&self) -> Self {
Self {
settings: self.settings.clone(),
definitions: self.definitions.clone(),
pending_schema_ids: BTreeSet::new(),
schema_id_to_name: BTreeMap::new(),
used_schema_names: BTreeSet::new(),
root_schema_id_stack: Vec::new(),
}
}
}
impl From<SchemaSettings> for SchemaGenerator {
fn from(settings: SchemaSettings) -> Self {
settings.into_generator()
}
}
impl SchemaGenerator {
#[must_use]
pub fn new(settings: SchemaSettings) -> SchemaGenerator {
SchemaGenerator {
settings,
definitions: JsonMap::new(),
pending_schema_ids: BTreeSet::new(),
schema_id_to_name: BTreeMap::new(),
used_schema_names: BTreeSet::new(),
root_schema_id_stack: Vec::new(),
}
}
#[must_use]
pub fn settings(&self) -> &SchemaSettings {
&self.settings
}
pub fn subschema_for<T: ?Sized + JsonSchema>(&mut self) -> Schema {
struct FindRef {
schema: Schema,
name_to_be_inserted: Option<CowStr>,
}
fn find_ref(
this: &mut SchemaGenerator,
uid: &SchemaUid,
inline_schema: bool,
schema_name: fn() -> CowStr,
) -> Option<FindRef> {
let return_ref = !inline_schema
&& (!this.settings.inline_subschemas || this.pending_schema_ids.contains(uid));
if !return_ref {
return None;
}
if this.root_schema_id_stack.last() == Some(uid) {
return Some(FindRef {
schema: Schema::new_ref("#".to_owned()),
name_to_be_inserted: None,
});
}
let name = this.schema_id_to_name.get(uid).cloned().unwrap_or_else(|| {
let base_name = schema_name();
let mut name = CowStr::Borrowed("");
if this.used_schema_names.contains(base_name.as_ref()) {
for i in 2.. {
name = format!("{base_name}{i}").into();
if !this.used_schema_names.contains(&name) {
break;
}
}
} else {
name = base_name;
}
this.used_schema_names.insert(name.clone());
this.schema_id_to_name.insert(uid.clone(), name.clone());
name
});
let reference = format!(
"#{}/{}",
this.definitions_path_stripped(),
crate::encoding::encode_ref_name(&name)
);
Some(FindRef {
schema: Schema::new_ref(reference),
name_to_be_inserted: (!this.definitions().contains_key(name.as_ref()))
.then_some(name),
})
}
let uid = self.schema_uid::<T>();
let Some(FindRef {
schema,
name_to_be_inserted,
}) = find_ref(self, &uid, T::inline_schema(), T::schema_name)
else {
return self.json_schema_internal::<T>(&uid);
};
if let Some(name) = name_to_be_inserted {
self.insert_new_subschema_for::<T>(name, &uid);
}
schema
}
fn insert_new_subschema_for<T: ?Sized + JsonSchema>(&mut self, name: CowStr, uid: &SchemaUid) {
let dummy = false.into();
self.definitions.insert(name.clone().into(), dummy);
let schema = self.json_schema_internal::<T>(uid);
self.definitions.insert(name.into(), schema.to_value());
}
#[must_use]
pub fn definitions(&self) -> &JsonMap<String, Value> {
&self.definitions
}
#[must_use]
pub fn definitions_mut(&mut self) -> &mut JsonMap<String, Value> {
&mut self.definitions
}
pub fn take_definitions(&mut self, apply_transforms: bool) -> JsonMap<String, Value> {
let mut definitions = core::mem::take(&mut self.definitions);
if apply_transforms {
for schema in definitions.values_mut().flat_map(<&mut Schema>::try_from) {
self.apply_transforms(schema);
}
}
definitions
}
pub fn transforms_mut(&mut self) -> impl Iterator<Item = &mut dyn GenTransform> {
self.settings.transforms.iter_mut().map(Box::as_mut)
}
pub fn root_schema_for<T: ?Sized + JsonSchema>(&mut self) -> Schema {
let schema_uid = self.schema_uid::<T>();
self.root_schema_id_stack.push(schema_uid.clone());
let mut schema = self.json_schema_internal::<T>(&schema_uid);
let object = schema.ensure_object();
object
.entry("title")
.or_insert_with(|| T::schema_name().into());
if let Some(meta_schema) = self.settings.meta_schema.as_deref() {
object.insert("$schema".into(), meta_schema.into());
}
self.add_definitions(object, self.definitions.clone());
self.apply_transforms(&mut schema);
self.root_schema_id_stack.pop();
schema
}
#[must_use]
pub fn into_root_schema_for<T: ?Sized + JsonSchema>(mut self) -> Schema {
let schema_uid = self.schema_uid::<T>();
self.root_schema_id_stack.push(schema_uid.clone());
let mut schema = self.json_schema_internal::<T>(&schema_uid);
let object = schema.ensure_object();
object
.entry("title")
.or_insert_with(|| T::schema_name().into());
if let Some(meta_schema) = core::mem::take(&mut self.settings.meta_schema) {
object.insert("$schema".into(), meta_schema.into());
}
let definitions = self.take_definitions(false);
self.add_definitions(object, definitions);
self.apply_transforms(&mut schema);
schema
}
pub fn root_schema_for_value<T: ?Sized + Serialize>(
&mut self,
value: &T,
) -> Result<Schema, serde_json::Error> {
let mut schema = value.serialize(crate::ser::Serializer {
generator: self,
include_title: true,
})?;
let object = schema.ensure_object();
if let Ok(example) = serde_json::to_value(value) {
object.insert("examples".into(), vec![example].into());
}
if let Some(meta_schema) = self.settings.meta_schema.as_deref() {
object.insert("$schema".into(), meta_schema.into());
}
self.add_definitions(object, self.definitions.clone());
self.apply_transforms(&mut schema);
Ok(schema)
}
pub fn into_root_schema_for_value<T: ?Sized + Serialize>(
mut self,
value: &T,
) -> Result<Schema, serde_json::Error> {
let mut schema = value.serialize(crate::ser::Serializer {
generator: &mut self,
include_title: true,
})?;
let object = schema.ensure_object();
if let Ok(example) = serde_json::to_value(value) {
object.insert("examples".into(), vec![example].into());
}
if let Some(meta_schema) = core::mem::take(&mut self.settings.meta_schema) {
object.insert("$schema".into(), meta_schema.into());
}
let definitions = self.take_definitions(false);
self.add_definitions(object, definitions);
self.apply_transforms(&mut schema);
Ok(schema)
}
#[must_use]
pub fn contract(&self) -> &Contract {
&self.settings.contract
}
fn json_schema_internal<T: ?Sized + JsonSchema>(&mut self, uid: &SchemaUid) -> Schema {
let did_add = self.pending_schema_ids.insert(uid.clone());
let schema = T::json_schema(self);
if did_add {
self.pending_schema_ids.remove(uid);
}
schema
}
fn add_definitions(
&mut self,
schema_object: &mut JsonMap<String, Value>,
mut definitions: JsonMap<String, Value>,
) {
if definitions.is_empty() {
return;
}
let pointer = self.definitions_path_stripped();
let Some(target) = json_pointer_mut(schema_object, pointer, true) else {
return;
};
target.append(&mut definitions);
}
fn apply_transforms(&mut self, schema: &mut Schema) {
for transform in self.transforms_mut() {
transform.transform(schema);
}
}
fn definitions_path_stripped(&self) -> &str {
let path = &self.settings.definitions_path;
let path = path.strip_prefix('#').unwrap_or(path);
path.strip_suffix('/').unwrap_or(path)
}
fn schema_uid<T: ?Sized + JsonSchema>(&self) -> SchemaUid {
SchemaUid(T::schema_id(), self.settings.contract.clone())
}
}
fn json_pointer_mut<'a>(
mut object: &'a mut JsonMap<String, Value>,
pointer: &str,
create_if_missing: bool,
) -> Option<&'a mut JsonMap<String, Value>> {
use serde_json::map::Entry;
let pointer = pointer.strip_prefix('/')?;
if pointer.is_empty() {
return Some(object);
}
for mut segment in pointer.split('/') {
let replaced: String;
if segment.contains('~') {
replaced = segment.replace("~1", "/").replace("~0", "~");
segment = &replaced;
}
let next_value = match object.entry(segment) {
Entry::Occupied(o) => o.into_mut(),
Entry::Vacant(v) if create_if_missing => v.insert(Value::Object(JsonMap::new())),
Entry::Vacant(_) => return None,
};
object = next_value.as_object_mut()?;
}
Some(object)
}
pub trait GenTransform: Transform + DynClone + Any + Send {
#[deprecated = "Only to support pre-1.86 rustc"]
#[doc(hidden)]
fn _as_any(&self) -> &dyn Any;
#[deprecated = "Only to support pre-1.86 rustc"]
#[doc(hidden)]
fn _as_any_mut(&mut self) -> &mut dyn Any;
#[deprecated = "Only to support pre-1.86 rustc"]
#[doc(hidden)]
fn _into_any(self: Box<Self>) -> Box<dyn Any>;
}
#[allow(deprecated, clippy::used_underscore_items)]
impl dyn GenTransform {
#[must_use]
pub fn is<T: Transform + Clone + Any + Send>(&self) -> bool {
self._as_any().is::<T>()
}
#[must_use]
pub fn downcast_ref<T: Transform + Clone + Any + Send>(&self) -> Option<&T> {
self._as_any().downcast_ref::<T>()
}
#[must_use]
pub fn downcast_mut<T: Transform + Clone + Any + Send>(&mut self) -> Option<&mut T> {
self._as_any_mut().downcast_mut::<T>()
}
#[allow(clippy::missing_panics_doc)] pub fn downcast<T: Transform + Clone + Any + Send>(
self: Box<Self>,
) -> Result<Box<T>, Box<Self>> {
if self.is::<T>() {
Ok(self._into_any().downcast().unwrap())
} else {
Err(self)
}
}
}
dyn_clone::clone_trait_object!(GenTransform);
impl<T> GenTransform for T
where
T: Transform + Clone + Any + Send,
{
fn _as_any(&self) -> &dyn Any {
self
}
fn _as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn _into_any(self: Box<Self>) -> Box<dyn Any> {
self
}
}
impl Debug for Box<dyn GenTransform> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
#[allow(clippy::used_underscore_items)]
self._debug_type_name(f)
}
}
fn _assert_send() {
fn assert<T: Send>() {}
assert::<SchemaSettings>();
assert::<SchemaGenerator>();
}