use crate::{Error, JsonSchema};
use serde_json::{Map, Value, json};
use specta::{
Types,
datatype::{NamedDataType, *},
};
pub fn export(js: &JsonSchema, types: &Types, ndt: &NamedDataType) -> Result<Value, Error> {
let Some(ty) = &ndt.ty else {
return Ok(json!({}));
};
datatype_to_schema(js, types, ty, true)
}
pub fn datatype_to_schema(
js: &JsonSchema,
types: &Types,
dt: &DataType,
is_definition: bool,
) -> Result<Value, Error> {
match dt {
DataType::Primitive(p) => Ok(primitive_to_schema(p)),
DataType::Nullable(inner) => {
let inner_schema = datatype_to_schema(js, types, inner, false)?;
Ok(json!({
"anyOf": [
inner_schema,
{"type": "null"}
]
}))
}
DataType::List(list) => {
let items = datatype_to_schema(js, types, &list.ty, false)?;
if let Some(len) = list.length {
Ok(json!({
"type": "array",
"items": items,
"minItems": len,
"maxItems": len
}))
} else {
Ok(json!({
"type": "array",
"items": items
}))
}
}
DataType::Map(map) => {
let value_schema = datatype_to_schema(js, types, map.value_ty(), false)?;
Ok(json!({
"type": "object",
"additionalProperties": value_schema
}))
}
DataType::Struct(s) => struct_to_schema(js, types, s),
DataType::Enum(e) => enum_to_schema(js, types, e),
DataType::Tuple(t) => tuple_to_schema(js, types, t),
DataType::Reference(r) => {
match r {
Reference::Named(r) => {
if is_definition {
if let Some(referenced_ndt) = types.get(r) {
let Some(ty) = &referenced_ndt.ty else {
return Ok(json!({}));
};
datatype_to_schema(js, types, ty, true)
} else {
Err(Error::InvalidReference(
"Reference not found in Types".to_string(),
))
}
} else {
let defs_key = js.schema_version.definitions_key();
if let Some(referenced_ndt) = types.get(r) {
Ok(json!({
"$ref": format!("#/{}/{}", defs_key, referenced_ndt.name)
}))
} else {
Err(Error::InvalidReference(
"Reference not found in Types".to_string(),
))
}
}
}
Reference::Opaque(_) => Err(Error::UnsupportedDataType(
"Opaque references are not supported by JSON Schema exporter".to_string(),
)),
}
}
DataType::Generic(_) => Ok(json!({})), DataType::Intersection(intersection) => Ok(json!({
"allOf": intersection
.iter()
.map(|ty| datatype_to_schema(js, types, ty, false))
.collect::<Result<Vec<_>, _>>()?
})),
}
}
fn primitive_to_schema(p: &Primitive) -> Value {
match p {
Primitive::bool => json!({"type": "boolean"}),
Primitive::str => json!({"type": "string"}),
Primitive::char => json!({"type": "string", "minLength": 1, "maxLength": 1}),
Primitive::i8 => json!({"type": "integer", "minimum": i8::MIN, "maximum": i8::MAX}),
Primitive::i16 => json!({"type": "integer", "minimum": i16::MIN, "maximum": i16::MAX}),
Primitive::i32 => json!({"type": "integer", "format": "int32"}),
Primitive::i64 => json!({"type": "integer", "format": "int64"}),
Primitive::i128 => json!({"type": "integer"}),
Primitive::isize => json!({"type": "integer"}),
Primitive::u8 => json!({"type": "integer", "minimum": 0, "maximum": u8::MAX}),
Primitive::u16 => json!({"type": "integer", "minimum": 0, "maximum": u16::MAX}),
Primitive::u32 => json!({"type": "integer", "minimum": 0, "format": "uint32"}),
Primitive::u64 => json!({"type": "integer", "minimum": 0, "format": "uint64"}),
Primitive::u128 => json!({"type": "integer", "minimum": 0}),
Primitive::usize => json!({"type": "integer", "minimum": 0}),
Primitive::f16 => json!({"type": "number", "format": "float16"}),
Primitive::f32 => json!({"type": "number", "format": "float"}),
Primitive::f64 => json!({"type": "number", "format": "double"}),
Primitive::f128 => json!({"type": "number", "format": "float128"}),
}
}
fn struct_to_schema(js: &JsonSchema, types: &Types, s: &Struct) -> Result<Value, Error> {
match &s.fields {
Fields::Unit => {
Ok(json!({"type": "null"}))
}
Fields::Unnamed(fields) => {
let items: Result<Vec<_>, _> = fields
.fields
.iter()
.filter_map(|field| field.ty.as_ref().map(|ty| (field, ty)))
.map(|(_, ty)| datatype_to_schema(js, types, ty, false))
.collect();
let items = items?;
Ok(json!({
"type": "array",
"prefixItems": items,
"items": false,
"minItems": items.len(),
"maxItems": items.len()
}))
}
Fields::Named(fields) => {
let mut properties = Map::new();
let mut required = Vec::new();
for (name, (field, ty)) in fields
.fields
.iter()
.filter_map(|(name, field)| field.ty.as_ref().map(|ty| (name, (field, ty))))
{
let schema = datatype_to_schema(js, types, ty, false)?;
properties.insert(name.clone().into_owned(), schema);
if !field.optional {
required.push(Value::String(name.clone().into_owned()));
}
}
let mut obj = json!({
"type": "object",
"properties": properties
});
if !required.is_empty() {
obj.as_object_mut()
.unwrap()
.insert("required".to_string(), Value::Array(required));
}
Ok(obj)
}
}
}
fn enum_to_schema(js: &JsonSchema, types: &Types, e: &Enum) -> Result<Value, Error> {
let variants: Result<Vec<_>, _> = e
.variants
.iter()
.filter(|(_, variant)| !variant.skip)
.map(|(name, variant)| variant_to_schema(js, types, name, variant))
.collect();
let variants = variants?;
if variants.is_empty() {
return Err(Error::ConversionError(
"Enum has no non-skipped variants".to_string(),
));
}
if variants.len() == 1 {
Ok(variants.into_iter().next().unwrap())
} else {
Ok(json!({
"anyOf": variants
}))
}
}
fn variant_to_schema(
js: &JsonSchema,
types: &Types,
name: &str,
variant: &Variant,
) -> Result<Value, Error> {
match &variant.fields {
Fields::Unit => {
Ok(json!({"const": name}))
}
Fields::Unnamed(fields) => {
let items: Result<Vec<_>, _> = fields
.fields
.iter()
.filter_map(|field| field.ty.as_ref().map(|ty| (field, ty)))
.map(|(_, ty)| datatype_to_schema(js, types, ty, false))
.collect();
let items = items?;
if items.len() == 1 {
Ok(json!({
"type": "object",
"required": [name],
"properties": {
name: items[0].clone()
},
"additionalProperties": false
}))
} else {
Ok(json!({
"type": "object",
"required": [name],
"properties": {
name: {
"type": "array",
"prefixItems": items.clone(),
"items": false,
"minItems": items.len(),
"maxItems": items.len()
}
},
"additionalProperties": false
}))
}
}
Fields::Named(fields) => {
let mut properties = Map::new();
let mut required = Vec::new();
for (field_name, (field, ty)) in fields
.fields
.iter()
.filter_map(|(name, field)| field.ty.as_ref().map(|ty| (name, (field, ty))))
{
let schema = datatype_to_schema(js, types, ty, false)?;
properties.insert(field_name.clone().into_owned(), schema);
if !field.optional {
required.push(Value::String(field_name.clone().into_owned()));
}
}
let mut inner_obj = json!({
"type": "object",
"properties": properties
});
if !required.is_empty() {
inner_obj
.as_object_mut()
.unwrap()
.insert("required".to_string(), Value::Array(required));
}
Ok(json!({
"type": "object",
"required": [name],
"properties": {
name: inner_obj
},
"additionalProperties": false
}))
}
}
}
fn tuple_to_schema(js: &JsonSchema, types: &Types, t: &Tuple) -> Result<Value, Error> {
if t.elements.is_empty() {
return Ok(json!({"type": "null"}));
}
let items: Result<Vec<_>, _> = t
.elements
.iter()
.map(|ty| datatype_to_schema(js, types, ty, false))
.collect();
let items = items?;
Ok(json!({
"type": "array",
"prefixItems": items.clone(),
"items": false,
"minItems": items.len(),
"maxItems": items.len()
}))
}