use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum SchemaType {
String,
Number,
Integer,
Boolean,
Array,
Object,
Null,
}
impl std::fmt::Display for SchemaType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::String => write!(f, "string"),
Self::Number => write!(f, "number"),
Self::Integer => write!(f, "integer"),
Self::Boolean => write!(f, "boolean"),
Self::Array => write!(f, "array"),
Self::Object => write!(f, "object"),
Self::Null => write!(f, "null"),
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Schema {
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
pub schema_type: Option<SchemaType>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default: Option<Value>,
#[serde(rename = "enum", skip_serializing_if = "Option::is_none")]
pub enum_values: Option<Vec<Value>>,
#[serde(rename = "const", skip_serializing_if = "Option::is_none")]
pub const_value: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub min_length: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_length: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pattern: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub format: 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(skip_serializing_if = "Option::is_none")]
pub exclusive_minimum: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub exclusive_maximum: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub multiple_of: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub items: Option<Box<Schema>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub min_items: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_items: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub unique_items: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub properties: Option<HashMap<String, Schema>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub required: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub additional_properties: Option<AdditionalProperties>,
#[serde(skip_serializing_if = "Option::is_none")]
pub min_properties: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_properties: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub all_of: Option<Vec<Schema>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub any_of: Option<Vec<Schema>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub one_of: Option<Vec<Schema>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub not: Option<Box<Schema>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum AdditionalProperties {
Boolean(bool),
Schema(Box<Schema>),
}
impl Schema {
#[must_use]
pub fn any() -> Self {
Self::default()
}
#[must_use]
pub fn string() -> Self {
Self {
schema_type: Some(SchemaType::String),
..Default::default()
}
}
#[must_use]
pub fn number() -> Self {
Self {
schema_type: Some(SchemaType::Number),
..Default::default()
}
}
#[must_use]
pub fn integer() -> Self {
Self {
schema_type: Some(SchemaType::Integer),
..Default::default()
}
}
#[must_use]
pub fn boolean() -> Self {
Self {
schema_type: Some(SchemaType::Boolean),
..Default::default()
}
}
#[must_use]
pub fn array() -> Self {
Self {
schema_type: Some(SchemaType::Array),
..Default::default()
}
}
#[must_use]
pub fn object() -> Self {
Self {
schema_type: Some(SchemaType::Object),
..Default::default()
}
}
#[must_use]
pub fn null() -> Self {
Self {
schema_type: Some(SchemaType::Null),
..Default::default()
}
}
#[must_use]
pub fn to_value(&self) -> Value {
serde_json::to_value(self).unwrap_or_else(|_| Value::Object(serde_json::Map::new()))
}
}
#[derive(Debug, Clone, Default)]
pub struct SchemaBuilder {
schema: Schema,
}
impl SchemaBuilder {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn string() -> Self {
Self {
schema: Schema::string(),
}
}
#[must_use]
pub fn number() -> Self {
Self {
schema: Schema::number(),
}
}
#[must_use]
pub fn integer() -> Self {
Self {
schema: Schema::integer(),
}
}
#[must_use]
pub fn boolean() -> Self {
Self {
schema: Schema::boolean(),
}
}
#[must_use]
pub fn array() -> Self {
Self {
schema: Schema::array(),
}
}
#[must_use]
pub fn object() -> Self {
Self {
schema: Schema::object(),
}
}
#[must_use]
pub fn null() -> Self {
Self {
schema: Schema::null(),
}
}
#[must_use]
pub fn title(mut self, title: impl Into<String>) -> Self {
self.schema.title = Some(title.into());
self
}
#[must_use]
pub fn description(mut self, description: impl Into<String>) -> Self {
self.schema.description = Some(description.into());
self
}
#[must_use]
pub fn default_value(mut self, default: impl Into<Value>) -> Self {
self.schema.default = Some(default.into());
self
}
#[must_use]
pub fn enum_values<I, V>(mut self, values: I) -> Self
where
I: IntoIterator<Item = V>,
V: Into<Value>,
{
self.schema.enum_values = Some(values.into_iter().map(Into::into).collect());
self
}
#[must_use]
pub fn const_value(mut self, value: impl Into<Value>) -> Self {
self.schema.const_value = Some(value.into());
self
}
#[must_use]
pub const fn min_length(mut self, min: u64) -> Self {
self.schema.min_length = Some(min);
self
}
#[must_use]
pub const fn max_length(mut self, max: u64) -> Self {
self.schema.max_length = Some(max);
self
}
#[must_use]
pub fn pattern(mut self, pattern: impl Into<String>) -> Self {
self.schema.pattern = Some(pattern.into());
self
}
#[must_use]
pub fn format(mut self, format: impl Into<String>) -> Self {
self.schema.format = Some(format.into());
self
}
#[must_use]
pub fn minimum(mut self, min: impl Into<f64>) -> Self {
self.schema.minimum = Some(min.into());
self
}
#[must_use]
pub fn maximum(mut self, max: impl Into<f64>) -> Self {
self.schema.maximum = Some(max.into());
self
}
#[must_use]
pub fn exclusive_minimum(mut self, min: impl Into<f64>) -> Self {
self.schema.exclusive_minimum = Some(min.into());
self
}
#[must_use]
pub fn exclusive_maximum(mut self, max: impl Into<f64>) -> Self {
self.schema.exclusive_maximum = Some(max.into());
self
}
#[must_use]
pub fn multiple_of(mut self, multiple: impl Into<f64>) -> Self {
self.schema.multiple_of = Some(multiple.into());
self
}
#[must_use]
pub fn items(mut self, items: Self) -> Self {
self.schema.items = Some(Box::new(items.schema));
self
}
#[must_use]
pub const fn min_items(mut self, min: u64) -> Self {
self.schema.min_items = Some(min);
self
}
#[must_use]
pub const fn max_items(mut self, max: u64) -> Self {
self.schema.max_items = Some(max);
self
}
#[must_use]
pub const fn unique_items(mut self, unique: bool) -> Self {
self.schema.unique_items = Some(unique);
self
}
#[must_use]
pub fn property(mut self, name: impl Into<String>, schema: Self) -> Self {
let properties = self.schema.properties.get_or_insert_with(HashMap::new);
properties.insert(name.into(), schema.schema);
self
}
#[must_use]
pub fn required<I, S>(mut self, required: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.schema.required = Some(required.into_iter().map(Into::into).collect());
self
}
#[must_use]
pub fn additional_properties(mut self, allowed: bool) -> Self {
self.schema.additional_properties = Some(AdditionalProperties::Boolean(allowed));
self
}
#[must_use]
pub fn additional_properties_schema(mut self, schema: Self) -> Self {
self.schema.additional_properties =
Some(AdditionalProperties::Schema(Box::new(schema.schema)));
self
}
#[must_use]
pub const fn min_properties(mut self, min: u64) -> Self {
self.schema.min_properties = Some(min);
self
}
#[must_use]
pub const fn max_properties(mut self, max: u64) -> Self {
self.schema.max_properties = Some(max);
self
}
#[must_use]
pub fn all_of<I>(mut self, schemas: I) -> Self
where
I: IntoIterator<Item = Self>,
{
self.schema.all_of = Some(schemas.into_iter().map(|b| b.schema).collect());
self
}
#[must_use]
pub fn any_of<I>(mut self, schemas: I) -> Self
where
I: IntoIterator<Item = Self>,
{
self.schema.any_of = Some(schemas.into_iter().map(|b| b.schema).collect());
self
}
#[must_use]
pub fn one_of<I>(mut self, schemas: I) -> Self
where
I: IntoIterator<Item = Self>,
{
self.schema.one_of = Some(schemas.into_iter().map(|b| b.schema).collect());
self
}
#[must_use]
pub fn not(mut self, schema: Self) -> Self {
self.schema.not = Some(Box::new(schema.schema));
self
}
#[must_use]
pub fn build(self) -> Schema {
self.schema
}
#[must_use]
pub fn to_value(self) -> Value {
self.schema.to_value()
}
}
pub mod formats {
pub const EMAIL: &str = "email";
pub const URI: &str = "uri";
pub const URI_REFERENCE: &str = "uri-reference";
pub const DATE_TIME: &str = "date-time";
pub const DATE: &str = "date";
pub const TIME: &str = "time";
pub const DURATION: &str = "duration";
pub const UUID: &str = "uuid";
pub const HOSTNAME: &str = "hostname";
pub const IPV4: &str = "ipv4";
pub const IPV6: &str = "ipv6";
pub const JSON_POINTER: &str = "json-pointer";
pub const REGEX: &str = "regex";
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_string_schema() {
let schema = SchemaBuilder::string()
.description("A test string")
.min_length(1)
.max_length(100)
.pattern(r"^[a-z]+$")
.build();
assert_eq!(schema.schema_type, Some(SchemaType::String));
assert_eq!(schema.description.as_deref(), Some("A test string"));
assert_eq!(schema.min_length, Some(1));
assert_eq!(schema.max_length, Some(100));
}
#[test]
fn test_number_schema() {
let schema = SchemaBuilder::number().minimum(0).maximum(100).build();
assert_eq!(schema.schema_type, Some(SchemaType::Number));
assert_eq!(schema.minimum, Some(0.0));
assert_eq!(schema.maximum, Some(100.0));
}
#[test]
fn test_object_schema() {
let schema = SchemaBuilder::object()
.property("name", SchemaBuilder::string())
.property("age", SchemaBuilder::integer().minimum(0))
.required(["name"])
.additional_properties(false)
.build();
assert_eq!(schema.schema_type, Some(SchemaType::Object));
assert!(schema.properties.is_some());
let props = schema.properties.as_ref().unwrap();
assert!(props.contains_key("name"));
assert!(props.contains_key("age"));
assert_eq!(schema.required, Some(vec!["name".to_string()]));
}
#[test]
fn test_array_schema() {
let schema = SchemaBuilder::array()
.items(SchemaBuilder::string())
.min_items(1)
.unique_items(true)
.build();
assert_eq!(schema.schema_type, Some(SchemaType::Array));
assert!(schema.items.is_some());
assert_eq!(schema.min_items, Some(1));
assert_eq!(schema.unique_items, Some(true));
}
#[test]
fn test_enum_schema() {
let schema = SchemaBuilder::string()
.enum_values(["red", "green", "blue"])
.build();
assert!(schema.enum_values.is_some());
let values = schema.enum_values.as_ref().unwrap();
assert_eq!(values.len(), 3);
}
#[test]
fn test_composition() {
let schema = SchemaBuilder::new()
.one_of([SchemaBuilder::string(), SchemaBuilder::integer()])
.build();
assert!(schema.one_of.is_some());
assert_eq!(schema.one_of.as_ref().unwrap().len(), 2);
}
#[test]
fn test_to_value() -> Result<(), Box<dyn std::error::Error>> {
let schema = SchemaBuilder::object()
.property("query", SchemaBuilder::string())
.required(["query"])
.to_value();
assert!(schema.is_object());
let obj = schema.as_object().ok_or("Expected object")?;
assert_eq!(obj.get("type").and_then(|v| v.as_str()), Some("object"));
Ok(())
}
#[test]
fn test_tool_input_schema() -> Result<(), Box<dyn std::error::Error>> {
let schema = SchemaBuilder::object()
.title("SearchInput")
.description("Input parameters for the search tool")
.property(
"query",
SchemaBuilder::string()
.description("The search query")
.min_length(1),
)
.property(
"limit",
SchemaBuilder::integer()
.description("Maximum number of results")
.minimum(1)
.maximum(100)
.default_value(10),
)
.property(
"filters",
SchemaBuilder::array()
.items(SchemaBuilder::string())
.description("Optional filter tags"),
)
.required(["query"])
.additional_properties(false)
.build();
let value = schema.to_value();
assert!(value.is_object());
let obj = value.as_object().ok_or("Expected object")?;
assert_eq!(obj.get("type").and_then(|v| v.as_str()), Some("object"));
assert_eq!(
obj.get("title").and_then(|v| v.as_str()),
Some("SearchInput")
);
Ok(())
}
#[test]
fn test_schema_type_display() {
assert_eq!(SchemaType::String.to_string(), "string");
assert_eq!(SchemaType::Number.to_string(), "number");
assert_eq!(SchemaType::Integer.to_string(), "integer");
assert_eq!(SchemaType::Boolean.to_string(), "boolean");
assert_eq!(SchemaType::Array.to_string(), "array");
assert_eq!(SchemaType::Object.to_string(), "object");
assert_eq!(SchemaType::Null.to_string(), "null");
}
#[test]
fn test_schema_any() {
let schema = Schema::any();
assert!(schema.schema_type.is_none());
}
#[test]
fn test_schema_null() {
let schema = Schema::null();
assert_eq!(schema.schema_type, Some(SchemaType::Null));
}
#[test]
fn test_schema_boolean() {
let schema = SchemaBuilder::boolean().build();
assert_eq!(schema.schema_type, Some(SchemaType::Boolean));
}
#[test]
fn test_string_format() {
let schema = SchemaBuilder::string().format(formats::EMAIL).build();
assert_eq!(schema.format, Some("email".to_string()));
}
#[test]
fn test_exclusive_bounds() {
let schema = SchemaBuilder::number()
.exclusive_minimum(0)
.exclusive_maximum(100)
.build();
assert_eq!(schema.exclusive_minimum, Some(0.0));
assert_eq!(schema.exclusive_maximum, Some(100.0));
}
#[test]
fn test_multiple_of() {
let schema = SchemaBuilder::integer().multiple_of(5).build();
assert_eq!(schema.multiple_of, Some(5.0));
}
#[test]
fn test_const_value() {
let schema = SchemaBuilder::string().const_value("constant").build();
assert_eq!(
schema.const_value,
Some(Value::String("constant".to_string()))
);
}
#[test]
fn test_min_max_properties() {
let schema = SchemaBuilder::object()
.min_properties(1)
.max_properties(10)
.build();
assert_eq!(schema.min_properties, Some(1));
assert_eq!(schema.max_properties, Some(10));
}
#[test]
fn test_max_items() {
let schema = SchemaBuilder::array()
.items(SchemaBuilder::string())
.max_items(5)
.build();
assert_eq!(schema.max_items, Some(5));
}
#[test]
fn test_additional_properties_schema() {
let schema = SchemaBuilder::object()
.additional_properties_schema(SchemaBuilder::string())
.build();
assert!(schema.additional_properties.is_some());
match schema.additional_properties {
Some(AdditionalProperties::Schema(s)) => {
assert_eq!(s.schema_type, Some(SchemaType::String));
}
_ => panic!("Expected schema for additional properties"),
}
}
#[test]
fn test_all_of_composition() {
let schema = SchemaBuilder::new()
.all_of([
SchemaBuilder::object().property("name", SchemaBuilder::string()),
SchemaBuilder::object().property("age", SchemaBuilder::integer()),
])
.build();
assert!(schema.all_of.is_some());
assert_eq!(schema.all_of.as_ref().unwrap().len(), 2);
}
#[test]
fn test_any_of_composition() {
let schema = SchemaBuilder::new()
.any_of([SchemaBuilder::string(), SchemaBuilder::null()])
.build();
assert!(schema.any_of.is_some());
assert_eq!(schema.any_of.as_ref().unwrap().len(), 2);
}
#[test]
fn test_not_composition() {
let schema = SchemaBuilder::new().not(SchemaBuilder::null()).build();
assert!(schema.not.is_some());
assert_eq!(
schema.not.as_ref().unwrap().schema_type,
Some(SchemaType::Null)
);
}
#[test]
fn test_integer_schema() {
let schema = SchemaBuilder::integer().minimum(0).maximum(100).build();
assert_eq!(schema.schema_type, Some(SchemaType::Integer));
}
#[test]
fn test_schema_serialization() {
let schema = SchemaBuilder::object()
.property("id", SchemaBuilder::integer())
.required(["id"])
.build();
let json = serde_json::to_string(&schema).unwrap();
let parsed: Schema = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.schema_type, Some(SchemaType::Object));
assert!(parsed.properties.is_some());
assert!(parsed.required.is_some());
}
#[test]
fn test_formats_constants() {
assert_eq!(formats::EMAIL, "email");
assert_eq!(formats::URI, "uri");
assert_eq!(formats::URI_REFERENCE, "uri-reference");
assert_eq!(formats::DATE_TIME, "date-time");
assert_eq!(formats::DATE, "date");
assert_eq!(formats::TIME, "time");
assert_eq!(formats::DURATION, "duration");
assert_eq!(formats::UUID, "uuid");
assert_eq!(formats::HOSTNAME, "hostname");
assert_eq!(formats::IPV4, "ipv4");
assert_eq!(formats::IPV6, "ipv6");
assert_eq!(formats::JSON_POINTER, "json-pointer");
assert_eq!(formats::REGEX, "regex");
}
}