kalosm_sample/structured_parser/
schema.rs

1#[cfg(test)]
2use pretty_assertions::assert_eq;
3
4use std::{fmt::Display, fmt::Write};
5
6struct IndentationWriter<'a> {
7    indentation: usize,
8    writer: &'a mut dyn std::fmt::Write,
9}
10
11impl<'a> IndentationWriter<'a> {
12    fn new(indentation: usize, writer: &'a mut dyn std::fmt::Write) -> Self {
13        Self {
14            indentation,
15            writer,
16        }
17    }
18
19    fn with_indent<O>(&mut self, f: impl FnOnce(&mut Self) -> O) -> O {
20        self.indentation += 1;
21        let out = f(self);
22        self.indentation -= 1;
23        out
24    }
25}
26
27impl std::fmt::Write for IndentationWriter<'_> {
28    fn write_str(&mut self, s: &str) -> std::fmt::Result {
29        for char in s.chars() {
30            self.writer.write_char(char)?;
31            if char == '\n' {
32                for _ in 0..self.indentation {
33                    self.writer.write_char('\t')?;
34                }
35            }
36        }
37        Ok(())
38    }
39}
40
41/// A literal value in a schema
42#[derive(Debug, Clone)]
43pub enum SchemaLiteral {
44    /// A string
45    String(String),
46    /// A number
47    Number(f64),
48    /// A boolean
49    Boolean(bool),
50    /// The null value
51    Null,
52}
53
54impl Display for SchemaLiteral {
55    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56        match self {
57            SchemaLiteral::String(string) => write!(f, "\"{}\"", string),
58            SchemaLiteral::Number(number) => write!(f, "{}", number),
59            SchemaLiteral::Boolean(boolean) => write!(f, "{}", boolean),
60            SchemaLiteral::Null => write!(f, "null"),
61        }
62    }
63}
64
65/// The type of a schema
66#[derive(Debug, Clone)]
67pub enum SchemaType {
68    /// A string schema
69    String(StringSchema),
70    /// A floating point or integer schema
71    Number(NumberSchema),
72    /// An integer schema
73    Integer(IntegerSchema),
74    /// A boolean schema
75    Boolean(BooleanSchema),
76    /// An array schema
77    Array(ArraySchema),
78    /// An object schema
79    Object(JsonObjectSchema),
80    /// An enum schema
81    Enum(EnumSchema),
82    /// A schema that matches any of the composite schemas
83    AnyOf(AnyOfSchema),
84    /// A schema that matches one of the composite schemas
85    OneOf(OneOfSchema),
86    /// A constant schema
87    Const(ConstSchema),
88    /// An if-then schema
89    IfThen(IfThenSchema),
90    /// The null schema
91    Null,
92}
93
94impl SchemaType {
95    fn display_with_description(
96        &self,
97        f: &mut std::fmt::Formatter<'_>,
98        description: Option<&str>,
99    ) -> std::fmt::Result {
100        match self {
101            SchemaType::String(schema) => schema.display_with_description(f, description),
102            SchemaType::Number(schema) => schema.display_with_description(f, description),
103            SchemaType::Integer(schema) => schema.display_with_description(f, description),
104            SchemaType::Boolean(schema) => schema.display_with_description(f, description),
105            SchemaType::Array(schema) => schema.display_with_description(f, description),
106            SchemaType::Object(schema) => schema.display_with_description(f, description),
107            SchemaType::Enum(schema) => schema.display_with_description(f, description),
108            SchemaType::AnyOf(schema) => schema.display_with_description(f, description),
109            SchemaType::OneOf(schema) => schema.display_with_description(f, description),
110            SchemaType::Const(schema) => schema.display_with_description(f, description),
111            SchemaType::IfThen(schema) => schema.display_with_description(f, description),
112            SchemaType::Null => match description {
113                Some(description) => f.write_fmt(format_args!(
114                    "{{\n\t\"description\": \"{description}\",\n\t\"type\": \"null\"\n}}"
115                )),
116                None => f.write_str("{ \"type\": \"null\" }"),
117            },
118        }
119    }
120}
121
122impl Display for SchemaType {
123    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
124        self.display_with_description(f, None)
125    }
126}
127
128/// A schema for an conditional schema
129#[derive(Debug, Clone)]
130pub struct IfThenSchema {
131    if_schema: Box<SchemaType>,
132    then_schema: Box<SchemaType>,
133}
134
135impl IfThenSchema {
136    /// Create a new if-then schema
137    pub fn new(if_schema: SchemaType, then_schema: SchemaType) -> Self {
138        Self {
139            if_schema: Box::new(if_schema),
140            then_schema: Box::new(then_schema),
141        }
142    }
143
144    fn display_with_description(
145        &self,
146        f: &mut std::fmt::Formatter<'_>,
147        description: Option<&str>,
148    ) -> std::fmt::Result {
149        f.write_char('{')?;
150        {
151            let mut writer = IndentationWriter::new(1, f);
152            if let Some(description) = description {
153                write!(&mut writer, "\n\"description\": \"{description}\",")?;
154            }
155            writer.write_str("\n\"if\": ")?;
156            write!(&mut writer, "{}", self.if_schema)?;
157            writer.write_str(",\n\"then\": ")?;
158            write!(&mut writer, "{}", self.then_schema)?;
159        }
160        f.write_str("\n}")
161    }
162}
163
164impl Display for IfThenSchema {
165    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
166        self.display_with_description(f, None)
167    }
168}
169
170/// A schema that matches any of the composite schemas
171#[derive(Debug, Clone)]
172pub struct AnyOfSchema {
173    any_of: Vec<SchemaType>,
174}
175
176impl AnyOfSchema {
177    /// Create a new any of schema
178    pub fn new(any_of: impl IntoIterator<Item = SchemaType>) -> Self {
179        Self {
180            any_of: any_of.into_iter().collect(),
181        }
182    }
183
184    fn display_with_description(
185        &self,
186        f: &mut std::fmt::Formatter<'_>,
187        description: Option<&str>,
188    ) -> std::fmt::Result {
189        f.write_char('{')?;
190        {
191            let mut writer = IndentationWriter::new(1, f);
192            if let Some(description) = description {
193                write!(&mut writer, "\n\"description\": \"{description}\",")?;
194            }
195            writer.write_str("\n\"anyOf\": [")?;
196            if !self.any_of.is_empty() {
197                writer.with_indent(|writer| {
198                    for (i, schema) in self.any_of.iter().enumerate() {
199                        if i > 0 {
200                            writer.write_char(',')?;
201                        }
202                        write!(writer, "\n{}", schema)?;
203                    }
204                    Ok(())
205                })?;
206                writer.write_str("\n")?;
207            }
208            writer.write_str("]")?;
209        }
210        f.write_str("\n}")
211    }
212}
213
214impl Display for AnyOfSchema {
215    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
216        self.display_with_description(f, None)
217    }
218}
219
220/// A schema that matches one of the composite schemas
221#[derive(Debug, Clone)]
222pub struct OneOfSchema {
223    one_of: Vec<SchemaType>,
224}
225
226impl OneOfSchema {
227    /// Create a new one of schema
228    pub fn new(one_of: impl IntoIterator<Item = SchemaType>) -> Self {
229        Self {
230            one_of: one_of.into_iter().collect(),
231        }
232    }
233
234    fn display_with_description(
235        &self,
236        f: &mut std::fmt::Formatter<'_>,
237        description: Option<&str>,
238    ) -> std::fmt::Result {
239        f.write_char('{')?;
240        {
241            let mut writer = IndentationWriter::new(1, f);
242            if let Some(description) = description {
243                write!(&mut writer, "\n\"description\": \"{description}\",")?;
244            }
245            writer.write_str("\n\"oneOf\": [")?;
246            if !self.one_of.is_empty() {
247                writer.with_indent(|writer| {
248                    for (i, schema) in self.one_of.iter().enumerate() {
249                        if i > 0 {
250                            writer.write_char(',')?;
251                        }
252                        write!(writer, "\n{}", schema)?;
253                    }
254                    Ok(())
255                })?;
256                writer.write_str("\n")?;
257            }
258            writer.write_str("]")?;
259        }
260        f.write_str("\n}")
261    }
262}
263
264impl Display for OneOfSchema {
265    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
266        self.display_with_description(f, None)
267    }
268}
269
270/// A schema for a constant
271#[derive(Debug, Clone)]
272pub struct ConstSchema {
273    value: SchemaLiteral,
274}
275
276impl ConstSchema {
277    /// Create a new const schema
278    pub fn new(value: impl Into<SchemaLiteral>) -> Self {
279        Self {
280            value: value.into(),
281        }
282    }
283
284    fn display_with_description(
285        &self,
286        f: &mut std::fmt::Formatter<'_>,
287        description: Option<&str>,
288    ) -> std::fmt::Result {
289        if let Some(description) = description {
290            write!(
291                f,
292                "{{\n\t\"descripiton\": \"{description}\"\n\t\"const\": {}\n}}",
293                self.value
294            )
295        } else {
296            write!(f, "{{ \"const\": {} }}", self.value)
297        }
298    }
299}
300
301impl Display for ConstSchema {
302    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
303        self.display_with_description(f, None)
304    }
305}
306
307#[test]
308fn test_const_schema() {
309    let schema = ConstSchema::new(SchemaLiteral::String("hello".to_string()));
310
311    assert_eq!(schema.to_string(), "{ \"const\": \"hello\" }");
312}
313
314/// A schema for an enum
315#[derive(Debug, Clone)]
316pub struct EnumSchema {
317    variants: Vec<SchemaLiteral>,
318}
319
320impl EnumSchema {
321    /// Create a new enum schema
322    pub fn new(variants: impl IntoIterator<Item = SchemaLiteral>) -> Self {
323        Self {
324            variants: variants.into_iter().collect(),
325        }
326    }
327
328    fn display_with_description(
329        &self,
330        f: &mut std::fmt::Formatter<'_>,
331        description: Option<&str>,
332    ) -> std::fmt::Result {
333        f.write_char('{')?;
334        {
335            let mut writer = IndentationWriter::new(1, f);
336            if let Some(description) = description {
337                write!(&mut writer, "\n\"description\": \"{description}\",")?;
338            }
339            writer.write_str("\n\"enum\": [")?;
340            {
341                for (i, variant) in self.variants.iter().enumerate() {
342                    if i > 0 {
343                        writer.write_str(", ")?;
344                    }
345                    write!(writer, "{}", variant)?;
346                }
347            }
348            writer.write_str("]\n")?;
349        }
350        f.write_str(" }")
351    }
352}
353
354impl Display for EnumSchema {
355    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
356        self.display_with_description(f, None)
357    }
358}
359
360/// A schema for a string
361#[derive(Debug, Clone)]
362pub struct StringSchema {
363    /// The length that is valid for the string
364    length: Option<std::ops::RangeInclusive<usize>>,
365    /// The regex pattern that the string must match
366    pattern: Option<String>,
367}
368
369impl Schema for String {
370    fn schema() -> SchemaType {
371        SchemaType::String(StringSchema::new())
372    }
373}
374
375impl Default for StringSchema {
376    fn default() -> Self {
377        Self::new()
378    }
379}
380
381impl StringSchema {
382    /// Create a new string schema
383    pub fn new() -> Self {
384        Self {
385            length: None,
386            pattern: None,
387        }
388    }
389
390    /// Set the length range of the string
391    pub fn with_length(
392        mut self,
393        length: impl Into<Option<std::ops::RangeInclusive<usize>>>,
394    ) -> Self {
395        self.length = length.into();
396        self
397    }
398
399    /// Set a regex pattern the string must match
400    pub fn with_pattern(mut self, pattern: impl ToString) -> Self {
401        self.pattern = Some(pattern.to_string());
402        self
403    }
404
405    fn display_with_description(
406        &self,
407        f: &mut std::fmt::Formatter<'_>,
408        description: Option<&str>,
409    ) -> std::fmt::Result {
410        f.write_char('{')?;
411        {
412            let mut writer = IndentationWriter::new(1, f);
413            if let Some(description) = description {
414                write!(&mut writer, "\n\"description\": \"{description}\",")?;
415            }
416            writer.write_str("\n\"type\": \"string\"")?;
417            if let Some(length) = &self.length {
418                if *length.start() > 0 {
419                    writer.write_fmt(format_args!(",\n\"minLength\": {}", length.start()))?;
420                }
421                if *length.end() < usize::MAX {
422                    writer.write_fmt(format_args!(",\n\"maxLength\": {}", length.end()))?;
423                }
424            }
425            if let Some(pattern) = &self.pattern {
426                writer.write_fmt(format_args!(",\n\"pattern\": \"{}\"", pattern))?;
427            }
428        }
429        f.write_str("\n}")
430    }
431}
432
433impl Display for StringSchema {
434    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
435        self.display_with_description(f, None)
436    }
437}
438
439/// A schema for a number (floating point or integer)
440#[derive(Debug, Clone)]
441pub struct NumberSchema {
442    /// The range that the number must be in
443    range: Option<std::ops::RangeInclusive<f64>>,
444}
445
446macro_rules! impl_schema_for_number {
447    ($ty:ty) => {
448        impl Schema for $ty {
449            fn schema() -> SchemaType {
450                SchemaType::Number(NumberSchema::new())
451            }
452        }
453    };
454}
455
456impl_schema_for_number!(f64);
457impl_schema_for_number!(f32);
458
459impl Default for NumberSchema {
460    fn default() -> Self {
461        Self::new()
462    }
463}
464
465impl NumberSchema {
466    /// Create a new number schema
467    pub fn new() -> Self {
468        Self { range: None }
469    }
470
471    /// Set the range of the number
472    pub fn with_range(mut self, range: impl Into<Option<std::ops::RangeInclusive<f64>>>) -> Self {
473        self.range = range.into();
474        self
475    }
476
477    fn display_with_description(
478        &self,
479        f: &mut std::fmt::Formatter<'_>,
480        description: Option<&str>,
481    ) -> std::fmt::Result {
482        match &self.range {
483            Some(range) => {
484                f.write_char('{')?;
485                {
486                    let mut writer = IndentationWriter::new(1, f);
487                    if let Some(description) = description {
488                        write!(&mut writer, "\n\"description\": \"{description}\",")?;
489                    }
490                    writer.write_str("\n\"type\": \"number\",")?;
491                    writer.write_fmt(format_args!("\n\"minimum\": {},", range.start()))?;
492                    writer.write_fmt(format_args!("\n\"maximum\": {}", range.end()))?;
493                }
494                f.write_str("\n}")
495            }
496            None => f.write_str("{ \"type\": \"number\" }"),
497        }
498    }
499}
500
501impl Display for NumberSchema {
502    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
503        self.display_with_description(f, None)
504    }
505}
506
507#[test]
508fn test_number_schema() {
509    let schema = NumberSchema {
510        range: Some(0.0..=100.0),
511    };
512
513    assert_eq!(
514        schema.to_string(),
515        "{\n\t\"type\": \"number\",\n\t\"minimum\": 0,\n\t\"maximum\": 100\n}"
516    );
517
518    let schema = NumberSchema { range: None };
519
520    assert_eq!(schema.to_string(), "{ \"type\": \"number\" }");
521}
522
523/// A schema for an integer
524#[derive(Debug, Clone, Default)]
525
526pub struct IntegerSchema;
527
528impl IntegerSchema {
529    /// Create a new integer schema
530    pub fn new() -> Self {
531        Self
532    }
533}
534
535macro_rules! impl_schema_for_integer {
536    ($ty:ty) => {
537        impl Schema for $ty {
538            fn schema() -> SchemaType {
539                SchemaType::Number(NumberSchema::new())
540            }
541        }
542    };
543}
544
545impl_schema_for_integer!(i128);
546impl_schema_for_integer!(i64);
547impl_schema_for_integer!(i32);
548impl_schema_for_integer!(i16);
549impl_schema_for_integer!(i8);
550impl_schema_for_integer!(isize);
551
552impl_schema_for_integer!(u128);
553impl_schema_for_integer!(u64);
554impl_schema_for_integer!(u32);
555impl_schema_for_integer!(u16);
556impl_schema_for_integer!(u8);
557impl_schema_for_integer!(usize);
558
559impl IntegerSchema {
560    fn display_with_description(
561        &self,
562        f: &mut std::fmt::Formatter<'_>,
563        description: Option<&str>,
564    ) -> std::fmt::Result {
565        if let Some(description) = description {
566            write!(
567                f,
568                "{{\n\t\"description\": \"{description}\",\n\t\"type\": \"integer\"\n}}"
569            )
570        } else {
571            f.write_str("{ \"type\": \"integer\" }")
572        }
573    }
574}
575
576impl Display for IntegerSchema {
577    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
578        self.display_with_description(f, None)
579    }
580}
581
582#[test]
583fn test_integer_schema() {
584    let schema = IntegerSchema;
585
586    assert_eq!(schema.to_string(), "{ \"type\": \"integer\" }");
587}
588
589/// A schema for a boolean
590#[derive(Debug, Clone, Default)]
591pub struct BooleanSchema;
592
593impl BooleanSchema {
594    /// Create a new boolean schema
595    pub fn new() -> Self {
596        Self
597    }
598
599    fn display_with_description(
600        &self,
601        f: &mut std::fmt::Formatter<'_>,
602        description: Option<&str>,
603    ) -> std::fmt::Result {
604        if let Some(description) = description {
605            write!(
606                f,
607                "{{\n\t\"description\": \"{description}\",\n\t\"type\": \"boolean\"\n}}"
608            )
609        } else {
610            f.write_str("{ \"type\": \"boolean\" }")
611        }
612    }
613}
614
615impl Display for BooleanSchema {
616    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
617        self.display_with_description(f, None)
618    }
619}
620
621#[test]
622fn test_boolean_schema() {
623    let schema = BooleanSchema;
624
625    assert_eq!(schema.to_string(), "{ \"type\": \"boolean\" }");
626}
627
628/// A schema for an array
629#[derive(Debug, Clone)]
630pub struct ArraySchema {
631    items: Box<SchemaType>,
632    length: Option<std::ops::RangeInclusive<usize>>,
633}
634
635impl<T: Schema> Schema for Vec<T> {
636    fn schema() -> SchemaType {
637        SchemaType::Array(ArraySchema::new(T::schema()))
638    }
639}
640
641impl<const N: usize, T: Schema> Schema for [T; N] {
642    fn schema() -> SchemaType {
643        SchemaType::Array(ArraySchema::new(T::schema()).with_length(N..=N))
644    }
645}
646
647impl ArraySchema {
648    /// Create a new array schema
649    pub fn new(items: SchemaType) -> Self {
650        Self {
651            items: Box::new(items),
652            length: None,
653        }
654    }
655
656    /// Set the length range of the array
657    pub fn with_length(
658        mut self,
659        length: impl Into<Option<std::ops::RangeInclusive<usize>>>,
660    ) -> Self {
661        self.length = length.into();
662        self
663    }
664
665    fn display_with_description(
666        &self,
667        f: &mut std::fmt::Formatter<'_>,
668        description: Option<&str>,
669    ) -> std::fmt::Result {
670        f.write_char('{')?;
671        {
672            let mut writer = IndentationWriter::new(1, f);
673            if let Some(description) = description {
674                write!(&mut writer, "\n\"description\": \"{description}\",")?;
675            }
676            writer.write_str("\n\"type\": \"array\"")?;
677            writer.write_str(",\n\"items\": ")?;
678            write!(&mut writer, "{}", self.items)?;
679            if let Some(length) = &self.length {
680                if *length.start() > 0 {
681                    writer.write_str(",\n\"minItems\": ")?;
682                    write!(&mut writer, "{}", length.start())?;
683                }
684                if *length.end() < usize::MAX {
685                    writer.write_str(",\n\"maxItems\": ")?;
686                    write!(&mut writer, "{}", length.end())?;
687                }
688            }
689            writer.write_str(",\n\"unevaluatedItems\": false")?;
690        }
691        f.write_str("\n}")
692    }
693}
694
695impl Display for ArraySchema {
696    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
697        self.display_with_description(f, None)
698    }
699}
700
701#[test]
702fn test_array_schema() {
703    let schema = ArraySchema {
704        items: Box::new(SchemaType::String(StringSchema {
705            length: Some(1..=10),
706            pattern: None,
707        })),
708        length: Some(0..=10),
709    };
710
711    assert_eq!(schema.to_string(), "{\n\t\"type\": \"array\",\n\t\"items\": {\n\t\t\"type\": \"string\",\n\t\t\"minLength\": 1,\n\t\t\"maxLength\": 10\n\t},\n\t\"maxItems\": 10,\n\t\"unevaluatedItems\": false\n}");
712
713    let schema = ArraySchema {
714        items: Box::new(SchemaType::String(StringSchema {
715            length: None,
716            pattern: None,
717        })),
718        length: Some(1..=usize::MAX),
719    };
720
721    assert_eq!(schema.to_string(), "{\n\t\"type\": \"array\",\n\t\"items\": {\n\t\t\"type\": \"string\"\n\t},\n\t\"minItems\": 1,\n\t\"unevaluatedItems\": false\n}");
722    let schema = ArraySchema {
723        items: Box::new(SchemaType::String(StringSchema {
724            length: None,
725            pattern: None,
726        })),
727        length: None,
728    };
729
730    assert_eq!(schema.to_string(), "{\n\t\"type\": \"array\",\n\t\"items\": {\n\t\t\"type\": \"string\"\n\t},\n\t\"unevaluatedItems\": false\n}");
731}
732
733/// A schema for an object
734#[derive(Debug, Clone)]
735pub struct JsonObjectSchema {
736    title: Option<String>,
737    description: Option<&'static str>,
738    properties: Vec<JsonPropertySchema>,
739}
740
741impl JsonObjectSchema {
742    /// Create a new object schema
743    pub fn new(properties: impl IntoIterator<Item = JsonPropertySchema>) -> Self {
744        Self {
745            title: None,
746            description: None,
747            properties: properties.into_iter().collect(),
748        }
749    }
750
751    /// Set the title of the object
752    pub fn with_title(mut self, title: impl ToString) -> Self {
753        self.title = Some(title.to_string());
754        self
755    }
756
757    /// Set the description of the object
758    pub fn with_description(mut self, description: impl Into<Option<&'static str>>) -> Self {
759        self.description = description.into();
760        self
761    }
762
763    fn display_with_description(
764        &self,
765        f: &mut std::fmt::Formatter<'_>,
766        description: Option<&str>,
767    ) -> std::fmt::Result {
768        f.write_char('{')?;
769        {
770            let mut writer = IndentationWriter::new(1, f);
771            writer.write_char('\n')?;
772            if let Some(description) = description {
773                writeln!(&mut writer, "\"description\": \"{description}\",")?;
774            }
775            if let Some(title) = &self.title {
776                writer.write_str("\"title\": \"")?;
777                writer.write_str(title)?;
778                writer.write_str("\",\n")?;
779            }
780            if let Some(description) = &self.description {
781                writer.write_fmt(format_args!("\"description\": \"{}\",\n", description))?;
782            }
783            writer.write_str("\"type\": \"object\",\n")?;
784            writer.write_str("\"properties\": {")?;
785            if !self.properties.is_empty() {
786                writer.with_indent(|writer| {
787                    for (i, property) in self.properties.iter().enumerate() {
788                        if i > 0 {
789                            writer.write_char(',')?;
790                        }
791                        write!(writer, "\n{}", property)?;
792                    }
793                    Ok(())
794                })?;
795                writer.write_str("\n")?;
796            }
797            writer.write_str("}")?;
798            let required = self
799                .properties
800                .iter()
801                .filter_map(|property| (property.required).then_some(property.name.clone()))
802                .collect::<Vec<_>>();
803            if !required.is_empty() {
804                writer.write_str(",\n\"required\": [")?;
805                {
806                    for (i, required) in required.iter().enumerate() {
807                        if i > 0 {
808                            writer.write_str(", ")?;
809                        }
810                        write!(writer, "\"{}\"", required)?;
811                    }
812                }
813                writer.write_str("]")?;
814            }
815            writer.write_str(",\n\"additionalProperties\": false")?;
816        }
817        f.write_str("\n}")
818    }
819}
820
821impl Display for JsonObjectSchema {
822    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
823        self.display_with_description(f, None)
824    }
825}
826
827#[test]
828fn test_object_schema() {
829    let schema = JsonObjectSchema {
830        title: Some("Person".to_string()),
831        description: Some("A person"),
832        properties: vec![
833            JsonPropertySchema {
834                name: "name".to_string(),
835                description: None,
836                required: true,
837                ty: SchemaType::String(StringSchema {
838                    length: Some(1..=10),
839                    pattern: None,
840                }),
841            },
842            JsonPropertySchema {
843                name: "age".to_string(),
844                description: None,
845                required: true,
846                ty: SchemaType::Number(NumberSchema {
847                    range: Some(0.0..=100.0),
848                }),
849            },
850            JsonPropertySchema {
851                name: "height".to_string(),
852                description: None,
853                required: false,
854                ty: SchemaType::Number(NumberSchema {
855                    range: Some(0.0..=500.0),
856                }),
857            },
858        ],
859    };
860
861    assert_eq!(schema.to_string(), "{\n\t\"title\": \"Person\",\n\t\"description\": \"A person\",\n\t\"type\": \"object\",\n\t\"properties\": {\n\t\t\"name\": {\n\t\t\t\"type\": \"string\",\n\t\t\t\"minLength\": 1,\n\t\t\t\"maxLength\": 10\n\t\t},\n\t\t\"age\": {\n\t\t\t\"type\": \"number\",\n\t\t\t\"minimum\": 0,\n\t\t\t\"maximum\": 100\n\t\t},\n\t\t\"height\": {\n\t\t\t\"type\": \"number\",\n\t\t\t\"minimum\": 0,\n\t\t\t\"maximum\": 500\n\t\t}\n\t},\n\t\"required\": [\"name\", \"age\"],\n\t\"additionalProperties\": false\n}");
862}
863
864/// A schema for a property of an object
865#[derive(Debug, Clone)]
866pub struct JsonPropertySchema {
867    name: String,
868    description: Option<&'static str>,
869    required: bool,
870    ty: SchemaType,
871}
872
873impl JsonPropertySchema {
874    /// Create a new property schema
875    pub fn new(name: impl ToString, ty: SchemaType) -> Self {
876        Self {
877            name: name.to_string(),
878            description: None,
879            required: false,
880            ty,
881        }
882    }
883
884    /// Set the description of the property
885    pub fn with_description(mut self, description: impl Into<Option<&'static str>>) -> Self {
886        self.description = description.into();
887        self
888    }
889
890    /// Set whether the property is required
891    pub fn with_required(mut self, required: bool) -> Self {
892        self.required = required;
893        self
894    }
895}
896
897impl Display for JsonPropertySchema {
898    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
899        f.write_fmt(format_args!("\"{}\": ", self.name))?;
900        self.ty.display_with_description(f, self.description)
901    }
902}
903
904/// A description of the format of a type
905pub trait Schema {
906    /// Get the schema for the type
907    fn schema() -> SchemaType;
908}
909
910impl<T: Schema> Schema for Option<T> {
911    fn schema() -> SchemaType {
912        SchemaType::OneOf(OneOfSchema::new([SchemaType::Null, T::schema()]))
913    }
914}
915
916impl<T: Schema> Schema for Box<T> {
917    fn schema() -> SchemaType {
918        T::schema()
919    }
920}