1use serde::{Deserialize, Serialize};
27use serde_json::Value;
28use std::collections::HashMap;
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
32#[serde(rename_all = "lowercase")]
33pub enum SchemaType {
34 String,
36 Number,
38 Integer,
40 Boolean,
42 Array,
44 Object,
46 Null,
48}
49
50impl std::fmt::Display for SchemaType {
51 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52 match self {
53 Self::String => write!(f, "string"),
54 Self::Number => write!(f, "number"),
55 Self::Integer => write!(f, "integer"),
56 Self::Boolean => write!(f, "boolean"),
57 Self::Array => write!(f, "array"),
58 Self::Object => write!(f, "object"),
59 Self::Null => write!(f, "null"),
60 }
61 }
62}
63
64#[derive(Debug, Clone, Default, Serialize, Deserialize)]
69#[serde(rename_all = "camelCase")]
70pub struct Schema {
71 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
73 pub schema_type: Option<SchemaType>,
74
75 #[serde(skip_serializing_if = "Option::is_none")]
77 pub description: Option<String>,
78
79 #[serde(skip_serializing_if = "Option::is_none")]
81 pub title: Option<String>,
82
83 #[serde(skip_serializing_if = "Option::is_none")]
85 pub default: Option<Value>,
86
87 #[serde(rename = "enum", skip_serializing_if = "Option::is_none")]
89 pub enum_values: Option<Vec<Value>>,
90
91 #[serde(rename = "const", skip_serializing_if = "Option::is_none")]
93 pub const_value: Option<Value>,
94
95 #[serde(skip_serializing_if = "Option::is_none")]
98 pub min_length: Option<u64>,
99
100 #[serde(skip_serializing_if = "Option::is_none")]
102 pub max_length: Option<u64>,
103
104 #[serde(skip_serializing_if = "Option::is_none")]
106 pub pattern: Option<String>,
107
108 #[serde(skip_serializing_if = "Option::is_none")]
110 pub format: Option<String>,
111
112 #[serde(skip_serializing_if = "Option::is_none")]
115 pub minimum: Option<f64>,
116
117 #[serde(skip_serializing_if = "Option::is_none")]
119 pub maximum: Option<f64>,
120
121 #[serde(skip_serializing_if = "Option::is_none")]
123 pub exclusive_minimum: Option<f64>,
124
125 #[serde(skip_serializing_if = "Option::is_none")]
127 pub exclusive_maximum: Option<f64>,
128
129 #[serde(skip_serializing_if = "Option::is_none")]
131 pub multiple_of: Option<f64>,
132
133 #[serde(skip_serializing_if = "Option::is_none")]
136 pub items: Option<Box<Schema>>,
137
138 #[serde(skip_serializing_if = "Option::is_none")]
140 pub min_items: Option<u64>,
141
142 #[serde(skip_serializing_if = "Option::is_none")]
144 pub max_items: Option<u64>,
145
146 #[serde(skip_serializing_if = "Option::is_none")]
148 pub unique_items: Option<bool>,
149
150 #[serde(skip_serializing_if = "Option::is_none")]
153 pub properties: Option<HashMap<String, Schema>>,
154
155 #[serde(skip_serializing_if = "Option::is_none")]
157 pub required: Option<Vec<String>>,
158
159 #[serde(skip_serializing_if = "Option::is_none")]
161 pub additional_properties: Option<AdditionalProperties>,
162
163 #[serde(skip_serializing_if = "Option::is_none")]
165 pub min_properties: Option<u64>,
166
167 #[serde(skip_serializing_if = "Option::is_none")]
169 pub max_properties: Option<u64>,
170
171 #[serde(skip_serializing_if = "Option::is_none")]
174 pub all_of: Option<Vec<Schema>>,
175
176 #[serde(skip_serializing_if = "Option::is_none")]
178 pub any_of: Option<Vec<Schema>>,
179
180 #[serde(skip_serializing_if = "Option::is_none")]
182 pub one_of: Option<Vec<Schema>>,
183
184 #[serde(skip_serializing_if = "Option::is_none")]
186 pub not: Option<Box<Schema>>,
187}
188
189#[derive(Debug, Clone, Serialize, Deserialize)]
191#[serde(untagged)]
192pub enum AdditionalProperties {
193 Boolean(bool),
195 Schema(Box<Schema>),
197}
198
199impl Schema {
200 #[must_use]
202 pub fn any() -> Self {
203 Self::default()
204 }
205
206 #[must_use]
208 pub fn string() -> Self {
209 Self {
210 schema_type: Some(SchemaType::String),
211 ..Default::default()
212 }
213 }
214
215 #[must_use]
217 pub fn number() -> Self {
218 Self {
219 schema_type: Some(SchemaType::Number),
220 ..Default::default()
221 }
222 }
223
224 #[must_use]
226 pub fn integer() -> Self {
227 Self {
228 schema_type: Some(SchemaType::Integer),
229 ..Default::default()
230 }
231 }
232
233 #[must_use]
235 pub fn boolean() -> Self {
236 Self {
237 schema_type: Some(SchemaType::Boolean),
238 ..Default::default()
239 }
240 }
241
242 #[must_use]
244 pub fn array() -> Self {
245 Self {
246 schema_type: Some(SchemaType::Array),
247 ..Default::default()
248 }
249 }
250
251 #[must_use]
253 pub fn object() -> Self {
254 Self {
255 schema_type: Some(SchemaType::Object),
256 ..Default::default()
257 }
258 }
259
260 #[must_use]
262 pub fn null() -> Self {
263 Self {
264 schema_type: Some(SchemaType::Null),
265 ..Default::default()
266 }
267 }
268
269 #[must_use]
271 pub fn to_value(&self) -> Value {
272 serde_json::to_value(self).unwrap_or_else(|_| Value::Object(serde_json::Map::new()))
273 }
274}
275
276#[derive(Debug, Clone, Default)]
292pub struct SchemaBuilder {
293 schema: Schema,
294}
295
296impl SchemaBuilder {
297 #[must_use]
299 pub fn new() -> Self {
300 Self::default()
301 }
302
303 #[must_use]
305 pub fn string() -> Self {
306 Self {
307 schema: Schema::string(),
308 }
309 }
310
311 #[must_use]
313 pub fn number() -> Self {
314 Self {
315 schema: Schema::number(),
316 }
317 }
318
319 #[must_use]
321 pub fn integer() -> Self {
322 Self {
323 schema: Schema::integer(),
324 }
325 }
326
327 #[must_use]
329 pub fn boolean() -> Self {
330 Self {
331 schema: Schema::boolean(),
332 }
333 }
334
335 #[must_use]
337 pub fn array() -> Self {
338 Self {
339 schema: Schema::array(),
340 }
341 }
342
343 #[must_use]
345 pub fn object() -> Self {
346 Self {
347 schema: Schema::object(),
348 }
349 }
350
351 #[must_use]
353 pub fn null() -> Self {
354 Self {
355 schema: Schema::null(),
356 }
357 }
358
359 #[must_use]
361 pub fn title(mut self, title: impl Into<String>) -> Self {
362 self.schema.title = Some(title.into());
363 self
364 }
365
366 #[must_use]
368 pub fn description(mut self, description: impl Into<String>) -> Self {
369 self.schema.description = Some(description.into());
370 self
371 }
372
373 #[must_use]
375 pub fn default_value(mut self, default: impl Into<Value>) -> Self {
376 self.schema.default = Some(default.into());
377 self
378 }
379
380 #[must_use]
382 pub fn enum_values<I, V>(mut self, values: I) -> Self
383 where
384 I: IntoIterator<Item = V>,
385 V: Into<Value>,
386 {
387 self.schema.enum_values = Some(values.into_iter().map(Into::into).collect());
388 self
389 }
390
391 #[must_use]
393 pub fn const_value(mut self, value: impl Into<Value>) -> Self {
394 self.schema.const_value = Some(value.into());
395 self
396 }
397
398 #[must_use]
402 pub const fn min_length(mut self, min: u64) -> Self {
403 self.schema.min_length = Some(min);
404 self
405 }
406
407 #[must_use]
409 pub const fn max_length(mut self, max: u64) -> Self {
410 self.schema.max_length = Some(max);
411 self
412 }
413
414 #[must_use]
416 pub fn pattern(mut self, pattern: impl Into<String>) -> Self {
417 self.schema.pattern = Some(pattern.into());
418 self
419 }
420
421 #[must_use]
423 pub fn format(mut self, format: impl Into<String>) -> Self {
424 self.schema.format = Some(format.into());
425 self
426 }
427
428 #[must_use]
432 pub fn minimum(mut self, min: impl Into<f64>) -> Self {
433 self.schema.minimum = Some(min.into());
434 self
435 }
436
437 #[must_use]
439 pub fn maximum(mut self, max: impl Into<f64>) -> Self {
440 self.schema.maximum = Some(max.into());
441 self
442 }
443
444 #[must_use]
446 pub fn exclusive_minimum(mut self, min: impl Into<f64>) -> Self {
447 self.schema.exclusive_minimum = Some(min.into());
448 self
449 }
450
451 #[must_use]
453 pub fn exclusive_maximum(mut self, max: impl Into<f64>) -> Self {
454 self.schema.exclusive_maximum = Some(max.into());
455 self
456 }
457
458 #[must_use]
460 pub fn multiple_of(mut self, multiple: impl Into<f64>) -> Self {
461 self.schema.multiple_of = Some(multiple.into());
462 self
463 }
464
465 #[must_use]
469 pub fn items(mut self, items: Self) -> Self {
470 self.schema.items = Some(Box::new(items.schema));
471 self
472 }
473
474 #[must_use]
476 pub const fn min_items(mut self, min: u64) -> Self {
477 self.schema.min_items = Some(min);
478 self
479 }
480
481 #[must_use]
483 pub const fn max_items(mut self, max: u64) -> Self {
484 self.schema.max_items = Some(max);
485 self
486 }
487
488 #[must_use]
490 pub const fn unique_items(mut self, unique: bool) -> Self {
491 self.schema.unique_items = Some(unique);
492 self
493 }
494
495 #[must_use]
499 pub fn property(mut self, name: impl Into<String>, schema: Self) -> Self {
500 let properties = self.schema.properties.get_or_insert_with(HashMap::new);
501 properties.insert(name.into(), schema.schema);
502 self
503 }
504
505 #[must_use]
507 pub fn required<I, S>(mut self, required: I) -> Self
508 where
509 I: IntoIterator<Item = S>,
510 S: Into<String>,
511 {
512 self.schema.required = Some(required.into_iter().map(Into::into).collect());
513 self
514 }
515
516 #[must_use]
518 pub fn additional_properties(mut self, allowed: bool) -> Self {
519 self.schema.additional_properties = Some(AdditionalProperties::Boolean(allowed));
520 self
521 }
522
523 #[must_use]
525 pub fn additional_properties_schema(mut self, schema: Self) -> Self {
526 self.schema.additional_properties =
527 Some(AdditionalProperties::Schema(Box::new(schema.schema)));
528 self
529 }
530
531 #[must_use]
533 pub const fn min_properties(mut self, min: u64) -> Self {
534 self.schema.min_properties = Some(min);
535 self
536 }
537
538 #[must_use]
540 pub const fn max_properties(mut self, max: u64) -> Self {
541 self.schema.max_properties = Some(max);
542 self
543 }
544
545 #[must_use]
549 pub fn all_of<I>(mut self, schemas: I) -> Self
550 where
551 I: IntoIterator<Item = Self>,
552 {
553 self.schema.all_of = Some(schemas.into_iter().map(|b| b.schema).collect());
554 self
555 }
556
557 #[must_use]
559 pub fn any_of<I>(mut self, schemas: I) -> Self
560 where
561 I: IntoIterator<Item = Self>,
562 {
563 self.schema.any_of = Some(schemas.into_iter().map(|b| b.schema).collect());
564 self
565 }
566
567 #[must_use]
569 pub fn one_of<I>(mut self, schemas: I) -> Self
570 where
571 I: IntoIterator<Item = Self>,
572 {
573 self.schema.one_of = Some(schemas.into_iter().map(|b| b.schema).collect());
574 self
575 }
576
577 #[must_use]
579 pub fn not(mut self, schema: Self) -> Self {
580 self.schema.not = Some(Box::new(schema.schema));
581 self
582 }
583
584 #[must_use]
586 pub fn build(self) -> Schema {
587 self.schema
588 }
589
590 #[must_use]
592 pub fn to_value(self) -> Value {
593 self.schema.to_value()
594 }
595}
596
597pub mod formats {
599 pub const EMAIL: &str = "email";
601 pub const URI: &str = "uri";
603 pub const URI_REFERENCE: &str = "uri-reference";
605 pub const DATE_TIME: &str = "date-time";
607 pub const DATE: &str = "date";
609 pub const TIME: &str = "time";
611 pub const DURATION: &str = "duration";
613 pub const UUID: &str = "uuid";
615 pub const HOSTNAME: &str = "hostname";
617 pub const IPV4: &str = "ipv4";
619 pub const IPV6: &str = "ipv6";
621 pub const JSON_POINTER: &str = "json-pointer";
623 pub const REGEX: &str = "regex";
625}
626
627#[cfg(test)]
628mod tests {
629 use super::*;
630
631 #[test]
632 fn test_string_schema() {
633 let schema = SchemaBuilder::string()
634 .description("A test string")
635 .min_length(1)
636 .max_length(100)
637 .pattern(r"^[a-z]+$")
638 .build();
639
640 assert_eq!(schema.schema_type, Some(SchemaType::String));
641 assert_eq!(schema.description.as_deref(), Some("A test string"));
642 assert_eq!(schema.min_length, Some(1));
643 assert_eq!(schema.max_length, Some(100));
644 }
645
646 #[test]
647 fn test_number_schema() {
648 let schema = SchemaBuilder::number().minimum(0).maximum(100).build();
649
650 assert_eq!(schema.schema_type, Some(SchemaType::Number));
651 assert_eq!(schema.minimum, Some(0.0));
652 assert_eq!(schema.maximum, Some(100.0));
653 }
654
655 #[test]
656 fn test_object_schema() {
657 let schema = SchemaBuilder::object()
658 .property("name", SchemaBuilder::string())
659 .property("age", SchemaBuilder::integer().minimum(0))
660 .required(["name"])
661 .additional_properties(false)
662 .build();
663
664 assert_eq!(schema.schema_type, Some(SchemaType::Object));
665 assert!(schema.properties.is_some());
666 let props = schema.properties.as_ref().unwrap();
667 assert!(props.contains_key("name"));
668 assert!(props.contains_key("age"));
669 assert_eq!(schema.required, Some(vec!["name".to_string()]));
670 }
671
672 #[test]
673 fn test_array_schema() {
674 let schema = SchemaBuilder::array()
675 .items(SchemaBuilder::string())
676 .min_items(1)
677 .unique_items(true)
678 .build();
679
680 assert_eq!(schema.schema_type, Some(SchemaType::Array));
681 assert!(schema.items.is_some());
682 assert_eq!(schema.min_items, Some(1));
683 assert_eq!(schema.unique_items, Some(true));
684 }
685
686 #[test]
687 fn test_enum_schema() {
688 let schema = SchemaBuilder::string()
689 .enum_values(["red", "green", "blue"])
690 .build();
691
692 assert!(schema.enum_values.is_some());
693 let values = schema.enum_values.as_ref().unwrap();
694 assert_eq!(values.len(), 3);
695 }
696
697 #[test]
698 fn test_composition() {
699 let schema = SchemaBuilder::new()
700 .one_of([SchemaBuilder::string(), SchemaBuilder::integer()])
701 .build();
702
703 assert!(schema.one_of.is_some());
704 assert_eq!(schema.one_of.as_ref().unwrap().len(), 2);
705 }
706
707 #[test]
708 fn test_to_value() {
709 let schema = SchemaBuilder::object()
710 .property("query", SchemaBuilder::string())
711 .required(["query"])
712 .to_value();
713
714 assert!(schema.is_object());
715 let obj = schema.as_object().unwrap();
716 assert_eq!(obj.get("type").and_then(|v| v.as_str()), Some("object"));
717 }
718
719 #[test]
720 fn test_tool_input_schema() {
721 let schema = SchemaBuilder::object()
723 .title("SearchInput")
724 .description("Input parameters for the search tool")
725 .property(
726 "query",
727 SchemaBuilder::string()
728 .description("The search query")
729 .min_length(1),
730 )
731 .property(
732 "limit",
733 SchemaBuilder::integer()
734 .description("Maximum number of results")
735 .minimum(1)
736 .maximum(100)
737 .default_value(10),
738 )
739 .property(
740 "filters",
741 SchemaBuilder::array()
742 .items(SchemaBuilder::string())
743 .description("Optional filter tags"),
744 )
745 .required(["query"])
746 .additional_properties(false)
747 .build();
748
749 let value = schema.to_value();
750 assert!(value.is_object());
751
752 let obj = value.as_object().unwrap();
754 assert_eq!(obj.get("type").and_then(|v| v.as_str()), Some("object"));
755 assert_eq!(
756 obj.get("title").and_then(|v| v.as_str()),
757 Some("SearchInput")
758 );
759 }
760}