use crate::registry::SchemaRegistry;
use crate::types::{
EnumDefinition, EnumRepresentation, PrimitiveType, SchemaDefinition, SchemaType,
StructDefinition, StructField, VariantData,
};
use serde_json::{json, Map, Value};
pub const SCHEMA_DIALECT: &str = "https://json-schema.org/draft/2020-12/schema";
pub fn generate(definition: &SchemaDefinition) -> String {
let value = generate_value(definition);
serde_json::to_string_pretty(&value).unwrap_or_else(|_| "{}".to_string())
}
pub fn generate_value(definition: &SchemaDefinition) -> Value {
let mut schema = generate_type_schema(&definition.schema_type);
if let Value::Object(ref mut map) = schema {
map.insert("$schema".to_string(), json!(SCHEMA_DIALECT));
map.insert("title".to_string(), json!(definition.name.as_ref()));
if let Some(ref desc) = definition.description {
map.insert("description".to_string(), json!(desc));
}
if let Some(ref format) = definition.format {
map.insert("format".to_string(), json!(format));
}
if definition.deprecated {
map.insert("deprecated".to_string(), json!(true));
}
if !definition.examples.is_empty() {
map.insert("examples".to_string(), json!(definition.examples));
}
if let Some(ref default) = definition.default {
map.insert("default".to_string(), default.clone());
}
}
schema
}
pub fn generate_bundle(registry: &SchemaRegistry) -> String {
let mut root = Map::new();
root.insert("$schema".to_string(), json!(SCHEMA_DIALECT));
if let Some(ref title) = registry.config().title {
root.insert("title".to_string(), json!(title));
}
if let Some(ref description) = registry.config().description {
root.insert("description".to_string(), json!(description));
}
let mut defs = Map::new();
for (name, definition) in registry.definitions() {
let schema = generate_definition_schema(definition);
defs.insert(name.to_string(), schema);
}
if !defs.is_empty() {
root.insert("$defs".to_string(), Value::Object(defs));
}
if registry.len() == 1 {
if let Some(name) = registry.type_names().next() {
root.insert("$ref".to_string(), json!(format!("#/$defs/{}", name)));
}
}
serde_json::to_string_pretty(&Value::Object(root)).unwrap_or_else(|_| "{}".to_string())
}
fn generate_definition_schema(definition: &SchemaDefinition) -> Value {
let mut schema = generate_type_schema(&definition.schema_type);
if let Value::Object(ref mut map) = schema {
if let Some(ref desc) = definition.description {
map.insert("description".to_string(), json!(desc));
}
if let Some(ref format) = definition.format {
map.insert("format".to_string(), json!(format));
}
if definition.deprecated {
map.insert("deprecated".to_string(), json!(true));
}
if !definition.examples.is_empty() {
map.insert("examples".to_string(), json!(definition.examples));
}
if let Some(ref default) = definition.default {
map.insert("default".to_string(), default.clone());
}
}
schema
}
fn generate_type_schema(schema_type: &SchemaType) -> Value {
match schema_type {
SchemaType::Primitive(prim) => generate_primitive_schema(prim),
SchemaType::Option(inner) => generate_option_schema(inner),
SchemaType::Array(inner) => generate_array_schema(inner),
SchemaType::Set(inner) => generate_set_schema(inner),
SchemaType::Map(inner) => generate_map_schema(inner),
SchemaType::Tuple(types) => generate_tuple_schema(types),
SchemaType::Struct(def) => generate_struct_schema(def),
SchemaType::Enum(def) => generate_enum_schema(def),
SchemaType::Newtype(def) => {
if def.transparent {
generate_type_schema(&def.inner_type)
} else {
generate_type_schema(&def.inner_type)
}
}
SchemaType::Reference(name) => json!({ "$ref": format!("#/$defs/{}", name) }),
SchemaType::Unit => json!({ "type": "null" }),
SchemaType::Any => json!({}),
}
}
fn generate_primitive_schema(prim: &PrimitiveType) -> Value {
match prim {
PrimitiveType::Bool => json!({ "type": "boolean" }),
PrimitiveType::I8 => json!({
"type": "integer",
"minimum": i8::MIN,
"maximum": i8::MAX
}),
PrimitiveType::I16 => json!({
"type": "integer",
"minimum": i16::MIN,
"maximum": i16::MAX
}),
PrimitiveType::I32 => json!({
"type": "integer",
"minimum": i32::MIN,
"maximum": i32::MAX
}),
PrimitiveType::I64 => json!({
"type": "integer",
"minimum": i64::MIN as f64,
"maximum": i64::MAX as f64
}),
PrimitiveType::I128 => json!({
"type": "integer",
"description": "128-bit signed integer"
}),
PrimitiveType::Isize => json!({
"type": "integer",
"description": "Pointer-sized signed integer"
}),
PrimitiveType::U8 => json!({
"type": "integer",
"minimum": 0,
"maximum": u8::MAX
}),
PrimitiveType::U16 => json!({
"type": "integer",
"minimum": 0,
"maximum": u16::MAX
}),
PrimitiveType::U32 => json!({
"type": "integer",
"minimum": 0,
"maximum": u32::MAX
}),
PrimitiveType::U64 => json!({
"type": "integer",
"minimum": 0,
"maximum": u64::MAX as f64
}),
PrimitiveType::U128 => json!({
"type": "integer",
"minimum": 0,
"description": "128-bit unsigned integer"
}),
PrimitiveType::Usize => json!({
"type": "integer",
"minimum": 0,
"description": "Pointer-sized unsigned integer"
}),
PrimitiveType::F32 => json!({ "type": "number" }),
PrimitiveType::F64 => json!({ "type": "number" }),
PrimitiveType::Char => json!({
"type": "string",
"minLength": 1,
"maxLength": 1
}),
PrimitiveType::String => json!({ "type": "string" }),
PrimitiveType::Bytes => json!({
"type": "string",
"contentEncoding": "base64"
}),
}
}
fn generate_option_schema(inner: &SchemaType) -> Value {
let inner_schema = generate_type_schema(inner);
json!({
"oneOf": [
inner_schema,
{ "type": "null" }
]
})
}
fn generate_array_schema(inner: &SchemaType) -> Value {
let items_schema = generate_type_schema(inner);
json!({
"type": "array",
"items": items_schema
})
}
fn generate_set_schema(inner: &SchemaType) -> Value {
let items_schema = generate_type_schema(inner);
json!({
"type": "array",
"items": items_schema,
"uniqueItems": true
})
}
fn generate_map_schema(value_type: &SchemaType) -> Value {
let value_schema = generate_type_schema(value_type);
json!({
"type": "object",
"additionalProperties": value_schema
})
}
fn generate_tuple_schema(types: &[Box<SchemaType>]) -> Value {
let items: Vec<Value> = types.iter().map(|t| generate_type_schema(t)).collect();
json!({
"type": "array",
"prefixItems": items,
"items": false,
"minItems": types.len(),
"maxItems": types.len()
})
}
fn generate_struct_schema(def: &StructDefinition) -> Value {
if def.is_tuple_struct {
return generate_tuple_struct_schema(def);
}
let mut properties = Map::new();
let mut required = Vec::new();
for (name, field) in &def.fields {
let field_schema = generate_field_schema(field);
properties.insert(name.clone(), field_schema);
if field.required {
required.push(json!(name));
}
}
let mut schema = json!({
"type": "object",
"properties": properties,
"additionalProperties": false
});
if !required.is_empty() {
schema["required"] = json!(required);
}
schema
}
fn generate_tuple_struct_schema(def: &StructDefinition) -> Value {
let items: Vec<Value> = def
.fields
.values()
.map(|f| generate_type_schema(&f.schema_type))
.collect();
json!({
"type": "array",
"prefixItems": items,
"items": false,
"minItems": items.len(),
"maxItems": items.len()
})
}
fn generate_field_schema(field: &StructField) -> Value {
let mut schema = generate_type_schema(&field.schema_type);
if let Value::Object(ref mut map) = schema {
if let Some(ref desc) = field.description {
map.insert("description".to_string(), json!(desc));
}
if let Some(ref format) = field.format {
map.insert("format".to_string(), json!(format));
}
if field.deprecated {
map.insert("deprecated".to_string(), json!(true));
}
if let Some(ref default) = field.default {
map.insert("default".to_string(), default.clone());
}
if let Some(min) = field.min_length {
map.insert("minLength".to_string(), json!(min));
}
if let Some(max) = field.max_length {
map.insert("maxLength".to_string(), json!(max));
}
if let Some(min) = field.minimum {
map.insert("minimum".to_string(), json!(min));
}
if let Some(max) = field.maximum {
map.insert("maximum".to_string(), json!(max));
}
if let Some(min) = field.exclusive_minimum {
map.insert("exclusiveMinimum".to_string(), json!(min));
}
if let Some(max) = field.exclusive_maximum {
map.insert("exclusiveMaximum".to_string(), json!(max));
}
if let Some(ref pattern) = field.pattern {
map.insert("pattern".to_string(), json!(pattern));
}
}
schema
}
fn generate_enum_schema(def: &EnumDefinition) -> Value {
if def.is_simple_enum() {
let values: Vec<&str> = def.variants.iter().map(|v| v.name.as_str()).collect();
return json!({
"type": "string",
"enum": values
});
}
let schemas: Vec<Value> = def
.variants
.iter()
.map(|variant| generate_variant_schema(variant, &def.representation))
.collect();
json!({ "oneOf": schemas })
}
fn generate_variant_schema(
variant: &crate::types::EnumVariant,
representation: &EnumRepresentation,
) -> Value {
let variant_name = &variant.name;
match &variant.data {
VariantData::Unit => match representation {
EnumRepresentation::External => json!({
"type": "object",
"properties": {
variant_name: { "type": "null" }
},
"required": [variant_name],
"additionalProperties": false
}),
EnumRepresentation::Internal { tag } => json!({
"type": "object",
"properties": {
tag: { "const": variant_name }
},
"required": [tag],
"additionalProperties": false
}),
EnumRepresentation::Adjacent { tag, content: _ } => json!({
"type": "object",
"properties": {
tag: { "const": variant_name }
},
"required": [tag],
"additionalProperties": false
}),
EnumRepresentation::Untagged => json!({ "type": "null" }),
},
VariantData::Newtype(inner) => {
let inner_schema = generate_type_schema(inner);
match representation {
EnumRepresentation::External => json!({
"type": "object",
"properties": {
variant_name: inner_schema
},
"required": [variant_name],
"additionalProperties": false
}),
EnumRepresentation::Internal { tag } => {
let mut schema = inner_schema;
if let Value::Object(ref mut map) = schema {
map.insert(
"properties".to_string(),
merge_properties(
map.get("properties"),
&json!({ tag: { "const": variant_name } }),
),
);
add_to_required(map, tag);
}
schema
}
EnumRepresentation::Adjacent { tag, content } => json!({
"type": "object",
"properties": {
tag: { "const": variant_name },
content: inner_schema
},
"required": [tag, content],
"additionalProperties": false
}),
EnumRepresentation::Untagged => inner_schema,
}
}
VariantData::Tuple(types) => {
let items: Vec<Value> = types.iter().map(|t| generate_type_schema(t)).collect();
let tuple_schema = json!({
"type": "array",
"prefixItems": items,
"items": false,
"minItems": items.len(),
"maxItems": items.len()
});
match representation {
EnumRepresentation::External => json!({
"type": "object",
"properties": {
variant_name: tuple_schema
},
"required": [variant_name],
"additionalProperties": false
}),
EnumRepresentation::Internal { tag: _ } => {
json!({
"type": "object",
"properties": {
variant_name: tuple_schema
},
"required": [variant_name],
"additionalProperties": false
})
}
EnumRepresentation::Adjacent { tag, content } => json!({
"type": "object",
"properties": {
tag: { "const": variant_name },
content: tuple_schema
},
"required": [tag, content],
"additionalProperties": false
}),
EnumRepresentation::Untagged => tuple_schema,
}
}
VariantData::Struct(fields) => {
let mut properties = Map::new();
let mut required = Vec::new();
for (name, field) in fields {
let field_schema = generate_field_schema(field);
properties.insert(name.clone(), field_schema);
if field.required {
required.push(name.clone());
}
}
match representation {
EnumRepresentation::External => {
let struct_schema = json!({
"type": "object",
"properties": properties,
"required": required,
"additionalProperties": false
});
json!({
"type": "object",
"properties": {
variant_name: struct_schema
},
"required": [variant_name],
"additionalProperties": false
})
}
EnumRepresentation::Internal { tag } => {
properties.insert(tag.clone(), json!({ "const": variant_name }));
required.push(tag.clone());
json!({
"type": "object",
"properties": properties,
"required": required,
"additionalProperties": false
})
}
EnumRepresentation::Adjacent { tag, content } => {
let struct_schema = json!({
"type": "object",
"properties": properties,
"required": required,
"additionalProperties": false
});
json!({
"type": "object",
"properties": {
tag: { "const": variant_name },
content: struct_schema
},
"required": [tag, content],
"additionalProperties": false
})
}
EnumRepresentation::Untagged => json!({
"type": "object",
"properties": properties,
"required": required,
"additionalProperties": false
}),
}
}
}
}
fn merge_properties(existing: Option<&Value>, new: &Value) -> Value {
match (existing, new) {
(Some(Value::Object(existing)), Value::Object(new)) => {
let mut merged = existing.clone();
for (k, v) in new {
merged.insert(k.clone(), v.clone());
}
Value::Object(merged)
}
(None, new) => new.clone(),
(Some(existing), _) => existing.clone(),
}
}
fn add_to_required(map: &mut Map<String, Value>, field: &str) {
let required = map
.entry("required".to_string())
.or_insert_with(|| json!([]));
if let Value::Array(ref mut arr) = required {
arr.push(json!(field));
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::{EnumVariant, NewtypeDefinition};
use indexmap::IndexMap;
#[test]
fn test_primitive_bool() {
let schema = generate_primitive_schema(&PrimitiveType::Bool);
assert_eq!(schema["type"], "boolean");
}
#[test]
fn test_primitive_i32() {
let schema = generate_primitive_schema(&PrimitiveType::I32);
assert_eq!(schema["type"], "integer");
assert_eq!(schema["minimum"], i32::MIN);
assert_eq!(schema["maximum"], i32::MAX);
}
#[test]
fn test_primitive_string() {
let schema = generate_primitive_schema(&PrimitiveType::String);
assert_eq!(schema["type"], "string");
}
#[test]
fn test_option_schema() {
let schema = generate_option_schema(&SchemaType::Primitive(PrimitiveType::String));
assert!(schema["oneOf"].is_array());
}
#[test]
fn test_array_schema() {
let schema = generate_array_schema(&SchemaType::Primitive(PrimitiveType::I32));
assert_eq!(schema["type"], "array");
assert_eq!(schema["items"]["type"], "integer");
}
#[test]
fn test_set_schema() {
let schema = generate_set_schema(&SchemaType::Primitive(PrimitiveType::String));
assert_eq!(schema["type"], "array");
assert_eq!(schema["uniqueItems"], true);
}
#[test]
fn test_map_schema() {
let schema = generate_map_schema(&SchemaType::Primitive(PrimitiveType::String));
assert_eq!(schema["type"], "object");
assert_eq!(schema["additionalProperties"]["type"], "string");
}
#[test]
fn test_tuple_schema() {
let types = vec![
Box::new(SchemaType::Primitive(PrimitiveType::I32)),
Box::new(SchemaType::Primitive(PrimitiveType::String)),
];
let schema = generate_tuple_schema(&types);
assert_eq!(schema["type"], "array");
assert_eq!(schema["prefixItems"].as_array().unwrap().len(), 2);
assert_eq!(schema["minItems"], 2);
assert_eq!(schema["maxItems"], 2);
}
#[test]
fn test_struct_schema() {
let def = StructDefinition::new()
.with_field(
"name",
StructField::new(SchemaType::Primitive(PrimitiveType::String), "name"),
)
.with_field(
"age",
StructField::new(
SchemaType::Option(Box::new(SchemaType::Primitive(PrimitiveType::U8))),
"age",
),
);
let schema = generate_struct_schema(&def);
assert_eq!(schema["type"], "object");
assert!(schema["properties"]["name"].is_object());
assert!(schema["properties"]["age"].is_object());
assert!(schema["required"]
.as_array()
.unwrap()
.contains(&json!("name")));
}
#[test]
fn test_simple_enum_schema() {
let def = EnumDefinition::new(EnumRepresentation::External)
.with_variant(EnumVariant::unit("Active"))
.with_variant(EnumVariant::unit("Inactive"));
let schema = generate_enum_schema(&def);
assert_eq!(schema["type"], "string");
assert!(schema["enum"].as_array().unwrap().contains(&json!("Active")));
}
#[test]
fn test_complex_enum_schema() {
let mut fields = IndexMap::new();
fields.insert(
"reason".to_string(),
StructField::new(SchemaType::Primitive(PrimitiveType::String), "reason"),
);
let def = EnumDefinition::new(EnumRepresentation::External)
.with_variant(EnumVariant::unit("Active"))
.with_variant(EnumVariant::struct_variant("Suspended", fields));
let schema = generate_enum_schema(&def);
assert!(schema["oneOf"].is_array());
assert_eq!(schema["oneOf"].as_array().unwrap().len(), 2);
}
#[test]
fn test_full_schema_definition() {
let def = SchemaDefinition::new(
"User",
SchemaType::Struct(
StructDefinition::new().with_field(
"id",
StructField::new(SchemaType::Primitive(PrimitiveType::U64), "id"),
),
),
)
.with_description("A user in the system");
let schema = generate_value(&def);
assert_eq!(schema["$schema"], SCHEMA_DIALECT);
assert_eq!(schema["title"], "User");
assert_eq!(schema["description"], "A user in the system");
}
#[test]
fn test_field_constraints() {
let field = StructField::new(SchemaType::Primitive(PrimitiveType::String), "name")
.with_length(Some(1), Some(100))
.with_pattern(r"^[a-zA-Z]+$")
.with_description("The user's name");
let schema = generate_field_schema(&field);
assert_eq!(schema["minLength"], 1);
assert_eq!(schema["maxLength"], 100);
assert_eq!(schema["pattern"], r"^[a-zA-Z]+$");
assert_eq!(schema["description"], "The user's name");
}
#[test]
fn test_newtype_transparent() {
let newtype = NewtypeDefinition::transparent(SchemaType::Primitive(PrimitiveType::String));
let schema = generate_type_schema(&SchemaType::Newtype(newtype));
assert_eq!(schema["type"], "string");
}
#[test]
fn test_reference_schema() {
let schema = generate_type_schema(&SchemaType::Reference("User".to_string()));
assert_eq!(schema["$ref"], "#/$defs/User");
}
}