use crate::_alloc_prelude::*;
use crate::{consts::meta_schemas, Schema};
use alloc::borrow::Cow;
use alloc::collections::BTreeSet;
use serde_json::{json, Map, Value};
pub trait Transform {
fn transform(&mut self, schema: &mut Schema);
#[doc(hidden)]
fn _debug_type_name(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str(core::any::type_name::<Self>())
}
}
impl<F> Transform for F
where
F: FnMut(&mut Schema),
{
fn transform(&mut self, schema: &mut Schema) {
self(schema);
}
}
pub fn transform_subschemas<T: Transform + ?Sized>(t: &mut T, schema: &mut Schema) {
for (key, value) in schema.as_object_mut().into_iter().flatten() {
match key.as_str() {
"not"
| "if"
| "then"
| "else"
| "contains"
| "additionalProperties"
| "propertyNames"
| "additionalItems" => {
if let Ok(subschema) = value.try_into() {
t.transform(subschema);
}
}
"allOf" | "anyOf" | "oneOf" | "prefixItems" => {
if let Some(array) = value.as_array_mut() {
for value in array {
if let Ok(subschema) = value.try_into() {
t.transform(subschema);
}
}
}
}
"items" => {
if let Some(array) = value.as_array_mut() {
for value in array {
if let Ok(subschema) = value.try_into() {
t.transform(subschema);
}
}
} else if let Ok(subschema) = value.try_into() {
t.transform(subschema);
}
}
"properties" | "patternProperties" | "$defs" | "definitions" => {
if let Some(obj) = value.as_object_mut() {
for value in obj.values_mut() {
if let Ok(subschema) = value.try_into() {
t.transform(subschema);
}
}
}
}
_ => {}
}
}
}
pub(crate) fn transform_immediate_subschemas<T: Transform + ?Sized>(
t: &mut T,
schema: &mut Schema,
) {
for (key, value) in schema.as_object_mut().into_iter().flatten() {
match key.as_str() {
"if" | "then" | "else" => {
if let Ok(subschema) = value.try_into() {
t.transform(subschema);
}
}
"allOf" | "anyOf" | "oneOf" => {
if let Some(array) = value.as_array_mut() {
for value in array {
if let Ok(subschema) = value.try_into() {
t.transform(subschema);
}
}
}
}
_ => {}
}
}
}
#[derive(Debug, Clone)]
#[allow(clippy::exhaustive_structs)]
pub struct RecursiveTransform<T>(pub T);
impl<T> Transform for RecursiveTransform<T>
where
T: Transform,
{
fn transform(&mut self, schema: &mut Schema) {
self.0.transform(schema);
transform_subschemas(self, schema);
}
}
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct ReplaceBoolSchemas {
pub skip_additional_properties: bool,
}
impl Transform for ReplaceBoolSchemas {
fn transform(&mut self, schema: &mut Schema) {
if let Some(obj) = schema.as_object_mut() {
if self.skip_additional_properties {
if let Some((ap_key, ap_value)) = obj.remove_entry("additionalProperties") {
transform_subschemas(self, schema);
schema.insert(ap_key, ap_value);
return;
}
}
transform_subschemas(self, schema);
} else {
schema.ensure_object();
}
}
}
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct RemoveRefSiblings;
impl Transform for RemoveRefSiblings {
fn transform(&mut self, schema: &mut Schema) {
transform_subschemas(self, schema);
if let Some(obj) = schema.as_object_mut().filter(|o| o.len() > 1) {
if let Some(ref_value) = obj.remove("$ref") {
if let Value::Array(all_of) = obj.entry("allOf").or_insert(Value::Array(Vec::new()))
{
all_of.push(json!({
"$ref": ref_value
}));
}
}
}
}
}
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct SetSingleExample;
impl Transform for SetSingleExample {
fn transform(&mut self, schema: &mut Schema) {
transform_subschemas(self, schema);
if let Some(Value::Array(examples)) = schema.remove("examples") {
if let Some(first_example) = examples.into_iter().next() {
schema.insert("example".into(), first_example);
}
}
}
}
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct ReplaceConstValue;
impl Transform for ReplaceConstValue {
fn transform(&mut self, schema: &mut Schema) {
transform_subschemas(self, schema);
if let Some(value) = schema.remove("const") {
schema.insert("enum".into(), Value::Array(vec![value]));
}
}
}
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct ReplacePrefixItems;
impl Transform for ReplacePrefixItems {
fn transform(&mut self, schema: &mut Schema) {
transform_subschemas(self, schema);
if let Some(prefix_items) = schema.remove("prefixItems") {
let previous_items = schema.insert("items".to_owned(), prefix_items);
if let Some(previous_items) = previous_items {
schema.insert("additionalItems".to_owned(), previous_items);
}
}
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct AddNullable {
pub remove_null_type: bool,
pub add_const_null: bool,
}
impl Default for AddNullable {
fn default() -> Self {
Self {
remove_null_type: true,
add_const_null: true,
}
}
}
impl Transform for AddNullable {
fn transform(&mut self, schema: &mut Schema) {
if schema.has_type("null") {
schema.insert("nullable".into(), true.into());
let ty = schema.get_mut("type").unwrap();
let only_allows_null =
ty.is_string() || ty.as_array().unwrap().iter().all(|v| v == "null");
if only_allows_null {
if self.add_const_null {
schema.insert("const".to_string(), Value::Null);
if self.remove_null_type {
schema.remove("type");
}
} else if self.remove_null_type {
*ty = Value::Array(Vec::new());
}
} else if self.remove_null_type {
let array = ty.as_array_mut().unwrap();
array.retain(|t| t != "null");
if array.len() == 1 {
*ty = array.remove(0);
}
}
}
transform_subschemas(self, schema);
}
}
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct ReplaceUnevaluatedProperties;
impl Transform for ReplaceUnevaluatedProperties {
fn transform(&mut self, schema: &mut Schema) {
transform_subschemas(self, schema);
let Some(up) = schema.remove("unevaluatedProperties") else {
return;
};
schema.insert("additionalProperties".to_owned(), up);
let mut gather_property_names = GatherPropertyNames::default();
gather_property_names.transform(schema);
let property_names = gather_property_names.0;
if property_names.is_empty() {
return;
}
if let Some(properties) = schema
.ensure_object()
.entry("properties")
.or_insert(Map::new().into())
.as_object_mut()
{
for name in property_names {
properties.entry(name).or_insert(true.into());
}
}
}
}
#[derive(Default)]
struct GatherPropertyNames(BTreeSet<String>);
impl Transform for GatherPropertyNames {
fn transform(&mut self, schema: &mut Schema) {
self.0.extend(
schema
.as_object()
.iter()
.filter_map(|o| o.get("properties"))
.filter_map(Value::as_object)
.flat_map(Map::keys)
.cloned(),
);
transform_immediate_subschemas(self, schema);
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct RestrictFormats {
pub infer_from_meta_schema: bool,
pub allowed_formats: BTreeSet<Cow<'static, str>>,
}
impl Default for RestrictFormats {
fn default() -> Self {
Self {
infer_from_meta_schema: true,
allowed_formats: BTreeSet::new(),
}
}
}
impl Transform for RestrictFormats {
fn transform(&mut self, schema: &mut Schema) {
let mut implementation = RestrictFormatsImpl {
infer_from_meta_schema: self.infer_from_meta_schema,
inferred_formats: None,
allowed_formats: &self.allowed_formats,
};
implementation.transform(schema);
}
}
static DEFINED_FORMATS: &[&str] = &[
"duration",
"uuid",
"date-time",
"date",
"time",
"email",
"idn-email",
"hostname",
"idn-hostname",
"ipv4",
"ipv6",
"uri",
"uri-reference",
"iri",
"iri-reference",
"uri-template",
"json-pointer",
"relative-json-pointer",
"regex",
];
struct RestrictFormatsImpl<'a> {
infer_from_meta_schema: bool,
inferred_formats: Option<&'static [&'static str]>,
allowed_formats: &'a BTreeSet<Cow<'static, str>>,
}
impl Transform for RestrictFormatsImpl<'_> {
fn transform(&mut self, schema: &mut Schema) {
let Some(obj) = schema.as_object_mut() else {
return;
};
let previous_inferred_formats = self.inferred_formats;
if self.infer_from_meta_schema && obj.contains_key("$schema") {
self.inferred_formats = match obj
.get("$schema")
.and_then(Value::as_str)
.unwrap_or_default()
{
meta_schemas::DRAFT07 => Some(&DEFINED_FORMATS[2..]),
meta_schemas::DRAFT2019_09 | meta_schemas::DRAFT2020_12 => Some(DEFINED_FORMATS),
_ => {
return;
}
};
}
if let Some(format) = obj.get("format").and_then(Value::as_str) {
if !self.allowed_formats.contains(format)
&& !self
.inferred_formats
.is_some_and(|formats| formats.contains(&format))
{
obj.remove("format");
}
}
transform_subschemas(self, schema);
self.inferred_formats = previous_inferred_formats;
}
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn restrict_formats() {
let mut schema = json_schema!({
"$schema": meta_schemas::DRAFT2020_12,
"anyOf": [
{ "format": "uuid" },
{ "$schema": meta_schemas::DRAFT07, "format": "uuid" },
{ "$schema": "http://unknown", "format": "uuid" },
{ "format": "date" },
{ "$schema": meta_schemas::DRAFT07, "format": "date" },
{ "$schema": "http://unknown", "format": "date" },
{ "format": "custom1" },
{ "$schema": meta_schemas::DRAFT07, "format": "custom1" },
{ "$schema": "http://unknown", "format": "custom1" },
{ "format": "custom2" },
{ "$schema": meta_schemas::DRAFT07, "format": "custom2" },
{ "$schema": "http://unknown", "format": "custom2" },
]
});
let mut transform = RestrictFormats::default();
transform.allowed_formats.insert("custom1".into());
transform.transform(&mut schema);
assert_eq!(
schema,
json_schema!({
"$schema": meta_schemas::DRAFT2020_12,
"anyOf": [
{ "format": "uuid" },
{ "$schema": meta_schemas::DRAFT07 },
{ "$schema": "http://unknown", "format": "uuid" },
{ "format": "date" },
{ "$schema": meta_schemas::DRAFT07, "format": "date" },
{ "$schema": "http://unknown", "format": "date" },
{ "format": "custom1" },
{ "$schema": meta_schemas::DRAFT07, "format": "custom1" },
{ "$schema": "http://unknown", "format": "custom1" },
{ },
{ "$schema": meta_schemas::DRAFT07 },
{ "$schema": "http://unknown", "format": "custom2" },
]
})
);
}
}