Skip to main content

fastapi_openapi/
schema.rs

1//! JSON Schema types for OpenAPI 3.1.
2
3use serde::{Deserialize, Serialize};
4use std::cell::RefCell;
5use std::collections::HashMap;
6
7/// JSON Schema representation.
8#[derive(Debug, Clone, Serialize, Deserialize)]
9#[serde(untagged)]
10pub enum Schema {
11    /// Boolean schema (true = any, false = none).
12    Boolean(bool),
13    /// Reference to another schema.
14    Ref(RefSchema),
15    /// Object schema.
16    Object(ObjectSchema),
17    /// Array schema.
18    Array(ArraySchema),
19    /// Primitive type schema.
20    Primitive(PrimitiveSchema),
21    /// Enum/union schema (oneOf, anyOf, allOf).
22    Enum(EnumSchema),
23}
24
25impl Schema {
26    /// Create a string schema.
27    pub fn string() -> Self {
28        Schema::Primitive(PrimitiveSchema::string())
29    }
30
31    /// Create an integer schema with optional format.
32    pub fn integer(format: Option<&str>) -> Self {
33        Schema::Primitive(PrimitiveSchema::integer(format))
34    }
35
36    /// Create a number schema with optional format.
37    pub fn number(format: Option<&str>) -> Self {
38        Schema::Primitive(PrimitiveSchema::number(format))
39    }
40
41    /// Create a boolean schema.
42    pub fn boolean() -> Self {
43        Schema::Primitive(PrimitiveSchema::boolean())
44    }
45
46    /// Create a reference schema.
47    pub fn reference(name: &str) -> Self {
48        Schema::Ref(RefSchema {
49            reference: format!("#/components/schemas/{name}"),
50        })
51    }
52
53    /// Create an array schema.
54    pub fn array(items: Schema) -> Self {
55        Schema::Array(ArraySchema {
56            items: Box::new(items),
57            min_items: None,
58            max_items: None,
59        })
60    }
61
62    /// Create an object schema with the given properties.
63    pub fn object(properties: HashMap<String, Schema>, required: Vec<String>) -> Self {
64        Schema::Object(ObjectSchema {
65            title: None,
66            description: None,
67            properties,
68            required,
69            additional_properties: None,
70            example: None,
71        })
72    }
73
74    /// Set nullable on this schema (if primitive).
75    #[must_use]
76    pub fn nullable(mut self) -> Self {
77        if let Schema::Primitive(ref mut p) = self {
78            p.nullable = true;
79        }
80        self
81    }
82
83    /// Set title on this schema (if object).
84    #[must_use]
85    pub fn with_title(mut self, title: impl Into<String>) -> Self {
86        if let Schema::Object(ref mut o) = self {
87            o.title = Some(title.into());
88        }
89        self
90    }
91
92    /// Set description on this schema (if object).
93    #[must_use]
94    pub fn with_description(mut self, description: impl Into<String>) -> Self {
95        if let Schema::Object(ref mut o) = self {
96            o.description = Some(description.into());
97        }
98        self
99    }
100
101    /// Create a oneOf schema (discriminated union - exactly one must match).
102    pub fn one_of(schemas: Vec<Schema>) -> Self {
103        Schema::Enum(EnumSchema {
104            one_of: schemas,
105            ..Default::default()
106        })
107    }
108
109    /// Create an anyOf schema (untagged union - at least one must match).
110    pub fn any_of(schemas: Vec<Schema>) -> Self {
111        Schema::Enum(EnumSchema {
112            any_of: schemas,
113            ..Default::default()
114        })
115    }
116
117    /// Create an allOf schema (intersection - all must match).
118    pub fn all_of(schemas: Vec<Schema>) -> Self {
119        Schema::Enum(EnumSchema {
120            all_of: schemas,
121            ..Default::default()
122        })
123    }
124
125    /// Create a string enum schema (for unit variants only).
126    pub fn string_enum(values: Vec<String>) -> Self {
127        Schema::Primitive(PrimitiveSchema {
128            schema_type: SchemaType::String,
129            format: None,
130            nullable: false,
131            minimum: None,
132            maximum: None,
133            exclusive_minimum: None,
134            exclusive_maximum: None,
135            min_length: None,
136            max_length: None,
137            pattern: None,
138            enum_values: Some(values),
139            example: None,
140        })
141    }
142
143    /// Create a oneOf schema with discriminator.
144    pub fn one_of_with_discriminator(
145        schemas: Vec<Schema>,
146        property_name: impl Into<String>,
147        mapping: HashMap<String, String>,
148    ) -> Self {
149        Schema::Enum(EnumSchema {
150            one_of: schemas,
151            discriminator: Some(Discriminator {
152                property_name: property_name.into(),
153                mapping,
154            }),
155            ..Default::default()
156        })
157    }
158}
159
160/// Schema reference.
161#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct RefSchema {
163    /// Reference path (e.g., "#/components/schemas/Item").
164    #[serde(rename = "$ref")]
165    pub reference: String,
166}
167
168/// Object schema.
169#[derive(Debug, Clone, Default, Serialize, Deserialize)]
170pub struct ObjectSchema {
171    /// Schema title.
172    #[serde(default, skip_serializing_if = "Option::is_none")]
173    pub title: Option<String>,
174    /// Schema description.
175    #[serde(default, skip_serializing_if = "Option::is_none")]
176    pub description: Option<String>,
177    /// Object properties.
178    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
179    pub properties: HashMap<String, Schema>,
180    /// Required property names.
181    #[serde(default, skip_serializing_if = "Vec::is_empty")]
182    pub required: Vec<String>,
183    /// Additional properties schema.
184    #[serde(default, skip_serializing_if = "Option::is_none")]
185    pub additional_properties: Option<Box<Schema>>,
186    /// Example value for this schema.
187    #[serde(default, skip_serializing_if = "Option::is_none")]
188    pub example: Option<serde_json::Value>,
189}
190
191/// Array schema.
192#[derive(Debug, Clone, Serialize, Deserialize)]
193pub struct ArraySchema {
194    /// Item schema.
195    pub items: Box<Schema>,
196    /// Minimum items.
197    #[serde(default, skip_serializing_if = "Option::is_none")]
198    pub min_items: Option<usize>,
199    /// Maximum items.
200    #[serde(default, skip_serializing_if = "Option::is_none")]
201    pub max_items: Option<usize>,
202}
203
204/// Enum/union schema supporting oneOf, anyOf, allOf, and string enums.
205///
206/// This is used for Rust enums which map to various OpenAPI constructs:
207/// - Unit variants only → string enum with `enum` keyword
208/// - Mixed variants → `oneOf` with discriminated union
209/// - Untagged enums → `anyOf`
210#[derive(Debug, Clone, Default, Serialize, Deserialize)]
211pub struct EnumSchema {
212    /// Schema title.
213    #[serde(default, skip_serializing_if = "Option::is_none")]
214    pub title: Option<String>,
215    /// Schema description.
216    #[serde(default, skip_serializing_if = "Option::is_none")]
217    pub description: Option<String>,
218    /// oneOf schemas (discriminated union - exactly one must match).
219    #[serde(default, skip_serializing_if = "Vec::is_empty", rename = "oneOf")]
220    pub one_of: Vec<Schema>,
221    /// anyOf schemas (untagged union - at least one must match).
222    #[serde(default, skip_serializing_if = "Vec::is_empty", rename = "anyOf")]
223    pub any_of: Vec<Schema>,
224    /// allOf schemas (intersection - all must match).
225    #[serde(default, skip_serializing_if = "Vec::is_empty", rename = "allOf")]
226    pub all_of: Vec<Schema>,
227    /// Discriminator for oneOf schemas.
228    #[serde(default, skip_serializing_if = "Option::is_none")]
229    pub discriminator: Option<Discriminator>,
230}
231
232/// Discriminator for oneOf schemas (OpenAPI 3.1).
233#[derive(Debug, Clone, Serialize, Deserialize)]
234pub struct Discriminator {
235    /// Property name that discriminates between variants.
236    #[serde(rename = "propertyName")]
237    pub property_name: String,
238    /// Mapping from discriminator values to schema references.
239    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
240    pub mapping: HashMap<String, String>,
241}
242
243/// Schema for a constant value (used for unit enum variants).
244#[derive(Debug, Clone, Serialize, Deserialize)]
245pub struct ConstSchema {
246    /// The constant value.
247    #[serde(rename = "const")]
248    pub const_value: serde_json::Value,
249}
250
251/// Schema for string enums (unit variants only).
252#[derive(Debug, Clone, Serialize, Deserialize)]
253pub struct StringEnumSchema {
254    /// Schema type (always "string").
255    #[serde(rename = "type")]
256    pub schema_type: SchemaType,
257    /// Allowed enum values.
258    #[serde(rename = "enum")]
259    pub enum_values: Vec<String>,
260}
261
262/// Primitive type schema.
263#[derive(Debug, Clone, Serialize, Deserialize)]
264pub struct PrimitiveSchema {
265    /// JSON Schema type.
266    #[serde(rename = "type")]
267    pub schema_type: SchemaType,
268    /// Format hint.
269    #[serde(default, skip_serializing_if = "Option::is_none")]
270    pub format: Option<String>,
271    /// Nullable flag (OpenAPI 3.1).
272    #[serde(default, skip_serializing_if = "is_false")]
273    pub nullable: bool,
274    /// Minimum value constraint (>= for numbers).
275    #[serde(default, skip_serializing_if = "Option::is_none")]
276    pub minimum: Option<i64>,
277    /// Maximum value constraint (<= for numbers).
278    #[serde(default, skip_serializing_if = "Option::is_none")]
279    pub maximum: Option<i64>,
280    /// Exclusive minimum value constraint (> for numbers).
281    #[serde(
282        default,
283        skip_serializing_if = "Option::is_none",
284        rename = "exclusiveMinimum"
285    )]
286    pub exclusive_minimum: Option<i64>,
287    /// Exclusive maximum value constraint (< for numbers).
288    #[serde(
289        default,
290        skip_serializing_if = "Option::is_none",
291        rename = "exclusiveMaximum"
292    )]
293    pub exclusive_maximum: Option<i64>,
294    /// Minimum length constraint (for strings).
295    #[serde(default, skip_serializing_if = "Option::is_none", rename = "minLength")]
296    pub min_length: Option<usize>,
297    /// Maximum length constraint (for strings).
298    #[serde(default, skip_serializing_if = "Option::is_none", rename = "maxLength")]
299    pub max_length: Option<usize>,
300    /// Pattern constraint (regex for strings).
301    #[serde(default, skip_serializing_if = "Option::is_none")]
302    pub pattern: Option<String>,
303    /// Enum values (for string enums with unit variants).
304    #[serde(default, skip_serializing_if = "Option::is_none", rename = "enum")]
305    pub enum_values: Option<Vec<String>>,
306    /// Example value for this schema.
307    #[serde(default, skip_serializing_if = "Option::is_none")]
308    pub example: Option<serde_json::Value>,
309}
310
311impl PrimitiveSchema {
312    /// Create a string schema.
313    pub fn string() -> Self {
314        Self {
315            schema_type: SchemaType::String,
316            format: None,
317            nullable: false,
318            minimum: None,
319            maximum: None,
320            exclusive_minimum: None,
321            exclusive_maximum: None,
322            min_length: None,
323            max_length: None,
324            pattern: None,
325            enum_values: None,
326            example: None,
327        }
328    }
329
330    /// Create an integer schema with optional format.
331    pub fn integer(format: Option<&str>) -> Self {
332        Self {
333            schema_type: SchemaType::Integer,
334            format: format.map(String::from),
335            nullable: false,
336            minimum: None,
337            maximum: None,
338            exclusive_minimum: None,
339            exclusive_maximum: None,
340            min_length: None,
341            max_length: None,
342            pattern: None,
343            enum_values: None,
344            example: None,
345        }
346    }
347
348    /// Create an unsigned integer schema with optional format.
349    pub fn unsigned_integer(format: Option<&str>) -> Self {
350        Self {
351            schema_type: SchemaType::Integer,
352            format: format.map(String::from),
353            nullable: false,
354            minimum: Some(0),
355            maximum: None,
356            exclusive_minimum: None,
357            exclusive_maximum: None,
358            min_length: None,
359            max_length: None,
360            pattern: None,
361            enum_values: None,
362            example: None,
363        }
364    }
365
366    /// Create a number schema with optional format.
367    pub fn number(format: Option<&str>) -> Self {
368        Self {
369            schema_type: SchemaType::Number,
370            format: format.map(String::from),
371            nullable: false,
372            minimum: None,
373            maximum: None,
374            exclusive_minimum: None,
375            exclusive_maximum: None,
376            min_length: None,
377            max_length: None,
378            pattern: None,
379            enum_values: None,
380            example: None,
381        }
382    }
383
384    /// Create a boolean schema.
385    pub fn boolean() -> Self {
386        Self {
387            schema_type: SchemaType::Boolean,
388            format: None,
389            nullable: false,
390            minimum: None,
391            maximum: None,
392            exclusive_minimum: None,
393            exclusive_maximum: None,
394            min_length: None,
395            max_length: None,
396            pattern: None,
397            enum_values: None,
398            example: None,
399        }
400    }
401
402    /// Set minimum value constraint (>=).
403    #[must_use]
404    pub fn with_minimum(mut self, value: i64) -> Self {
405        self.minimum = Some(value);
406        self
407    }
408
409    /// Set maximum value constraint (<=).
410    #[must_use]
411    pub fn with_maximum(mut self, value: i64) -> Self {
412        self.maximum = Some(value);
413        self
414    }
415
416    /// Set exclusive minimum value constraint (>).
417    #[must_use]
418    pub fn with_exclusive_minimum(mut self, value: i64) -> Self {
419        self.exclusive_minimum = Some(value);
420        self
421    }
422
423    /// Set exclusive maximum value constraint (<).
424    #[must_use]
425    pub fn with_exclusive_maximum(mut self, value: i64) -> Self {
426        self.exclusive_maximum = Some(value);
427        self
428    }
429
430    /// Set minimum length constraint (for strings).
431    #[must_use]
432    pub fn with_min_length(mut self, len: usize) -> Self {
433        self.min_length = Some(len);
434        self
435    }
436
437    /// Set maximum length constraint (for strings).
438    #[must_use]
439    pub fn with_max_length(mut self, len: usize) -> Self {
440        self.max_length = Some(len);
441        self
442    }
443
444    /// Set pattern constraint (regex for strings).
445    #[must_use]
446    pub fn with_pattern(mut self, pattern: impl Into<String>) -> Self {
447        self.pattern = Some(pattern.into());
448        self
449    }
450}
451
452#[allow(clippy::trivially_copy_pass_by_ref)]
453fn is_false(b: &bool) -> bool {
454    !*b
455}
456
457/// JSON Schema primitive types.
458#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
459#[serde(rename_all = "lowercase")]
460pub enum SchemaType {
461    /// String type.
462    String,
463    /// Number type (float).
464    Number,
465    /// Integer type.
466    Integer,
467    /// Boolean type.
468    Boolean,
469    /// Null type.
470    Null,
471}
472
473/// Trait for types that can generate JSON Schema.
474pub trait JsonSchema {
475    /// Generate the JSON Schema for this type.
476    fn schema() -> Schema;
477
478    /// Get the schema name for use in `#/components/schemas/`.
479    #[must_use]
480    fn schema_name() -> Option<&'static str> {
481        None
482    }
483
484    /// Get the schema for this type, registering it with the given registry.
485    ///
486    /// If the type has a schema name, this registers the full schema definition
487    /// and returns a `$ref` reference. Otherwise, returns the inline schema.
488    fn schema_with_registry(registry: &SchemaRegistry) -> Schema
489    where
490        Self: Sized,
491    {
492        if let Some(name) = Self::schema_name() {
493            registry.get_or_register::<Self>(name)
494        } else {
495            Self::schema()
496        }
497    }
498}
499
500// ============================================================================
501// Schema Registry for $ref support
502// ============================================================================
503
504/// Registry for collecting and deduplicating JSON schemas.
505///
506/// The `SchemaRegistry` tracks schemas by name and returns `$ref` references
507/// when a schema is already registered. This prevents schema duplication
508/// in OpenAPI documents.
509///
510/// # Example
511///
512/// ```ignore
513/// use fastapi_openapi::{SchemaRegistry, JsonSchema, Schema};
514///
515/// #[derive(JsonSchema)]
516/// struct User {
517///     id: i64,
518///     name: String,
519/// }
520///
521/// let registry = SchemaRegistry::new();
522///
523/// // First access registers the schema and returns a $ref
524/// let schema1 = User::schema_with_registry(&registry);
525/// assert!(matches!(schema1, Schema::Ref(_)));
526///
527/// // Second access returns the same $ref without re-registering
528/// let schema2 = User::schema_with_registry(&registry);
529/// assert!(matches!(schema2, Schema::Ref(_)));
530///
531/// // Export all collected schemas for components/schemas
532/// let schemas = registry.into_schemas();
533/// assert!(schemas.contains_key("User"));
534/// ```
535#[derive(Debug, Default)]
536pub struct SchemaRegistry {
537    /// Registered schemas by name.
538    schemas: RefCell<HashMap<String, Schema>>,
539}
540
541impl SchemaRegistry {
542    /// Create a new empty registry.
543    #[must_use]
544    pub fn new() -> Self {
545        Self {
546            schemas: RefCell::new(HashMap::new()),
547        }
548    }
549
550    /// Get or register a schema by name.
551    ///
552    /// If the schema is already registered, returns a `$ref` reference.
553    /// Otherwise, generates the schema, registers it, and returns a `$ref`.
554    pub fn get_or_register<T: JsonSchema>(&self, name: &str) -> Schema {
555        let mut schemas = self.schemas.borrow_mut();
556
557        if !schemas.contains_key(name) {
558            // Register the full schema definition
559            schemas.insert(name.to_string(), T::schema());
560        }
561
562        // Return a $ref
563        Schema::reference(name)
564    }
565
566    /// Register a schema directly by name.
567    ///
568    /// Returns a `$ref` to the registered schema.
569    pub fn register(&self, name: impl Into<String>, schema: Schema) -> Schema {
570        let name = name.into();
571        self.schemas.borrow_mut().insert(name.clone(), schema);
572        Schema::reference(&name)
573    }
574
575    /// Check if a schema with the given name is already registered.
576    #[must_use]
577    pub fn contains(&self, name: &str) -> bool {
578        self.schemas.borrow().contains_key(name)
579    }
580
581    /// Get the number of registered schemas.
582    #[must_use]
583    pub fn len(&self) -> usize {
584        self.schemas.borrow().len()
585    }
586
587    /// Check if the registry is empty.
588    #[must_use]
589    pub fn is_empty(&self) -> bool {
590        self.schemas.borrow().is_empty()
591    }
592
593    /// Consume the registry and return all collected schemas.
594    ///
595    /// The returned map is suitable for use in `components.schemas`.
596    #[must_use]
597    pub fn into_schemas(self) -> HashMap<String, Schema> {
598        self.schemas.into_inner()
599    }
600
601    /// Get a clone of all registered schemas without consuming the registry.
602    #[must_use]
603    pub fn schemas(&self) -> HashMap<String, Schema> {
604        self.schemas.borrow().clone()
605    }
606
607    /// Merge another registry's schemas into this one.
608    ///
609    /// If a schema with the same name exists in both, the existing one is kept.
610    pub fn merge(&self, other: &SchemaRegistry) {
611        let mut schemas = self.schemas.borrow_mut();
612        for (name, schema) in other.schemas.borrow().iter() {
613            schemas
614                .entry(name.clone())
615                .or_insert_with(|| schema.clone());
616        }
617    }
618}
619
620impl Clone for SchemaRegistry {
621    fn clone(&self) -> Self {
622        Self {
623            schemas: RefCell::new(self.schemas.borrow().clone()),
624        }
625    }
626}
627
628// ============================================================================
629// Primitive type implementations
630// ============================================================================
631
632// Implement for primitive types
633impl JsonSchema for String {
634    fn schema() -> Schema {
635        Schema::Primitive(PrimitiveSchema::string())
636    }
637}
638
639impl JsonSchema for &str {
640    fn schema() -> Schema {
641        Schema::Primitive(PrimitiveSchema::string())
642    }
643}
644
645impl JsonSchema for bool {
646    fn schema() -> Schema {
647        Schema::Primitive(PrimitiveSchema::boolean())
648    }
649}
650
651// Signed integers
652impl JsonSchema for i8 {
653    fn schema() -> Schema {
654        Schema::Primitive(PrimitiveSchema::integer(Some("int8")))
655    }
656}
657
658impl JsonSchema for i16 {
659    fn schema() -> Schema {
660        Schema::Primitive(PrimitiveSchema::integer(Some("int16")))
661    }
662}
663
664impl JsonSchema for i32 {
665    fn schema() -> Schema {
666        Schema::Primitive(PrimitiveSchema::integer(Some("int32")))
667    }
668}
669
670impl JsonSchema for i64 {
671    fn schema() -> Schema {
672        Schema::Primitive(PrimitiveSchema::integer(Some("int64")))
673    }
674}
675
676impl JsonSchema for i128 {
677    fn schema() -> Schema {
678        // i128 doesn't have a standard OpenAPI format, use integer without format
679        Schema::Primitive(PrimitiveSchema::integer(None))
680    }
681}
682
683impl JsonSchema for isize {
684    fn schema() -> Schema {
685        // isize is typically 64-bit on modern systems
686        Schema::Primitive(PrimitiveSchema::integer(Some("int64")))
687    }
688}
689
690// Unsigned integers (with minimum: 0)
691impl JsonSchema for u8 {
692    fn schema() -> Schema {
693        Schema::Primitive(PrimitiveSchema::unsigned_integer(Some("uint8")))
694    }
695}
696
697impl JsonSchema for u16 {
698    fn schema() -> Schema {
699        Schema::Primitive(PrimitiveSchema::unsigned_integer(Some("uint16")))
700    }
701}
702
703impl JsonSchema for u32 {
704    fn schema() -> Schema {
705        Schema::Primitive(PrimitiveSchema::unsigned_integer(Some("uint32")))
706    }
707}
708
709impl JsonSchema for u64 {
710    fn schema() -> Schema {
711        Schema::Primitive(PrimitiveSchema::unsigned_integer(Some("uint64")))
712    }
713}
714
715impl JsonSchema for u128 {
716    fn schema() -> Schema {
717        // u128 doesn't have a standard OpenAPI format
718        Schema::Primitive(PrimitiveSchema::unsigned_integer(None))
719    }
720}
721
722impl JsonSchema for usize {
723    fn schema() -> Schema {
724        // usize is typically 64-bit on modern systems
725        Schema::Primitive(PrimitiveSchema::unsigned_integer(Some("uint64")))
726    }
727}
728
729// Floating point
730impl JsonSchema for f32 {
731    fn schema() -> Schema {
732        Schema::Primitive(PrimitiveSchema::number(Some("float")))
733    }
734}
735
736impl JsonSchema for f64 {
737    fn schema() -> Schema {
738        Schema::Primitive(PrimitiveSchema::number(Some("double")))
739    }
740}
741
742// NonZero types
743impl JsonSchema for std::num::NonZeroI8 {
744    fn schema() -> Schema {
745        Schema::Primitive(PrimitiveSchema::integer(Some("int8")))
746    }
747}
748
749impl JsonSchema for std::num::NonZeroI16 {
750    fn schema() -> Schema {
751        Schema::Primitive(PrimitiveSchema::integer(Some("int16")))
752    }
753}
754
755impl JsonSchema for std::num::NonZeroI32 {
756    fn schema() -> Schema {
757        Schema::Primitive(PrimitiveSchema::integer(Some("int32")))
758    }
759}
760
761impl JsonSchema for std::num::NonZeroI64 {
762    fn schema() -> Schema {
763        Schema::Primitive(PrimitiveSchema::integer(Some("int64")))
764    }
765}
766
767impl JsonSchema for std::num::NonZeroI128 {
768    fn schema() -> Schema {
769        Schema::Primitive(PrimitiveSchema::integer(None))
770    }
771}
772
773impl JsonSchema for std::num::NonZeroIsize {
774    fn schema() -> Schema {
775        Schema::Primitive(PrimitiveSchema::integer(Some("int64")))
776    }
777}
778
779impl JsonSchema for std::num::NonZeroU8 {
780    fn schema() -> Schema {
781        let mut schema = PrimitiveSchema::unsigned_integer(Some("uint8"));
782        schema.minimum = Some(1); // NonZero must be >= 1
783        Schema::Primitive(schema)
784    }
785}
786
787impl JsonSchema for std::num::NonZeroU16 {
788    fn schema() -> Schema {
789        let mut schema = PrimitiveSchema::unsigned_integer(Some("uint16"));
790        schema.minimum = Some(1);
791        Schema::Primitive(schema)
792    }
793}
794
795impl JsonSchema for std::num::NonZeroU32 {
796    fn schema() -> Schema {
797        let mut schema = PrimitiveSchema::unsigned_integer(Some("uint32"));
798        schema.minimum = Some(1);
799        Schema::Primitive(schema)
800    }
801}
802
803impl JsonSchema for std::num::NonZeroU64 {
804    fn schema() -> Schema {
805        let mut schema = PrimitiveSchema::unsigned_integer(Some("uint64"));
806        schema.minimum = Some(1);
807        Schema::Primitive(schema)
808    }
809}
810
811impl JsonSchema for std::num::NonZeroU128 {
812    fn schema() -> Schema {
813        let mut schema = PrimitiveSchema::unsigned_integer(None);
814        schema.minimum = Some(1);
815        Schema::Primitive(schema)
816    }
817}
818
819impl JsonSchema for std::num::NonZeroUsize {
820    fn schema() -> Schema {
821        let mut schema = PrimitiveSchema::unsigned_integer(Some("uint64"));
822        schema.minimum = Some(1);
823        Schema::Primitive(schema)
824    }
825}
826
827impl<T: JsonSchema> JsonSchema for Option<T> {
828    fn schema() -> Schema {
829        match T::schema() {
830            Schema::Primitive(mut p) => {
831                p.nullable = true;
832                Schema::Primitive(p)
833            }
834            other => other,
835        }
836    }
837}
838
839impl<T: JsonSchema> JsonSchema for Vec<T> {
840    fn schema() -> Schema {
841        Schema::Array(ArraySchema {
842            items: Box::new(T::schema()),
843            min_items: None,
844            max_items: None,
845        })
846    }
847}
848
849#[cfg(test)]
850mod tests {
851    use super::*;
852
853    // Helper to extract primitive schema details
854    fn get_primitive(schema: Schema) -> PrimitiveSchema {
855        match schema {
856            Schema::Primitive(p) => p,
857            _ => panic!("Expected primitive schema"),
858        }
859    }
860
861    #[test]
862    fn test_signed_integers() {
863        // i8
864        let s = get_primitive(i8::schema());
865        assert!(matches!(s.schema_type, SchemaType::Integer));
866        assert_eq!(s.format.as_deref(), Some("int8"));
867        assert_eq!(s.minimum, None);
868
869        // i16
870        let s = get_primitive(i16::schema());
871        assert!(matches!(s.schema_type, SchemaType::Integer));
872        assert_eq!(s.format.as_deref(), Some("int16"));
873
874        // i32
875        let s = get_primitive(i32::schema());
876        assert!(matches!(s.schema_type, SchemaType::Integer));
877        assert_eq!(s.format.as_deref(), Some("int32"));
878
879        // i64
880        let s = get_primitive(i64::schema());
881        assert!(matches!(s.schema_type, SchemaType::Integer));
882        assert_eq!(s.format.as_deref(), Some("int64"));
883
884        // i128 (no standard format)
885        let s = get_primitive(i128::schema());
886        assert!(matches!(s.schema_type, SchemaType::Integer));
887        assert_eq!(s.format, None);
888
889        // isize
890        let s = get_primitive(isize::schema());
891        assert!(matches!(s.schema_type, SchemaType::Integer));
892        assert_eq!(s.format.as_deref(), Some("int64"));
893    }
894
895    #[test]
896    fn test_unsigned_integers() {
897        // u8
898        let s = get_primitive(u8::schema());
899        assert!(matches!(s.schema_type, SchemaType::Integer));
900        assert_eq!(s.format.as_deref(), Some("uint8"));
901        assert_eq!(s.minimum, Some(0));
902
903        // u16
904        let s = get_primitive(u16::schema());
905        assert!(matches!(s.schema_type, SchemaType::Integer));
906        assert_eq!(s.format.as_deref(), Some("uint16"));
907        assert_eq!(s.minimum, Some(0));
908
909        // u32
910        let s = get_primitive(u32::schema());
911        assert!(matches!(s.schema_type, SchemaType::Integer));
912        assert_eq!(s.format.as_deref(), Some("uint32"));
913        assert_eq!(s.minimum, Some(0));
914
915        // u64
916        let s = get_primitive(u64::schema());
917        assert!(matches!(s.schema_type, SchemaType::Integer));
918        assert_eq!(s.format.as_deref(), Some("uint64"));
919        assert_eq!(s.minimum, Some(0));
920
921        // u128 (no standard format)
922        let s = get_primitive(u128::schema());
923        assert!(matches!(s.schema_type, SchemaType::Integer));
924        assert_eq!(s.format, None);
925        assert_eq!(s.minimum, Some(0));
926
927        // usize
928        let s = get_primitive(usize::schema());
929        assert!(matches!(s.schema_type, SchemaType::Integer));
930        assert_eq!(s.format.as_deref(), Some("uint64"));
931        assert_eq!(s.minimum, Some(0));
932    }
933
934    #[test]
935    fn test_floats() {
936        // f32
937        let s = get_primitive(f32::schema());
938        assert!(matches!(s.schema_type, SchemaType::Number));
939        assert_eq!(s.format.as_deref(), Some("float"));
940
941        // f64
942        let s = get_primitive(f64::schema());
943        assert!(matches!(s.schema_type, SchemaType::Number));
944        assert_eq!(s.format.as_deref(), Some("double"));
945    }
946
947    #[test]
948    fn test_nonzero_signed() {
949        use std::num::{NonZeroI32, NonZeroI64};
950
951        let s = get_primitive(NonZeroI32::schema());
952        assert!(matches!(s.schema_type, SchemaType::Integer));
953        assert_eq!(s.format.as_deref(), Some("int32"));
954
955        let s = get_primitive(NonZeroI64::schema());
956        assert!(matches!(s.schema_type, SchemaType::Integer));
957        assert_eq!(s.format.as_deref(), Some("int64"));
958    }
959
960    #[test]
961    fn test_nonzero_unsigned() {
962        use std::num::{NonZeroU32, NonZeroU64};
963
964        // NonZero unsigned should have minimum: 1
965        let s = get_primitive(NonZeroU32::schema());
966        assert!(matches!(s.schema_type, SchemaType::Integer));
967        assert_eq!(s.format.as_deref(), Some("uint32"));
968        assert_eq!(s.minimum, Some(1));
969
970        let s = get_primitive(NonZeroU64::schema());
971        assert!(matches!(s.schema_type, SchemaType::Integer));
972        assert_eq!(s.format.as_deref(), Some("uint64"));
973        assert_eq!(s.minimum, Some(1));
974    }
975
976    #[test]
977    fn test_string_and_bool() {
978        let s = get_primitive(String::schema());
979        assert!(matches!(s.schema_type, SchemaType::String));
980
981        let s = get_primitive(bool::schema());
982        assert!(matches!(s.schema_type, SchemaType::Boolean));
983    }
984
985    #[test]
986    fn test_serialization() {
987        // Verify JSON serialization of unsigned integer with minimum
988        let schema = u32::schema();
989        let json = serde_json::to_string(&schema).unwrap();
990        assert!(json.contains(r#""type":"integer""#));
991        assert!(json.contains(r#""format":"uint32""#));
992        assert!(json.contains(r#""minimum":0"#));
993
994        // Verify NonZero has minimum 1
995        let schema = std::num::NonZeroU32::schema();
996        let json = serde_json::to_string(&schema).unwrap();
997        assert!(json.contains(r#""minimum":1"#));
998
999        // Verify signed doesn't have minimum
1000        let schema = i32::schema();
1001        let json = serde_json::to_string(&schema).unwrap();
1002        assert!(!json.contains("minimum"));
1003    }
1004
1005    #[test]
1006    fn test_string_enum_schema() {
1007        // Test string_enum helper
1008        let schema = Schema::string_enum(vec![
1009            "Red".to_string(),
1010            "Green".to_string(),
1011            "Blue".to_string(),
1012        ]);
1013        let json = serde_json::to_string(&schema).unwrap();
1014        assert!(json.contains(r#""type":"string""#));
1015        assert!(json.contains(r#""enum":["Red","Green","Blue"]"#));
1016    }
1017
1018    #[test]
1019    fn test_one_of_schema() {
1020        // Test oneOf helper
1021        let schema = Schema::one_of(vec![Schema::string(), Schema::integer(Some("int32"))]);
1022        let json = serde_json::to_string(&schema).unwrap();
1023        assert!(json.contains(r#""oneOf""#));
1024    }
1025
1026    #[test]
1027    fn test_any_of_schema() {
1028        // Test anyOf helper (for untagged enums)
1029        let schema = Schema::any_of(vec![Schema::string(), Schema::boolean()]);
1030        let json = serde_json::to_string(&schema).unwrap();
1031        assert!(json.contains(r#""anyOf""#));
1032    }
1033
1034    #[test]
1035    fn test_enum_schema_with_discriminator() {
1036        // Test oneOf with discriminator
1037        let mut mapping = HashMap::new();
1038        mapping.insert("dog".to_string(), "#/components/schemas/Dog".to_string());
1039        mapping.insert("cat".to_string(), "#/components/schemas/Cat".to_string());
1040
1041        let schema = Schema::one_of_with_discriminator(
1042            vec![Schema::reference("Dog"), Schema::reference("Cat")],
1043            "petType",
1044            mapping,
1045        );
1046        let json = serde_json::to_string(&schema).unwrap();
1047        assert!(json.contains(r#""oneOf""#));
1048        assert!(json.contains(r#""discriminator""#));
1049        assert!(json.contains(r#""propertyName":"petType""#));
1050    }
1051
1052    // =========================================================================
1053    // Schema Constraint Tests
1054    // =========================================================================
1055
1056    #[test]
1057    fn test_exclusive_minimum_serialization() {
1058        let schema = PrimitiveSchema::integer(Some("int32")).with_exclusive_minimum(5);
1059        let json = serde_json::to_string(&Schema::Primitive(schema)).unwrap();
1060        assert!(json.contains(r#""exclusiveMinimum":5"#));
1061        // Should not have regular minimum
1062        assert!(!json.contains(r#""minimum""#));
1063    }
1064
1065    #[test]
1066    fn test_exclusive_maximum_serialization() {
1067        let schema = PrimitiveSchema::integer(Some("int32")).with_exclusive_maximum(100);
1068        let json = serde_json::to_string(&Schema::Primitive(schema)).unwrap();
1069        assert!(json.contains(r#""exclusiveMaximum":100"#));
1070        // Should not have regular maximum
1071        assert!(!json.contains(r#""maximum""#));
1072    }
1073
1074    #[test]
1075    fn test_min_length_serialization() {
1076        let schema = PrimitiveSchema::string().with_min_length(3);
1077        let json = serde_json::to_string(&Schema::Primitive(schema)).unwrap();
1078        assert!(json.contains(r#""minLength":3"#));
1079    }
1080
1081    #[test]
1082    fn test_max_length_serialization() {
1083        let schema = PrimitiveSchema::string().with_max_length(255);
1084        let json = serde_json::to_string(&Schema::Primitive(schema)).unwrap();
1085        assert!(json.contains(r#""maxLength":255"#));
1086    }
1087
1088    #[test]
1089    fn test_pattern_serialization() {
1090        let schema = PrimitiveSchema::string().with_pattern(r"^[a-z]+$");
1091        let json = serde_json::to_string(&Schema::Primitive(schema)).unwrap();
1092        assert!(json.contains(r#""pattern":"^[a-z]+$""#));
1093    }
1094
1095    #[test]
1096    fn test_combined_string_constraints() {
1097        let schema = PrimitiveSchema::string()
1098            .with_min_length(1)
1099            .with_max_length(50)
1100            .with_pattern(r"^[A-Z][a-z]+$");
1101        let json = serde_json::to_string(&Schema::Primitive(schema)).unwrap();
1102        assert!(json.contains(r#""minLength":1"#));
1103        assert!(json.contains(r#""maxLength":50"#));
1104        assert!(json.contains(r#""pattern":"^[A-Z][a-z]+$""#));
1105    }
1106
1107    #[test]
1108    fn test_combined_number_constraints() {
1109        let schema = PrimitiveSchema::integer(Some("int32"))
1110            .with_minimum(0)
1111            .with_maximum(100);
1112        let json = serde_json::to_string(&Schema::Primitive(schema)).unwrap();
1113        assert!(json.contains(r#""minimum":0"#));
1114        assert!(json.contains(r#""maximum":100"#));
1115    }
1116
1117    #[test]
1118    fn test_constraints_not_serialized_when_none() {
1119        let schema = PrimitiveSchema::string();
1120        let json = serde_json::to_string(&Schema::Primitive(schema)).unwrap();
1121        // None of the constraint fields should appear
1122        assert!(!json.contains("minimum"));
1123        assert!(!json.contains("maximum"));
1124        assert!(!json.contains("exclusiveMinimum"));
1125        assert!(!json.contains("exclusiveMaximum"));
1126        assert!(!json.contains("minLength"));
1127        assert!(!json.contains("maxLength"));
1128        assert!(!json.contains("pattern"));
1129    }
1130
1131    // =========================================================================
1132    // SchemaRegistry Tests
1133    // =========================================================================
1134
1135    #[test]
1136    fn test_registry_new_is_empty() {
1137        let registry = SchemaRegistry::new();
1138        assert!(registry.is_empty());
1139        assert_eq!(registry.len(), 0);
1140    }
1141
1142    #[test]
1143    fn test_registry_register_direct() {
1144        let registry = SchemaRegistry::new();
1145        let schema = Schema::string();
1146
1147        let result = registry.register("Username", schema);
1148
1149        assert!(registry.contains("Username"));
1150        assert_eq!(registry.len(), 1);
1151
1152        // Result should be a $ref
1153        if let Schema::Ref(ref_schema) = result {
1154            assert_eq!(ref_schema.reference, "#/components/schemas/Username");
1155        } else {
1156            panic!("Expected Schema::Ref");
1157        }
1158    }
1159
1160    #[test]
1161    fn test_registry_get_or_register_new() {
1162        let registry = SchemaRegistry::new();
1163
1164        // First call registers and returns $ref
1165        let result = registry.get_or_register::<String>("StringType");
1166
1167        assert!(registry.contains("StringType"));
1168        if let Schema::Ref(ref_schema) = result {
1169            assert_eq!(ref_schema.reference, "#/components/schemas/StringType");
1170        } else {
1171            panic!("Expected Schema::Ref");
1172        }
1173    }
1174
1175    #[test]
1176    fn test_registry_get_or_register_existing() {
1177        let registry = SchemaRegistry::new();
1178
1179        // First call
1180        let _result1 = registry.get_or_register::<String>("StringType");
1181        let initial_len = registry.len();
1182
1183        // Second call should not add a new entry
1184        let result2 = registry.get_or_register::<String>("StringType");
1185
1186        assert_eq!(registry.len(), initial_len);
1187        if let Schema::Ref(ref_schema) = result2 {
1188            assert_eq!(ref_schema.reference, "#/components/schemas/StringType");
1189        } else {
1190            panic!("Expected Schema::Ref");
1191        }
1192    }
1193
1194    #[test]
1195    fn test_registry_into_schemas() {
1196        let registry = SchemaRegistry::new();
1197        registry.register("Type1", Schema::string());
1198        registry.register("Type2", Schema::boolean());
1199
1200        let schemas = registry.into_schemas();
1201
1202        assert_eq!(schemas.len(), 2);
1203        assert!(schemas.contains_key("Type1"));
1204        assert!(schemas.contains_key("Type2"));
1205    }
1206
1207    #[test]
1208    fn test_registry_schemas_clone() {
1209        let registry = SchemaRegistry::new();
1210        registry.register("Type1", Schema::string());
1211
1212        let schemas = registry.schemas();
1213
1214        // Registry should still have the schema
1215        assert!(registry.contains("Type1"));
1216        assert_eq!(schemas.len(), 1);
1217    }
1218
1219    #[test]
1220    fn test_registry_merge() {
1221        let registry1 = SchemaRegistry::new();
1222        registry1.register("Type1", Schema::string());
1223
1224        let registry2 = SchemaRegistry::new();
1225        registry2.register("Type2", Schema::boolean());
1226        registry2.register("Type1", Schema::integer(Some("int32"))); // Duplicate name
1227
1228        registry1.merge(&registry2);
1229
1230        // Should have both types
1231        assert_eq!(registry1.len(), 2);
1232        assert!(registry1.contains("Type1"));
1233        assert!(registry1.contains("Type2"));
1234
1235        // Original Type1 should be preserved (string, not integer)
1236        let schemas = registry1.into_schemas();
1237        if let Schema::Primitive(p) = &schemas["Type1"] {
1238            assert!(matches!(p.schema_type, SchemaType::String));
1239        } else {
1240            panic!("Expected primitive string schema");
1241        }
1242    }
1243
1244    #[test]
1245    fn test_registry_clone() {
1246        let registry1 = SchemaRegistry::new();
1247        registry1.register("Type1", Schema::string());
1248
1249        let registry2 = registry1.clone();
1250
1251        assert!(registry2.contains("Type1"));
1252        assert_eq!(registry2.len(), 1);
1253
1254        // Modifications to clone don't affect original
1255        registry2.register("Type2", Schema::boolean());
1256        assert!(!registry1.contains("Type2"));
1257        assert!(registry2.contains("Type2"));
1258    }
1259
1260    #[test]
1261    fn test_ref_schema_serialization() {
1262        let ref_schema = Schema::reference("User");
1263        let json = serde_json::to_string(&ref_schema).unwrap();
1264        assert!(json.contains(r##""$ref":"#/components/schemas/User""##));
1265    }
1266
1267    #[test]
1268    fn test_registry_with_object_schema() {
1269        let registry = SchemaRegistry::new();
1270
1271        let user_schema = Schema::object(
1272            [
1273                ("id".to_string(), Schema::integer(Some("int64"))),
1274                ("name".to_string(), Schema::string()),
1275            ]
1276            .into_iter()
1277            .collect(),
1278            vec!["id".to_string(), "name".to_string()],
1279        );
1280
1281        let result = registry.register("User", user_schema);
1282
1283        // Should return a $ref
1284        if let Schema::Ref(ref_schema) = result {
1285            assert_eq!(ref_schema.reference, "#/components/schemas/User");
1286        } else {
1287            panic!("Expected Schema::Ref");
1288        }
1289
1290        // The stored schema should be the object
1291        let schemas = registry.into_schemas();
1292        if let Schema::Object(obj) = &schemas["User"] {
1293            assert!(obj.properties.contains_key("id"));
1294            assert!(obj.properties.contains_key("name"));
1295            assert!(obj.required.contains(&"id".to_string()));
1296        } else {
1297            panic!("Expected object schema");
1298        }
1299    }
1300
1301    #[test]
1302    fn test_registry_nested_refs() {
1303        let registry = SchemaRegistry::new();
1304
1305        // Register Address schema
1306        let address_schema = Schema::object(
1307            [
1308                ("street".to_string(), Schema::string()),
1309                ("city".to_string(), Schema::string()),
1310            ]
1311            .into_iter()
1312            .collect(),
1313            vec!["street".to_string(), "city".to_string()],
1314        );
1315        let _address_ref = registry.register("Address", address_schema);
1316
1317        // Register User schema with $ref to Address
1318        let user_schema = Schema::object(
1319            [
1320                ("name".to_string(), Schema::string()),
1321                ("address".to_string(), Schema::reference("Address")),
1322            ]
1323            .into_iter()
1324            .collect(),
1325            vec!["name".to_string()],
1326        );
1327        let _user_ref = registry.register("User", user_schema);
1328
1329        let schemas = registry.into_schemas();
1330        assert_eq!(schemas.len(), 2);
1331
1332        // User's address field should be a $ref
1333        if let Schema::Object(obj) = &schemas["User"] {
1334            if let Schema::Ref(ref_schema) = &obj.properties["address"] {
1335                assert_eq!(ref_schema.reference, "#/components/schemas/Address");
1336            } else {
1337                panic!("Expected address to be a $ref");
1338            }
1339        } else {
1340            panic!("Expected User to be an object");
1341        }
1342    }
1343
1344    #[test]
1345    fn test_registry_default() {
1346        let registry = SchemaRegistry::default();
1347        assert!(registry.is_empty());
1348    }
1349}