use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct JsonSchema {
#[serde(rename = "$schema", skip_serializing_if = "Option::is_none")]
pub schema: Option<String>,
#[serde(rename = "$id", skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
pub schema_type: Option<JsonSchemaType>,
#[serde(skip_serializing_if = "Option::is_none")]
pub format: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub properties: Option<HashMap<String, JsonSchema>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub required: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub items: Option<Box<JsonSchema>>,
#[serde(
rename = "additionalProperties",
skip_serializing_if = "Option::is_none"
)]
pub additional_properties: Option<AdditionalProperties>,
#[serde(rename = "minLength", skip_serializing_if = "Option::is_none")]
pub min_length: Option<usize>,
#[serde(rename = "maxLength", skip_serializing_if = "Option::is_none")]
pub max_length: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pattern: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub minimum: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub maximum: Option<f64>,
#[serde(rename = "exclusiveMinimum", skip_serializing_if = "Option::is_none")]
pub exclusive_minimum: Option<f64>,
#[serde(rename = "exclusiveMaximum", skip_serializing_if = "Option::is_none")]
pub exclusive_maximum: Option<f64>,
#[serde(rename = "multipleOf", skip_serializing_if = "Option::is_none")]
pub multiple_of: Option<f64>,
#[serde(rename = "minItems", skip_serializing_if = "Option::is_none")]
pub min_items: Option<usize>,
#[serde(rename = "maxItems", skip_serializing_if = "Option::is_none")]
pub max_items: Option<usize>,
#[serde(rename = "uniqueItems", skip_serializing_if = "Option::is_none")]
pub unique_items: Option<bool>,
#[serde(rename = "enum", skip_serializing_if = "Option::is_none")]
pub r#enum: Option<Vec<serde_json::Value>>,
#[serde(rename = "const", skip_serializing_if = "Option::is_none")]
pub r#const: Option<serde_json::Value>,
#[serde(rename = "$ref", skip_serializing_if = "Option::is_none")]
pub reference: Option<String>,
#[serde(rename = "anyOf", skip_serializing_if = "Option::is_none")]
pub any_of: Option<Vec<JsonSchema>>,
#[serde(rename = "allOf", skip_serializing_if = "Option::is_none")]
pub all_of: Option<Vec<JsonSchema>>,
#[serde(rename = "oneOf", skip_serializing_if = "Option::is_none")]
pub one_of: Option<Vec<JsonSchema>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub not: Option<Box<JsonSchema>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub examples: Option<Vec<serde_json::Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub deprecated: Option<bool>,
#[serde(rename = "readOnly", skip_serializing_if = "Option::is_none")]
pub read_only: Option<bool>,
#[serde(rename = "writeOnly", skip_serializing_if = "Option::is_none")]
pub write_only: Option<bool>,
#[serde(rename = "$defs", skip_serializing_if = "Option::is_none")]
pub defs: Option<HashMap<String, JsonSchema>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum JsonSchemaType {
String,
Number,
Integer,
Boolean,
Array,
Object,
Null,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum AdditionalProperties {
Bool(bool),
Schema(Box<JsonSchema>),
}
impl JsonSchema {
pub fn new() -> Self {
Self {
schema: None,
id: None,
title: None,
description: None,
schema_type: None,
format: None,
properties: None,
required: None,
items: None,
additional_properties: None,
min_length: None,
max_length: None,
pattern: None,
minimum: None,
maximum: None,
exclusive_minimum: None,
exclusive_maximum: None,
multiple_of: None,
min_items: None,
max_items: None,
unique_items: None,
r#enum: None,
r#const: None,
reference: None,
any_of: None,
all_of: None,
one_of: None,
not: None,
default: None,
examples: None,
deprecated: None,
read_only: None,
write_only: None,
defs: None,
}
}
pub fn string() -> Self {
Self {
schema_type: Some(JsonSchemaType::String),
..Self::new()
}
}
pub fn integer() -> Self {
Self {
schema_type: Some(JsonSchemaType::Integer),
..Self::new()
}
}
pub fn number() -> Self {
Self {
schema_type: Some(JsonSchemaType::Number),
..Self::new()
}
}
pub fn boolean() -> Self {
Self {
schema_type: Some(JsonSchemaType::Boolean),
..Self::new()
}
}
pub fn array(items: JsonSchema) -> Self {
Self {
schema_type: Some(JsonSchemaType::Array),
items: Some(Box::new(items)),
..Self::new()
}
}
pub fn object() -> Self {
Self {
schema_type: Some(JsonSchemaType::Object),
properties: Some(HashMap::new()),
additional_properties: Some(AdditionalProperties::Bool(false)),
..Self::new()
}
}
pub fn null() -> Self {
Self {
schema_type: Some(JsonSchemaType::Null),
..Self::new()
}
}
pub fn reference(name: &str) -> Self {
Self {
reference: Some(format!("#/$defs/{}", name)),
..Self::new()
}
}
pub fn title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
pub fn description(mut self, desc: impl Into<String>) -> Self {
self.description = Some(desc.into());
self
}
pub fn format(mut self, format: impl Into<String>) -> Self {
self.format = Some(format.into());
self
}
pub fn property(mut self, name: impl Into<String>, schema: JsonSchema) -> Self {
self.properties
.get_or_insert_with(HashMap::new)
.insert(name.into(), schema);
self
}
pub fn required(mut self, fields: &[&str]) -> Self {
self.required = Some(fields.iter().map(|s| s.to_string()).collect());
self
}
pub fn min_length(mut self, min: usize) -> Self {
self.min_length = Some(min);
self
}
pub fn max_length(mut self, max: usize) -> Self {
self.max_length = Some(max);
self
}
pub fn pattern(mut self, pattern: impl Into<String>) -> Self {
self.pattern = Some(pattern.into());
self
}
pub fn minimum(mut self, min: impl Into<f64>) -> Self {
self.minimum = Some(min.into());
self
}
pub fn maximum(mut self, max: impl Into<f64>) -> Self {
self.maximum = Some(max.into());
self
}
pub fn exclusive_minimum(mut self, min: impl Into<f64>) -> Self {
self.exclusive_minimum = Some(min.into());
self
}
pub fn exclusive_maximum(mut self, max: impl Into<f64>) -> Self {
self.exclusive_maximum = Some(max.into());
self
}
pub fn multiple_of(mut self, divisor: impl Into<f64>) -> Self {
self.multiple_of = Some(divisor.into());
self
}
pub fn min_items(mut self, min: usize) -> Self {
self.min_items = Some(min);
self
}
pub fn max_items(mut self, max: usize) -> Self {
self.max_items = Some(max);
self
}
pub fn unique_items(mut self, unique: bool) -> Self {
self.unique_items = Some(unique);
self
}
pub fn enum_values<T: Serialize>(mut self, values: &[T]) -> Self {
self.r#enum = Some(
values
.iter()
.map(|v| serde_json::to_value(v).expect("Failed to serialize enum value"))
.collect(),
);
self
}
pub fn const_value<T: Serialize>(mut self, value: T) -> Self {
self.r#const = Some(serde_json::to_value(value).expect("Failed to serialize const value"));
self
}
pub fn default<T: Serialize>(mut self, value: T) -> Self {
self.default =
Some(serde_json::to_value(value).expect("Failed to serialize default value"));
self
}
pub fn examples<T: Serialize>(mut self, values: Vec<T>) -> Self {
self.examples = Some(
values
.into_iter()
.map(|v| serde_json::to_value(v).expect("Failed to serialize example value"))
.collect(),
);
self
}
pub fn deprecated(mut self, deprecated: bool) -> Self {
self.deprecated = Some(deprecated);
self
}
pub fn read_only(mut self, read_only: bool) -> Self {
self.read_only = Some(read_only);
self
}
pub fn write_only(mut self, write_only: bool) -> Self {
self.write_only = Some(write_only);
self
}
pub fn any_of(schemas: Vec<JsonSchema>) -> Self {
Self {
any_of: Some(schemas),
..Self::new()
}
}
pub fn all_of(schemas: Vec<JsonSchema>) -> Self {
Self {
all_of: Some(schemas),
..Self::new()
}
}
pub fn one_of(schemas: Vec<JsonSchema>) -> Self {
Self {
one_of: Some(schemas),
..Self::new()
}
}
pub fn negation(schema: JsonSchema) -> Self {
Self {
not: Some(Box::new(schema)),
..Self::new()
}
}
}
impl Default for JsonSchema {
fn default() -> Self {
Self::new()
}
}
pub trait ToJsonSchema {
fn schema_name() -> &'static str;
fn json_schema() -> JsonSchema;
}
impl ToJsonSchema for String {
fn schema_name() -> &'static str {
"string"
}
fn json_schema() -> JsonSchema {
JsonSchema::string()
}
}
impl ToJsonSchema for str {
fn schema_name() -> &'static str {
"string"
}
fn json_schema() -> JsonSchema {
JsonSchema::string()
}
}
impl ToJsonSchema for u8 {
fn schema_name() -> &'static str {
"integer"
}
fn json_schema() -> JsonSchema {
JsonSchema::integer().minimum(0).maximum(255)
}
}
impl ToJsonSchema for u16 {
fn schema_name() -> &'static str {
"integer"
}
fn json_schema() -> JsonSchema {
JsonSchema::integer().minimum(0).maximum(65535)
}
}
impl ToJsonSchema for u32 {
fn schema_name() -> &'static str {
"integer"
}
fn json_schema() -> JsonSchema {
JsonSchema::integer().minimum(0)
}
}
impl ToJsonSchema for i32 {
fn schema_name() -> &'static str {
"integer"
}
fn json_schema() -> JsonSchema {
JsonSchema::integer()
}
}
impl ToJsonSchema for i64 {
fn schema_name() -> &'static str {
"integer"
}
fn json_schema() -> JsonSchema {
JsonSchema::integer()
}
}
impl ToJsonSchema for f32 {
fn schema_name() -> &'static str {
"number"
}
fn json_schema() -> JsonSchema {
JsonSchema::number()
}
}
impl ToJsonSchema for f64 {
fn schema_name() -> &'static str {
"number"
}
fn json_schema() -> JsonSchema {
JsonSchema::number()
}
}
impl ToJsonSchema for bool {
fn schema_name() -> &'static str {
"boolean"
}
fn json_schema() -> JsonSchema {
JsonSchema::boolean()
}
}
impl<T: ToJsonSchema> ToJsonSchema for Vec<T> {
fn schema_name() -> &'static str {
"array"
}
fn json_schema() -> JsonSchema {
JsonSchema::array(T::json_schema())
}
}
impl<T: ToJsonSchema> ToJsonSchema for Option<T> {
fn schema_name() -> &'static str {
T::schema_name()
}
fn json_schema() -> JsonSchema {
T::json_schema()
}
}
pub struct JsonSchemaBuilder {
id: Option<String>,
title: Option<String>,
description: Option<String>,
defs: HashMap<String, JsonSchema>,
}
impl JsonSchemaBuilder {
pub fn new() -> Self {
Self {
id: None,
title: None,
description: None,
defs: HashMap::new(),
}
}
pub fn id(mut self, id: impl Into<String>) -> Self {
self.id = Some(id.into());
self
}
pub fn title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
pub fn description(mut self, desc: impl Into<String>) -> Self {
self.description = Some(desc.into());
self
}
pub fn register<T: ToJsonSchema>(mut self) -> Self {
self.defs
.insert(T::schema_name().to_string(), T::json_schema());
self
}
pub fn add_schema(mut self, name: impl Into<String>, schema: JsonSchema) -> Self {
self.defs.insert(name.into(), schema);
self
}
pub fn build(self) -> JsonSchema {
JsonSchema {
schema: Some("https://json-schema.org/draft/2020-12/schema".to_string()),
id: self.id,
title: self.title,
description: self.description,
defs: if self.defs.is_empty() {
None
} else {
Some(self.defs)
},
..JsonSchema::new()
}
}
pub fn to_json(&self) -> Result<String, serde_json::Error> {
let schema = JsonSchema {
schema: Some("https://json-schema.org/draft/2020-12/schema".to_string()),
id: self.id.clone(),
title: self.title.clone(),
description: self.description.clone(),
defs: if self.defs.is_empty() {
None
} else {
Some(self.defs.clone())
},
..JsonSchema::new()
};
serde_json::to_string_pretty(&schema)
}
}
impl Default for JsonSchemaBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_string_schema() {
let schema = JsonSchema::string().min_length(3).max_length(50);
assert!(matches!(schema.schema_type, Some(JsonSchemaType::String)));
assert_eq!(schema.min_length, Some(3));
assert_eq!(schema.max_length, Some(50));
}
#[test]
fn test_integer_schema() {
let schema = JsonSchema::integer().minimum(0).maximum(100);
assert!(matches!(schema.schema_type, Some(JsonSchemaType::Integer)));
assert_eq!(schema.minimum, Some(0.0));
assert_eq!(schema.maximum, Some(100.0));
}
#[test]
fn test_object_schema() {
let schema = JsonSchema::object()
.property("name", JsonSchema::string())
.property("age", JsonSchema::integer())
.required(&["name"]);
assert!(matches!(schema.schema_type, Some(JsonSchemaType::Object)));
assert_eq!(schema.properties.as_ref().unwrap().len(), 2);
assert!(schema
.required
.as_ref()
.unwrap()
.contains(&"name".to_string()));
}
#[test]
fn test_array_schema() {
let schema = JsonSchema::array(JsonSchema::string())
.min_items(1)
.max_items(10);
assert!(matches!(schema.schema_type, Some(JsonSchemaType::Array)));
assert!(schema.items.is_some());
assert_eq!(schema.min_items, Some(1));
}
#[test]
fn test_reference() {
let schema = JsonSchema::reference("User");
assert_eq!(schema.reference, Some("#/$defs/User".to_string()));
}
#[test]
fn test_any_of() {
let schema = JsonSchema::any_of(vec![JsonSchema::string(), JsonSchema::integer()]);
assert!(schema.any_of.is_some());
assert_eq!(schema.any_of.as_ref().unwrap().len(), 2);
}
#[test]
fn test_builder() {
struct User;
impl ToJsonSchema for User {
fn schema_name() -> &'static str {
"User"
}
fn json_schema() -> JsonSchema {
JsonSchema::object()
.property("email", JsonSchema::string().format("email"))
.required(&["email"])
}
}
let doc = JsonSchemaBuilder::new()
.title("My Schema")
.register::<User>()
.build();
assert!(doc.schema.is_some());
assert_eq!(doc.title, Some("My Schema".to_string()));
assert!(doc.defs.as_ref().unwrap().contains_key("User"));
}
#[test]
fn test_serialization() {
let schema = JsonSchema::object()
.property("email", JsonSchema::string().format("email"))
.property("age", JsonSchema::integer().minimum(0))
.required(&["email", "age"]);
let json = serde_json::to_string(&schema).unwrap();
assert!(json.contains("\"type\":\"object\""));
assert!(json.contains("\"email\""));
}
}