domainstack_schema/
schema.rs

1//! OpenAPI schema type definitions.
2
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6/// OpenAPI schema object representing a data type.
7#[derive(Debug, Clone, Serialize, Deserialize)]
8#[serde(rename_all = "camelCase")]
9pub struct Schema {
10    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
11    pub schema_type: Option<SchemaType>,
12
13    #[serde(skip_serializing_if = "Option::is_none")]
14    pub format: Option<String>,
15
16    #[serde(skip_serializing_if = "Option::is_none")]
17    pub description: Option<String>,
18
19    #[serde(skip_serializing_if = "Option::is_none")]
20    pub properties: Option<HashMap<String, Schema>>,
21
22    #[serde(skip_serializing_if = "Option::is_none")]
23    pub required: Option<Vec<String>>,
24
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub items: Option<Box<Schema>>,
27
28    // String constraints
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub min_length: Option<usize>,
31
32    #[serde(skip_serializing_if = "Option::is_none")]
33    pub max_length: Option<usize>,
34
35    #[serde(skip_serializing_if = "Option::is_none")]
36    pub pattern: Option<String>,
37
38    // Numeric constraints
39    #[serde(skip_serializing_if = "Option::is_none")]
40    pub minimum: Option<f64>,
41
42    #[serde(skip_serializing_if = "Option::is_none")]
43    pub maximum: Option<f64>,
44
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub multiple_of: Option<f64>,
47
48    // Array constraints
49    #[serde(skip_serializing_if = "Option::is_none")]
50    pub min_items: Option<usize>,
51
52    #[serde(skip_serializing_if = "Option::is_none")]
53    pub max_items: Option<usize>,
54
55    #[serde(skip_serializing_if = "Option::is_none")]
56    pub unique_items: Option<bool>,
57
58    // Enum constraint
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub r#enum: Option<Vec<serde_json::Value>>,
61
62    // Reference to another schema
63    #[serde(rename = "$ref", skip_serializing_if = "Option::is_none")]
64    pub reference: Option<String>,
65
66    // Schema composition (v0.8)
67    #[serde(skip_serializing_if = "Option::is_none")]
68    pub any_of: Option<Vec<Schema>>,
69
70    #[serde(skip_serializing_if = "Option::is_none")]
71    pub all_of: Option<Vec<Schema>>,
72
73    #[serde(skip_serializing_if = "Option::is_none")]
74    pub one_of: Option<Vec<Schema>>,
75
76    // Metadata (v0.8)
77    #[serde(skip_serializing_if = "Option::is_none")]
78    pub default: Option<serde_json::Value>,
79
80    #[serde(skip_serializing_if = "Option::is_none")]
81    pub example: Option<serde_json::Value>,
82
83    #[serde(skip_serializing_if = "Option::is_none")]
84    pub examples: Option<Vec<serde_json::Value>>,
85
86    // Field modifiers (v0.8)
87    #[serde(skip_serializing_if = "Option::is_none")]
88    pub read_only: Option<bool>,
89
90    #[serde(skip_serializing_if = "Option::is_none")]
91    pub write_only: Option<bool>,
92
93    #[serde(skip_serializing_if = "Option::is_none")]
94    pub deprecated: Option<bool>,
95
96    // Vendor extensions (v0.8) - for non-mappable validations
97    #[serde(flatten, skip_serializing_if = "Option::is_none")]
98    pub extensions: Option<HashMap<String, serde_json::Value>>,
99}
100
101/// OpenAPI schema types.
102#[derive(Debug, Clone, Serialize, Deserialize)]
103#[serde(rename_all = "lowercase")]
104pub enum SchemaType {
105    String,
106    Number,
107    Integer,
108    Boolean,
109    Array,
110    Object,
111}
112
113impl Schema {
114    /// Create a new empty schema.
115    pub fn new() -> Self {
116        Self {
117            schema_type: None,
118            format: None,
119            description: None,
120            properties: None,
121            required: None,
122            items: None,
123            min_length: None,
124            max_length: None,
125            pattern: None,
126            minimum: None,
127            maximum: None,
128            multiple_of: None,
129            min_items: None,
130            max_items: None,
131            unique_items: None,
132            r#enum: None,
133            reference: None,
134            any_of: None,
135            all_of: None,
136            one_of: None,
137            default: None,
138            example: None,
139            examples: None,
140            read_only: None,
141            write_only: None,
142            deprecated: None,
143            extensions: None,
144        }
145    }
146
147    /// Create a string schema.
148    pub fn string() -> Self {
149        Self {
150            schema_type: Some(SchemaType::String),
151            ..Self::new()
152        }
153    }
154
155    /// Create an integer schema.
156    pub fn integer() -> Self {
157        Self {
158            schema_type: Some(SchemaType::Integer),
159            ..Self::new()
160        }
161    }
162
163    /// Create a number schema.
164    pub fn number() -> Self {
165        Self {
166            schema_type: Some(SchemaType::Number),
167            ..Self::new()
168        }
169    }
170
171    /// Create a boolean schema.
172    pub fn boolean() -> Self {
173        Self {
174            schema_type: Some(SchemaType::Boolean),
175            ..Self::new()
176        }
177    }
178
179    /// Create an array schema.
180    pub fn array(items: Schema) -> Self {
181        Self {
182            schema_type: Some(SchemaType::Array),
183            items: Some(Box::new(items)),
184            ..Self::new()
185        }
186    }
187
188    /// Create an object schema.
189    pub fn object() -> Self {
190        Self {
191            schema_type: Some(SchemaType::Object),
192            properties: Some(HashMap::new()),
193            ..Self::new()
194        }
195    }
196
197    /// Create a reference to another schema.
198    pub fn reference(name: &str) -> Self {
199        Self {
200            reference: Some(format!("#/components/schemas/{}", name)),
201            ..Self::new()
202        }
203    }
204
205    /// Set the format (e.g., "email", "date-time").
206    pub fn format(mut self, format: impl Into<String>) -> Self {
207        self.format = Some(format.into());
208        self
209    }
210
211    /// Set the description.
212    pub fn description(mut self, desc: impl Into<String>) -> Self {
213        self.description = Some(desc.into());
214        self
215    }
216
217    /// Add a property to an object schema.
218    pub fn property(mut self, name: impl Into<String>, schema: Schema) -> Self {
219        self.properties
220            .get_or_insert_with(HashMap::new)
221            .insert(name.into(), schema);
222        self
223    }
224
225    /// Set required fields.
226    pub fn required(mut self, fields: &[&str]) -> Self {
227        self.required = Some(fields.iter().map(|s| s.to_string()).collect());
228        self
229    }
230
231    /// Set minimum length for strings.
232    pub fn min_length(mut self, min: usize) -> Self {
233        self.min_length = Some(min);
234        self
235    }
236
237    /// Set maximum length for strings.
238    pub fn max_length(mut self, max: usize) -> Self {
239        self.max_length = Some(max);
240        self
241    }
242
243    /// Set regex pattern for strings.
244    pub fn pattern(mut self, pattern: impl Into<String>) -> Self {
245        self.pattern = Some(pattern.into());
246        self
247    }
248
249    /// Set minimum value for numbers.
250    pub fn minimum(mut self, min: impl Into<f64>) -> Self {
251        self.minimum = Some(min.into());
252        self
253    }
254
255    /// Set maximum value for numbers.
256    pub fn maximum(mut self, max: impl Into<f64>) -> Self {
257        self.maximum = Some(max.into());
258        self
259    }
260
261    /// Set multiple_of constraint for numbers.
262    pub fn multiple_of(mut self, divisor: impl Into<f64>) -> Self {
263        self.multiple_of = Some(divisor.into());
264        self
265    }
266
267    /// Set minimum items for arrays.
268    pub fn min_items(mut self, min: usize) -> Self {
269        self.min_items = Some(min);
270        self
271    }
272
273    /// Set maximum items for arrays.
274    pub fn max_items(mut self, max: usize) -> Self {
275        self.max_items = Some(max);
276        self
277    }
278
279    /// Set unique items constraint for arrays.
280    pub fn unique_items(mut self, unique: bool) -> Self {
281        self.unique_items = Some(unique);
282        self
283    }
284
285    /// Set enum values.
286    ///
287    /// # Panics
288    /// Panics if any value cannot be serialized to JSON.
289    /// Use [`Self::try_enum_values`] for a non-panicking alternative.
290    pub fn enum_values<T: Serialize>(mut self, values: &[T]) -> Self {
291        self.r#enum = Some(
292            values
293                .iter()
294                .map(|v| serde_json::to_value(v).expect("Failed to serialize enum value"))
295                .collect(),
296        );
297        self
298    }
299
300    /// Set enum values (non-panicking version).
301    ///
302    /// Returns an error if any value cannot be serialized to JSON.
303    pub fn try_enum_values<T: Serialize>(
304        mut self,
305        values: &[T],
306    ) -> Result<Self, serde_json::Error> {
307        let serialized: Result<Vec<_>, _> = values.iter().map(serde_json::to_value).collect();
308        self.r#enum = Some(serialized?);
309        Ok(self)
310    }
311
312    // === v0.8 features ===
313
314    /// Create a schema that matches any of the given schemas (union type).
315    ///
316    /// # Example
317    /// ```rust
318    /// use domainstack_schema::Schema;
319    ///
320    /// let schema = Schema::any_of(vec![
321    ///     Schema::string(),
322    ///     Schema::integer(),
323    /// ]);
324    /// ```
325    pub fn any_of(schemas: Vec<Schema>) -> Self {
326        Self {
327            any_of: Some(schemas),
328            ..Self::new()
329        }
330    }
331
332    /// Create a schema that matches all of the given schemas (intersection/composition).
333    ///
334    /// # Example
335    /// ```rust
336    /// use domainstack_schema::Schema;
337    ///
338    /// let schema = Schema::all_of(vec![
339    ///     Schema::reference("BaseUser"),
340    ///     Schema::object().property("admin", Schema::boolean()),
341    /// ]);
342    /// ```
343    pub fn all_of(schemas: Vec<Schema>) -> Self {
344        Self {
345            all_of: Some(schemas),
346            ..Self::new()
347        }
348    }
349
350    /// Create a schema that matches exactly one of the given schemas (discriminated union).
351    ///
352    /// # Example
353    /// ```rust
354    /// use domainstack_schema::Schema;
355    ///
356    /// let schema = Schema::one_of(vec![
357    ///     Schema::object().property("type", Schema::string().enum_values(&["card"])),
358    ///     Schema::object().property("type", Schema::string().enum_values(&["cash"])),
359    /// ]);
360    /// ```
361    pub fn one_of(schemas: Vec<Schema>) -> Self {
362        Self {
363            one_of: Some(schemas),
364            ..Self::new()
365        }
366    }
367
368    /// Set a default value for this schema.
369    ///
370    /// # Example
371    /// ```rust
372    /// use domainstack_schema::Schema;
373    /// use serde_json::json;
374    ///
375    /// let schema = Schema::string().default(json!("guest"));
376    /// ```
377    ///
378    /// # Panics
379    /// Panics if the value cannot be serialized to JSON.
380    /// Use [`Self::try_default`] for a non-panicking alternative.
381    pub fn default<T: Serialize>(mut self, value: T) -> Self {
382        self.default =
383            Some(serde_json::to_value(value).expect("Failed to serialize default value"));
384        self
385    }
386
387    /// Set a default value for this schema (non-panicking version).
388    ///
389    /// Returns an error if the value cannot be serialized to JSON.
390    pub fn try_default<T: Serialize>(mut self, value: T) -> Result<Self, serde_json::Error> {
391        self.default = Some(serde_json::to_value(value)?);
392        Ok(self)
393    }
394
395    /// Set an example value for this schema.
396    ///
397    /// # Example
398    /// ```rust
399    /// use domainstack_schema::Schema;
400    /// use serde_json::json;
401    ///
402    /// let schema = Schema::string().example(json!("john_doe"));
403    /// ```
404    ///
405    /// # Panics
406    /// Panics if the value cannot be serialized to JSON.
407    /// Use [`Self::try_example`] for a non-panicking alternative.
408    pub fn example<T: Serialize>(mut self, value: T) -> Self {
409        self.example =
410            Some(serde_json::to_value(value).expect("Failed to serialize example value"));
411        self
412    }
413
414    /// Set an example value for this schema (non-panicking version).
415    ///
416    /// Returns an error if the value cannot be serialized to JSON.
417    pub fn try_example<T: Serialize>(mut self, value: T) -> Result<Self, serde_json::Error> {
418        self.example = Some(serde_json::to_value(value)?);
419        Ok(self)
420    }
421
422    /// Set multiple example values for this schema.
423    ///
424    /// # Example
425    /// ```rust
426    /// use domainstack_schema::Schema;
427    /// use serde_json::json;
428    ///
429    /// let schema = Schema::string().examples(vec![
430    ///     json!("alice"),
431    ///     json!("bob"),
432    /// ]);
433    /// ```
434    ///
435    /// # Panics
436    /// Panics if any value cannot be serialized to JSON.
437    /// Use [`Self::try_examples`] for a non-panicking alternative.
438    pub fn examples<T: Serialize>(mut self, values: Vec<T>) -> Self {
439        self.examples = Some(
440            values
441                .into_iter()
442                .map(|v| serde_json::to_value(v).expect("Failed to serialize example value"))
443                .collect(),
444        );
445        self
446    }
447
448    /// Set multiple example values for this schema (non-panicking version).
449    ///
450    /// Returns an error if any value cannot be serialized to JSON.
451    pub fn try_examples<T: Serialize>(mut self, values: Vec<T>) -> Result<Self, serde_json::Error> {
452        let serialized: Result<Vec<_>, _> = values
453            .into_iter()
454            .map(|v| serde_json::to_value(v))
455            .collect();
456        self.examples = Some(serialized?);
457        Ok(self)
458    }
459
460    /// Mark this field as read-only (returned in responses, not accepted in requests).
461    ///
462    /// # Example
463    /// ```rust
464    /// use domainstack_schema::Schema;
465    ///
466    /// let schema = Schema::string().read_only(true);
467    /// ```
468    pub fn read_only(mut self, read_only: bool) -> Self {
469        self.read_only = Some(read_only);
470        self
471    }
472
473    /// Mark this field as write-only (accepted in requests, not returned in responses).
474    ///
475    /// # Example
476    /// ```rust
477    /// use domainstack_schema::Schema;
478    ///
479    /// let password = Schema::string()
480    ///     .format("password")
481    ///     .write_only(true);
482    /// ```
483    pub fn write_only(mut self, write_only: bool) -> Self {
484        self.write_only = Some(write_only);
485        self
486    }
487
488    /// Mark this field as deprecated.
489    ///
490    /// # Example
491    /// ```rust
492    /// use domainstack_schema::Schema;
493    ///
494    /// let schema = Schema::string()
495    ///     .deprecated(true)
496    ///     .description("Use 'new_field' instead");
497    /// ```
498    pub fn deprecated(mut self, deprecated: bool) -> Self {
499        self.deprecated = Some(deprecated);
500        self
501    }
502
503    /// Add a vendor extension (for validations that don't map to OpenAPI).
504    ///
505    /// Extension keys should start with "x-".
506    ///
507    /// # Example
508    /// ```rust
509    /// use domainstack_schema::Schema;
510    /// use serde_json::json;
511    ///
512    /// let schema = Schema::object()
513    ///     .property("end_date", Schema::string().format("date"))
514    ///     .extension("x-domainstack-validations", json!({
515    ///         "cross_field": ["end_date > start_date"]
516    ///     }));
517    /// ```
518    ///
519    /// # Panics
520    /// Panics if the value cannot be serialized to JSON.
521    /// Use [`Self::try_extension`] for a non-panicking alternative.
522    pub fn extension<T: Serialize>(mut self, key: impl Into<String>, value: T) -> Self {
523        self.extensions.get_or_insert_with(HashMap::new).insert(
524            key.into(),
525            serde_json::to_value(value).expect("Failed to serialize extension value"),
526        );
527        self
528    }
529
530    /// Add a vendor extension (non-panicking version).
531    ///
532    /// Extension keys should start with "x-".
533    /// Returns an error if the value cannot be serialized to JSON.
534    pub fn try_extension<T: Serialize>(
535        mut self,
536        key: impl Into<String>,
537        value: T,
538    ) -> Result<Self, serde_json::Error> {
539        self.extensions
540            .get_or_insert_with(HashMap::new)
541            .insert(key.into(), serde_json::to_value(value)?);
542        Ok(self)
543    }
544}
545
546impl Default for Schema {
547    fn default() -> Self {
548        Self::new()
549    }
550}
551
552#[cfg(test)]
553mod tests {
554    use super::*;
555
556    #[test]
557    fn test_string_schema() {
558        let schema = Schema::string()
559            .min_length(5)
560            .max_length(100)
561            .format("email");
562
563        assert!(matches!(schema.schema_type, Some(SchemaType::String)));
564        assert_eq!(schema.min_length, Some(5));
565        assert_eq!(schema.max_length, Some(100));
566        assert_eq!(schema.format, Some("email".to_string()));
567    }
568
569    #[test]
570    fn test_integer_schema() {
571        let schema = Schema::integer().minimum(0).maximum(100);
572
573        assert!(matches!(schema.schema_type, Some(SchemaType::Integer)));
574        assert_eq!(schema.minimum, Some(0.0));
575        assert_eq!(schema.maximum, Some(100.0));
576    }
577
578    #[test]
579    fn test_object_schema() {
580        let schema = Schema::object()
581            .property("name", Schema::string())
582            .property("age", Schema::integer().minimum(0))
583            .required(&["name", "age"]);
584
585        assert!(matches!(schema.schema_type, Some(SchemaType::Object)));
586        assert_eq!(schema.properties.as_ref().unwrap().len(), 2);
587        assert_eq!(schema.required.as_ref().unwrap().len(), 2);
588    }
589
590    #[test]
591    fn test_array_schema() {
592        let schema = Schema::array(Schema::string())
593            .min_items(1)
594            .max_items(10)
595            .unique_items(true);
596
597        assert!(matches!(schema.schema_type, Some(SchemaType::Array)));
598        assert_eq!(schema.min_items, Some(1));
599        assert_eq!(schema.max_items, Some(10));
600        assert_eq!(schema.unique_items, Some(true));
601    }
602
603    #[test]
604    fn test_schema_serialization() {
605        let schema = Schema::object()
606            .property("email", Schema::string().format("email"))
607            .property("age", Schema::integer().minimum(18).maximum(120))
608            .required(&["email", "age"]);
609
610        let json = serde_json::to_string_pretty(&schema).unwrap();
611        assert!(json.contains("\"type\": \"object\""));
612        assert!(json.contains("\"email\""));
613        assert!(json.contains("\"age\""));
614    }
615
616    // === v0.8 tests ===
617
618    #[test]
619    fn test_any_of_composition() {
620        let schema = Schema::any_of(vec![Schema::string(), Schema::integer()]);
621
622        assert!(schema.any_of.is_some());
623        assert_eq!(schema.any_of.as_ref().unwrap().len(), 2);
624    }
625
626    #[test]
627    fn test_all_of_composition() {
628        let schema = Schema::all_of(vec![
629            Schema::reference("BaseUser"),
630            Schema::object().property("admin", Schema::boolean()),
631        ]);
632
633        assert!(schema.all_of.is_some());
634        assert_eq!(schema.all_of.as_ref().unwrap().len(), 2);
635    }
636
637    #[test]
638    fn test_one_of_composition() {
639        let schema = Schema::one_of(vec![
640            Schema::object().property("type", Schema::string()),
641            Schema::object().property("kind", Schema::string()),
642        ]);
643
644        assert!(schema.one_of.is_some());
645        assert_eq!(schema.one_of.as_ref().unwrap().len(), 2);
646    }
647
648    #[test]
649    fn test_default_value() {
650        use serde_json::json;
651
652        let schema = Schema::string().default(json!("guest"));
653
654        assert!(schema.default.is_some());
655        assert_eq!(schema.default.unwrap(), json!("guest"));
656    }
657
658    #[test]
659    fn test_example() {
660        use serde_json::json;
661
662        let schema = Schema::string().example(json!("john_doe"));
663
664        assert!(schema.example.is_some());
665        assert_eq!(schema.example.unwrap(), json!("john_doe"));
666    }
667
668    #[test]
669    fn test_examples() {
670        use serde_json::json;
671
672        let schema = Schema::string().examples(vec![json!("alice"), json!("bob")]);
673
674        assert!(schema.examples.is_some());
675        assert_eq!(schema.examples.as_ref().unwrap().len(), 2);
676    }
677
678    #[test]
679    fn test_read_only() {
680        let schema = Schema::string().read_only(true);
681
682        assert_eq!(schema.read_only, Some(true));
683    }
684
685    #[test]
686    fn test_write_only() {
687        let schema = Schema::string().format("password").write_only(true);
688
689        assert_eq!(schema.write_only, Some(true));
690        assert_eq!(schema.format, Some("password".to_string()));
691    }
692
693    #[test]
694    fn test_deprecated() {
695        let schema = Schema::string().deprecated(true);
696
697        assert_eq!(schema.deprecated, Some(true));
698    }
699
700    #[test]
701    fn test_vendor_extension() {
702        use serde_json::json;
703
704        let schema = Schema::object().extension(
705            "x-domainstack-validations",
706            json!({"cross_field": ["end > start"]}),
707        );
708
709        assert!(schema.extensions.is_some());
710        let extensions = schema.extensions.as_ref().unwrap();
711        assert!(extensions.contains_key("x-domainstack-validations"));
712    }
713
714    #[test]
715    fn test_composition_serialization() {
716        let schema = Schema::any_of(vec![Schema::string(), Schema::integer()]);
717
718        let json_value = serde_json::to_value(&schema).unwrap();
719        assert!(json_value.get("anyOf").is_some());
720    }
721
722    #[test]
723    fn test_read_write_only_request_response() {
724        // Password: write-only (send in request, never returned)
725        let password = Schema::string()
726            .format("password")
727            .write_only(true)
728            .min_length(8);
729
730        // ID: read-only (returned in response, never accepted in request)
731        let id = Schema::string().read_only(true);
732
733        let user_schema = Schema::object()
734            .property("id", id)
735            .property("email", Schema::string().format("email"))
736            .property("password", password)
737            .required(&["email", "password"]);
738
739        let json = serde_json::to_string_pretty(&user_schema).unwrap();
740        assert!(json.contains("\"writeOnly\": true"));
741        assert!(json.contains("\"readOnly\": true"));
742    }
743
744    #[test]
745    fn test_string_constraints() {
746        let schema = Schema::string()
747            .min_length(5)
748            .max_length(100)
749            .pattern("^[a-z]+$");
750
751        assert_eq!(schema.min_length, Some(5));
752        assert_eq!(schema.max_length, Some(100));
753        assert_eq!(schema.pattern, Some("^[a-z]+$".to_string()));
754    }
755
756    #[test]
757    fn test_numeric_constraints() {
758        let schema = Schema::number()
759            .minimum(0.0)
760            .maximum(100.5)
761            .multiple_of(0.5);
762
763        assert_eq!(schema.minimum, Some(0.0));
764        assert_eq!(schema.maximum, Some(100.5));
765        assert_eq!(schema.multiple_of, Some(0.5));
766    }
767
768    #[test]
769    fn test_array_constraints() {
770        let schema = Schema::array(Schema::string())
771            .min_items(1)
772            .max_items(10)
773            .unique_items(true);
774
775        assert_eq!(schema.min_items, Some(1));
776        assert_eq!(schema.max_items, Some(10));
777        assert_eq!(schema.unique_items, Some(true));
778    }
779
780    #[test]
781    fn test_enum_values() {
782        let schema = Schema::string().enum_values(&["red", "green", "blue"]);
783
784        assert!(schema.r#enum.is_some());
785        let enum_vals = schema.r#enum.unwrap();
786        assert_eq!(enum_vals.len(), 3);
787    }
788
789    #[test]
790    fn test_format_and_description() {
791        let schema = Schema::string()
792            .format("email")
793            .description("User's email address");
794
795        assert_eq!(schema.format, Some("email".to_string()));
796        assert_eq!(schema.description, Some("User's email address".to_string()));
797    }
798
799    #[test]
800    fn test_reference_schema() {
801        let schema = Schema::reference("User");
802
803        assert_eq!(
804            schema.reference,
805            Some("#/components/schemas/User".to_string())
806        );
807        assert!(schema.schema_type.is_none());
808    }
809
810    #[test]
811    fn test_new_schema() {
812        let schema = Schema::new();
813
814        assert!(schema.schema_type.is_none());
815        assert!(schema.properties.is_none());
816        assert!(schema.required.is_none());
817    }
818
819    #[test]
820    fn test_boolean_schema() {
821        let schema = Schema::boolean();
822
823        assert!(matches!(schema.schema_type, Some(SchemaType::Boolean)));
824    }
825
826    #[test]
827    fn test_number_schema() {
828        let schema = Schema::number();
829
830        assert!(matches!(schema.schema_type, Some(SchemaType::Number)));
831    }
832
833    // Tests for try_* methods
834    #[test]
835    fn test_try_enum_values_valid() {
836        let schema = Schema::string().try_enum_values(&["red", "green", "blue"]);
837
838        assert!(schema.is_ok());
839        let schema = schema.unwrap();
840        assert!(schema.r#enum.is_some());
841        assert_eq!(schema.r#enum.as_ref().unwrap().len(), 3);
842    }
843
844    #[test]
845    fn test_try_enum_values_with_numbers() {
846        let schema = Schema::integer().try_enum_values(&[1, 2, 3, 5, 8]);
847
848        assert!(schema.is_ok());
849        let schema = schema.unwrap();
850        assert_eq!(schema.r#enum.as_ref().unwrap().len(), 5);
851    }
852
853    #[test]
854    fn test_try_enum_values_empty() {
855        let schema = Schema::string().try_enum_values::<&str>(&[]);
856
857        assert!(schema.is_ok());
858        let schema = schema.unwrap();
859        assert!(schema.r#enum.is_some());
860        assert!(schema.r#enum.as_ref().unwrap().is_empty());
861    }
862
863    #[test]
864    fn test_try_default_valid() {
865        let schema = Schema::string().try_default("guest");
866
867        assert!(schema.is_ok());
868        let schema = schema.unwrap();
869        assert_eq!(schema.default, Some(serde_json::json!("guest")));
870    }
871
872    #[test]
873    fn test_try_default_with_number() {
874        let schema = Schema::integer().try_default(42);
875
876        assert!(schema.is_ok());
877        let schema = schema.unwrap();
878        assert_eq!(schema.default, Some(serde_json::json!(42)));
879    }
880
881    #[test]
882    fn test_try_default_with_object() {
883        use serde_json::json;
884
885        let schema = Schema::object().try_default(json!({"name": "default"}));
886
887        assert!(schema.is_ok());
888        let schema = schema.unwrap();
889        assert_eq!(schema.default, Some(json!({"name": "default"})));
890    }
891
892    #[test]
893    fn test_try_example_valid() {
894        let schema = Schema::string().try_example("john_doe");
895
896        assert!(schema.is_ok());
897        let schema = schema.unwrap();
898        assert_eq!(schema.example, Some(serde_json::json!("john_doe")));
899    }
900
901    #[test]
902    fn test_try_example_with_complex_value() {
903        use serde_json::json;
904
905        let schema = Schema::object().try_example(json!({
906            "name": "Alice",
907            "age": 30,
908            "active": true
909        }));
910
911        assert!(schema.is_ok());
912    }
913
914    #[test]
915    fn test_try_examples_valid() {
916        use serde_json::json;
917
918        let schema = Schema::string().try_examples(vec![json!("alice"), json!("bob")]);
919
920        assert!(schema.is_ok());
921        let schema = schema.unwrap();
922        assert_eq!(schema.examples.as_ref().unwrap().len(), 2);
923    }
924
925    #[test]
926    fn test_try_examples_empty() {
927        let schema = Schema::string().try_examples::<serde_json::Value>(vec![]);
928
929        assert!(schema.is_ok());
930        let schema = schema.unwrap();
931        assert!(schema.examples.as_ref().unwrap().is_empty());
932    }
933
934    #[test]
935    fn test_try_examples_many() {
936        let examples: Vec<_> = (0..100).collect();
937        let schema = Schema::integer().try_examples(examples);
938
939        assert!(schema.is_ok());
940        let schema = schema.unwrap();
941        assert_eq!(schema.examples.as_ref().unwrap().len(), 100);
942    }
943
944    #[test]
945    fn test_try_extension_valid() {
946        use serde_json::json;
947
948        let schema = Schema::object().try_extension("x-custom", json!({"rule": "end > start"}));
949
950        assert!(schema.is_ok());
951        let schema = schema.unwrap();
952        assert!(schema.extensions.as_ref().unwrap().contains_key("x-custom"));
953    }
954
955    #[test]
956    fn test_try_extension_multiple() {
957        use serde_json::json;
958
959        let schema = Schema::object()
960            .try_extension("x-first", json!("value1"))
961            .and_then(|s| s.try_extension("x-second", json!("value2")));
962
963        assert!(schema.is_ok());
964        let schema = schema.unwrap();
965        let exts = schema.extensions.as_ref().unwrap();
966        assert_eq!(exts.len(), 2);
967    }
968
969    // Composition edge cases
970    #[test]
971    fn test_any_of_empty() {
972        let schema = Schema::any_of(vec![]);
973
974        assert!(schema.any_of.is_some());
975        assert!(schema.any_of.as_ref().unwrap().is_empty());
976    }
977
978    #[test]
979    fn test_all_of_empty() {
980        let schema = Schema::all_of(vec![]);
981
982        assert!(schema.all_of.is_some());
983        assert!(schema.all_of.as_ref().unwrap().is_empty());
984    }
985
986    #[test]
987    fn test_one_of_empty() {
988        let schema = Schema::one_of(vec![]);
989
990        assert!(schema.one_of.is_some());
991        assert!(schema.one_of.as_ref().unwrap().is_empty());
992    }
993
994    #[test]
995    fn test_nested_composition() {
996        let schema = Schema::any_of(vec![
997            Schema::all_of(vec![Schema::string(), Schema::integer()]),
998            Schema::one_of(vec![Schema::boolean(), Schema::number()]),
999        ]);
1000
1001        assert!(schema.any_of.is_some());
1002        let any_of = schema.any_of.as_ref().unwrap();
1003        assert!(any_of[0].all_of.is_some());
1004        assert!(any_of[1].one_of.is_some());
1005    }
1006
1007    // Builder chaining edge cases
1008    #[test]
1009    fn test_multiple_format_calls() {
1010        let schema = Schema::string().format("email").format("hostname");
1011
1012        // Last format wins
1013        assert_eq!(schema.format, Some("hostname".to_string()));
1014    }
1015
1016    #[test]
1017    fn test_multiple_description_calls() {
1018        let schema = Schema::string()
1019            .description("First description")
1020            .description("Second description");
1021
1022        assert_eq!(schema.description, Some("Second description".to_string()));
1023    }
1024
1025    #[test]
1026    fn test_multiple_min_max_calls() {
1027        let schema = Schema::integer()
1028            .minimum(0)
1029            .maximum(100)
1030            .minimum(10)
1031            .maximum(50);
1032
1033        // Last values win
1034        assert_eq!(schema.minimum, Some(10.0));
1035        assert_eq!(schema.maximum, Some(50.0));
1036    }
1037
1038    // Reference edge cases
1039    #[test]
1040    fn test_reference_with_path() {
1041        let schema = Schema::reference("nested/Type");
1042
1043        assert_eq!(
1044            schema.reference,
1045            Some("#/components/schemas/nested/Type".to_string())
1046        );
1047    }
1048
1049    #[test]
1050    fn test_reference_empty_name() {
1051        let schema = Schema::reference("");
1052
1053        assert_eq!(schema.reference, Some("#/components/schemas/".to_string()));
1054    }
1055
1056    // Complex property building
1057    #[test]
1058    fn test_deep_object_nesting() {
1059        let schema = Schema::object()
1060            .property(
1061                "level1",
1062                Schema::object().property(
1063                    "level2",
1064                    Schema::object().property("level3", Schema::string()),
1065                ),
1066            )
1067            .required(&["level1"]);
1068
1069        let props = schema.properties.as_ref().unwrap();
1070        assert!(props.contains_key("level1"));
1071    }
1072
1073    #[test]
1074    fn test_array_of_objects() {
1075        let item_schema = Schema::object()
1076            .property("id", Schema::integer())
1077            .property("name", Schema::string())
1078            .required(&["id", "name"]);
1079
1080        let schema = Schema::array(item_schema).min_items(0).max_items(100);
1081
1082        assert!(schema.items.is_some());
1083        assert_eq!(schema.min_items, Some(0));
1084    }
1085
1086    // Default trait
1087    #[test]
1088    fn test_schema_default_trait() {
1089        let schema: Schema = Default::default();
1090
1091        assert!(schema.schema_type.is_none());
1092        assert!(schema.properties.is_none());
1093    }
1094
1095    // Serialization with all fields
1096    #[test]
1097    fn test_full_schema_serialization() {
1098        use serde_json::json;
1099
1100        let schema = Schema::object()
1101            .property("id", Schema::string().read_only(true))
1102            .property("name", Schema::string().min_length(1).max_length(100))
1103            .property("score", Schema::number().minimum(0.0).maximum(100.0))
1104            .property("tags", Schema::array(Schema::string()).unique_items(true))
1105            .required(&["name"])
1106            .description("A user object")
1107            .deprecated(false)
1108            .example(json!({"name": "Alice", "score": 95.5}));
1109
1110        let json_str = serde_json::to_string(&schema).unwrap();
1111        assert!(json_str.contains("\"description\""));
1112        assert!(json_str.contains("\"readOnly\""));
1113    }
1114}