use referencing::Draft;
use serde_json::{json, Map, Value};
use crate::{
canonical::ir::{CanonicalJson, IntegerBounds, Schema, SchemaKind, StringLeaf},
JsonTypeSet,
};
pub(crate) fn to_json_schema(root: &Schema, draft: Draft) -> Value {
let value = emit(root.kind(), draft);
if matches!(root.kind(), SchemaKind::Raw(_)) {
return value;
}
match schema_uri(draft) {
Some(uri) => with_schema_uri(value, uri),
None => value,
}
}
fn emit(kind: &SchemaKind, draft: Draft) -> Value {
match kind {
SchemaKind::True if matches!(draft, Draft::Draft4) => Value::Object(Map::new()),
SchemaKind::True => Value::Bool(true),
SchemaKind::False if matches!(draft, Draft::Draft4) => json!({"not": {}}),
SchemaKind::False => Value::Bool(false),
SchemaKind::Const(value) if value.as_value().is_null() => json!({"type": "null"}),
SchemaKind::Const(value) if matches!(draft, Draft::Draft4) => {
json!({"enum": [value.to_value()]})
}
SchemaKind::Const(value) => json!({"const": value.to_value()}),
SchemaKind::Enum(values) => emit_enum(values),
SchemaKind::String(leaf) => emit_string(leaf),
SchemaKind::Integer(leaf) => emit_integer(leaf),
SchemaKind::MultiType(set) => emit_multi_type(*set),
SchemaKind::TypedGroup { ty, body } => {
let mut map = match emit(body.kind(), draft) {
Value::Object(map) => map,
other => unreachable!("value-set body emits an object: {other:?}"),
};
map.insert("type".into(), Value::String(ty.to_string()));
Value::Object(map)
}
SchemaKind::AnyOf(branches) => json!({
"anyOf": branches
.iter()
.map(|branch| emit(branch.kind(), draft))
.collect::<Vec<_>>()
}),
SchemaKind::Raw(value) => value.get().clone(),
}
}
fn emit_string(leaf: &StringLeaf) -> Value {
let mut map = Map::new();
map.insert("type".into(), Value::String("string".into()));
if let Some(min) = &leaf.lengths.minimum {
map.insert("minLength".into(), Value::Number(min.to_number()));
}
if let Some(max) = &leaf.lengths.maximum {
map.insert("maxLength".into(), Value::Number(max.to_number()));
}
match leaf.patterns.as_slice() {
[] => {}
[pattern] => {
map.insert("pattern".into(), Value::String(pattern.to_string()));
}
patterns => {
let conjuncts = patterns
.iter()
.map(|pattern| json!({ "pattern": pattern.as_ref() }))
.collect();
map.insert("allOf".into(), Value::Array(conjuncts));
}
}
Value::Object(map)
}
fn emit_integer(bounds: &IntegerBounds) -> Value {
let mut map = Map::new();
map.insert("type".into(), Value::String("integer".into()));
if let Some(min) = &bounds.minimum {
map.insert("minimum".into(), Value::Number(min.to_number()));
}
if let Some(max) = &bounds.maximum {
map.insert("maximum".into(), Value::Number(max.to_number()));
}
Value::Object(map)
}
fn emit_enum(values: &[CanonicalJson]) -> Value {
if let Some(set) = SchemaKind::finite_values_saturated_domain(values) {
return emit_multi_type(set);
}
json!({
"enum": values.iter().map(CanonicalJson::to_value).collect::<Vec<_>>()
})
}
fn emit_multi_type(set: JsonTypeSet) -> Value {
let mut names = set.iter().map(|ty| ty.to_string());
match (names.next(), names.next()) {
(Some(only), None) => json!({"type": only}),
(first, second) => {
let names: Vec<Value> = first
.into_iter()
.chain(second)
.chain(names)
.map(Value::String)
.collect();
json!({"type": names})
}
}
}
pub(crate) fn schema_uri(draft: Draft) -> Option<&'static str> {
match draft {
Draft::Draft4 => Some("http://json-schema.org/draft-04/schema#"),
Draft::Draft6 => Some("http://json-schema.org/draft-06/schema#"),
Draft::Draft7 => Some("http://json-schema.org/draft-07/schema#"),
Draft::Draft201909 => Some("https://json-schema.org/draft/2019-09/schema"),
Draft::Draft202012 => Some("https://json-schema.org/draft/2020-12/schema"),
_ => None,
}
}
fn with_schema_uri(value: Value, uri: &'static str) -> Value {
let mut map = match value {
Value::Object(map) => map,
Value::Bool(true) => Map::new(),
Value::Bool(false) => {
let mut map = Map::new();
map.insert("not".into(), Value::Object(Map::new()));
map
}
other => unreachable!("emit yields only objects or booleans: {other:?}"),
};
map.insert("$schema".into(), Value::String(uri.into()));
Value::Object(map)
}