Skip to main content

volga_open_api/
schema.rs

1//! Types and utilities for OpenAPI schema.
2
3use serde::{
4    Deserialize, Deserializer, Serialize,
5    de::{DeserializeSeed, Error as DeError, IntoDeserializer, MapAccess, SeqAccess, Visitor},
6};
7use serde_json::{Map, Number, Value, json};
8use std::{
9    cmp::Ordering,
10    collections::{BTreeMap, BTreeSet},
11};
12
13/// Represents OpenAPI schema.
14#[derive(Clone, Debug, Serialize, Deserialize)]
15pub struct OpenApiSchema {
16    #[serde(rename = "$ref", skip_serializing_if = "Option::is_none")]
17    pub(super) schema_ref: Option<String>,
18
19    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
20    pub(super) schema_type: Option<String>,
21
22    #[serde(skip_serializing_if = "Option::is_none")]
23    pub(super) format: Option<String>,
24
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub(super) title: Option<String>,
27
28    #[serde(skip_serializing_if = "Option::is_none")]
29    pub(super) properties: Option<BTreeMap<String, OpenApiSchema>>,
30
31    #[serde(skip_serializing_if = "Option::is_none")]
32    pub(super) required: Option<Vec<String>>,
33
34    #[serde(
35        rename = "additionalProperties",
36        skip_serializing_if = "Option::is_none"
37    )]
38    pub(super) additional_properties: Option<bool>,
39
40    #[serde(skip_serializing_if = "Option::is_none")]
41    pub(super) items: Option<Box<OpenApiSchema>>,
42
43    #[serde(skip_serializing_if = "Option::is_none")]
44    pub(super) nullable: Option<bool>,
45
46    #[serde(rename = "minLength", skip_serializing_if = "Option::is_none")]
47    pub(super) min_length: Option<usize>,
48
49    #[serde(rename = "maxLength", skip_serializing_if = "Option::is_none")]
50    pub(super) max_length: Option<usize>,
51
52    #[serde(rename = "minItems", skip_serializing_if = "Option::is_none")]
53    pub(super) min_items: Option<usize>,
54
55    #[serde(rename = "maxItems", skip_serializing_if = "Option::is_none")]
56    pub(super) max_items: Option<usize>,
57
58    #[serde(rename = "minProperties", skip_serializing_if = "Option::is_none")]
59    pub(super) min_properties: Option<usize>,
60
61    #[serde(rename = "maxProperties", skip_serializing_if = "Option::is_none")]
62    pub(super) max_properties: Option<usize>,
63
64    #[serde(skip_serializing_if = "Option::is_none")]
65    pub(super) minimum: Option<Number>,
66
67    #[serde(skip_serializing_if = "Option::is_none")]
68    pub(super) maximum: Option<Number>,
69}
70
71/// A constraint bound to the field it describes
72#[derive(Clone, Debug, PartialEq)]
73pub struct FieldConstraint {
74    /// The name of the field, as it appears on the wire
75    pub field: String,
76
77    /// What the constraint says
78    pub constraint: SchemaConstraint,
79}
80
81impl FieldConstraint {
82    /// Creates a new [`FieldConstraint`] for `field`
83    pub fn new(field: impl Into<String>, constraint: SchemaConstraint) -> Self {
84        Self {
85            field: field.into(),
86            constraint,
87        }
88    }
89}
90
91/// A constraint on a schema, named the way OpenAPI names it.
92#[derive(Clone, Debug, PartialEq)]
93#[non_exhaustive]
94pub enum SchemaConstraint {
95    /// A lower bound on the size of the value, published under the keyword its schema type
96    /// calls for: `minLength` for a string, `minItems` for an array, `minProperties` for an
97    /// object. The keywords are not interchangeable - one published against the wrong type
98    /// reads as no constraint at all - and only the schema knows which type this is.
99    MinSize(usize),
100    /// An upper bound on the size of the value; see [`SchemaConstraint::MinSize`]
101    MaxSize(usize),
102    /// `minimum`
103    Minimum(Number),
104    /// `maximum`
105    Maximum(Number),
106    /// The field is an object, whose own fields carry these
107    Nested(Vec<FieldConstraint>),
108    /// The field is an array, whose elements carry these
109    Each(Vec<FieldConstraint>),
110}
111
112/// Keeps the tighter of two lower bounds.
113///
114/// A field carrying a rule twice is checked against both at runtime, so the published bound
115/// is their intersection - which also makes the order the rules arrive in irrelevant.
116fn keep_greatest(slot: &mut Option<usize>, value: usize) {
117    *slot = Some(match *slot {
118        Some(current) => current.max(value),
119        None => value,
120    });
121}
122
123/// Keeps the tighter of two upper bounds; see [`keep_greatest`]
124fn keep_least(slot: &mut Option<usize>, value: usize) {
125    *slot = Some(match *slot {
126        Some(current) => current.min(value),
127        None => value,
128    });
129}
130
131/// Keeps the tighter of two numeric lower bounds; see [`keep_greatest`]
132fn keep_greatest_number(slot: &mut Option<Number>, value: &Number) {
133    let replace = match slot.as_ref() {
134        Some(current) => compare_numbers(value, current) == Some(Ordering::Greater),
135        None => true,
136    };
137    if replace {
138        *slot = Some(value.clone());
139    }
140}
141
142/// Keeps the tighter of two numeric upper bounds; see [`keep_greatest`]
143fn keep_least_number(slot: &mut Option<Number>, value: &Number) {
144    let replace = match slot.as_ref() {
145        Some(current) => compare_numbers(value, current) == Some(Ordering::Less),
146        None => true,
147    };
148    if replace {
149        *slot = Some(value.clone());
150    }
151}
152
153/// Orders two JSON numbers, exactly wherever both are whole
154fn compare_numbers(left: &Number, right: &Number) -> Option<Ordering> {
155    if let (Some(left), Some(right)) = (left.as_u64(), right.as_u64()) {
156        return Some(left.cmp(&right));
157    }
158    if let (Some(left), Some(right)) = (left.as_i64(), right.as_i64()) {
159        return Some(left.cmp(&right));
160    }
161    left.as_f64()?.partial_cmp(&right.as_f64()?)
162}
163
164/// The OpenAPI name for a number carried as an `f32` (registered `format`)
165const FLOAT_FORMAT: &str = "float";
166
167/// The OpenAPI name for a number carried as an `f64`
168const DOUBLE_FORMAT: &str = "double";
169
170/// The pair of OpenAPI keywords that bounds the size of one kind of schema
171enum SizeKeyword {
172    /// `minLength` / `maxLength`
173    Length,
174    /// `minItems` / `maxItems`
175    Items,
176    /// `minProperties` / `maxProperties`
177    Properties,
178}
179
180impl OpenApiSchema {
181    /// An entirely unset schema, which every constructor below starts from
182    fn empty() -> Self {
183        Self {
184            schema_ref: None,
185            schema_type: None,
186            format: None,
187            title: None,
188            properties: None,
189            required: None,
190            additional_properties: None,
191            items: None,
192            nullable: None,
193            min_length: None,
194            max_length: None,
195            min_items: None,
196            max_items: None,
197            min_properties: None,
198            max_properties: None,
199            minimum: None,
200            maximum: None,
201        }
202    }
203
204    /// Applies a constraint to this schema
205    pub fn with_constraint(mut self, constraint: SchemaConstraint) -> Self {
206        self.apply_constraint(&constraint);
207        self
208    }
209
210    /// Applies a constraint to this schema in place.
211    ///
212    /// A constraint describing a shape this schema does not have is dropped rather than
213    /// forced on: `Nested` needs properties to descend into, `Each` an element schema.
214    pub(super) fn apply_constraint(&mut self, constraint: &SchemaConstraint) {
215        match constraint {
216            SchemaConstraint::MinSize(value) => match self.size_keyword() {
217                SizeKeyword::Items => keep_greatest(&mut self.min_items, *value),
218                SizeKeyword::Properties => keep_greatest(&mut self.min_properties, *value),
219                SizeKeyword::Length => keep_greatest(&mut self.min_length, *value),
220            },
221            SchemaConstraint::MaxSize(value) => match self.size_keyword() {
222                SizeKeyword::Items => keep_least(&mut self.max_items, *value),
223                SizeKeyword::Properties => keep_least(&mut self.max_properties, *value),
224                SizeKeyword::Length => keep_least(&mut self.max_length, *value),
225            },
226            SchemaConstraint::Minimum(value) => {
227                let value = self.narrow(value);
228                keep_greatest_number(&mut self.minimum, &value);
229            }
230            SchemaConstraint::Maximum(value) => {
231                let value = self.narrow(value);
232                keep_least_number(&mut self.maximum, &value);
233            }
234            SchemaConstraint::Nested(fields) => self.apply_field_constraints(fields),
235            SchemaConstraint::Each(fields) => {
236                if let Some(items) = self.items.as_mut() {
237                    items.apply_field_constraints(fields);
238                }
239            }
240        }
241    }
242
243    /// Reads a bound at the width this schema is compared at.
244    ///
245    /// A `0.7` written against an `f32` field is not the `0.7` its digits spell - the check
246    /// rounds it to the nearer `f32` - and the schema is the only place that knows the width,
247    /// since the field's type may be spelled through an alias. Rounding here is the same
248    /// round-to-nearest the literal went through, so the two land on the same number.
249    fn narrow(&self, value: &Number) -> Number {
250        // Only `f32` moves a bound: an `f64` field is compared against the number the
251        // digits already spell, and an integer bound cannot reach a float field at all -
252        // `f64: PartialOrd<{integer}>` does not exist, so such a rule fails to compile
253        let narrowed = match self.format.as_deref() {
254            Some(FLOAT_FORMAT) => value.as_f64().map(|value| f64::from(value as f32)),
255            _ => None,
256        };
257
258        narrowed
259            .and_then(Number::from_f64)
260            .unwrap_or_else(|| value.clone())
261    }
262
263    /// Reports which pair of size keywords describes this schema.
264    ///
265    /// A schema that names no type is taken for a string, which is what a size rule is
266    /// written against unless the field says otherwise.
267    fn size_keyword(&self) -> SizeKeyword {
268        match self.schema_type.as_deref() {
269            Some("array") => SizeKeyword::Items,
270            Some("object") => SizeKeyword::Properties,
271            _ => SizeKeyword::Length,
272        }
273    }
274
275    /// Applies the constraints of the fields this schema declares as properties.
276    ///
277    /// A field this schema does not describe is ignored.
278    pub(super) fn apply_field_constraints(&mut self, constraints: &[FieldConstraint]) {
279        let Some(properties) = self.properties.as_mut() else {
280            return;
281        };
282        for constraint in constraints {
283            if let Some(property) = properties.get_mut(&constraint.field) {
284                property.apply_constraint(&constraint.constraint);
285            }
286        }
287    }
288
289    /// Generates schema for object field
290    pub fn object() -> Self {
291        Self {
292            schema_type: Some("object".to_string()),
293            ..Self::empty()
294        }
295    }
296
297    /// Generates schema for string field
298    pub fn string() -> Self {
299        Self {
300            schema_type: Some("string".to_string()),
301            ..Self::empty()
302        }
303    }
304
305    /// Generates schema for integer field
306    pub fn integer() -> Self {
307        Self {
308            schema_type: Some("integer".to_string()),
309            ..Self::empty()
310        }
311    }
312
313    /// Generates schema for number field
314    pub fn number() -> Self {
315        Self {
316            schema_type: Some("number".to_string()),
317            ..Self::empty()
318        }
319    }
320
321    /// Generates schema for boolean field
322    pub fn boolean() -> Self {
323        Self {
324            schema_type: Some("boolean".to_string()),
325            ..Self::empty()
326        }
327    }
328
329    /// Generates schema for binary field
330    pub fn binary() -> Self {
331        Self {
332            schema_type: Some("string".to_string()),
333            format: Some("binary".to_string()),
334            ..Self::empty()
335        }
336    }
337
338    /// Generates schema for multipart form data
339    pub fn multipart() -> Self {
340        Self::object()
341            .with_property("file", OpenApiSchema::binary())
342            .with_property("meta", OpenApiSchema::string())
343            .with_required(["file"])
344    }
345
346    /// Generates schema for an array of items
347    pub fn array(items: OpenApiSchema) -> Self {
348        Self {
349            schema_type: Some("array".to_string()),
350            items: Some(Box::new(items)),
351            ..Self::empty()
352        }
353    }
354
355    /// Generates schema reference
356    pub fn reference(name: &str) -> Self {
357        Self {
358            schema_ref: Some(format!("#/components/schemas/{name}")),
359            ..Self::empty()
360        }
361    }
362
363    /// Generates schema from example
364    pub fn from_example(example: &Value) -> Self {
365        match example {
366            Value::Null => OpenApiSchema::object().nullable(),
367            Value::Bool(_) => OpenApiSchema::boolean(),
368            Value::Number(number) => {
369                if number.is_i64() || number.is_u64() {
370                    OpenApiSchema::integer()
371                } else {
372                    OpenApiSchema::number()
373                }
374            }
375            Value::String(_) => OpenApiSchema::string(),
376            Value::Array(items) => {
377                let item_schema = items
378                    .first()
379                    .map(OpenApiSchema::from_example)
380                    .unwrap_or_else(OpenApiSchema::object);
381                OpenApiSchema::array(item_schema)
382            }
383            Value::Object(map) => {
384                let mut schema = OpenApiSchema::object();
385                let mut required = Vec::new();
386                for (key, value) in map {
387                    schema = schema.with_property(key.clone(), OpenApiSchema::from_example(value));
388                    required.push(key.clone());
389                }
390                schema.with_required(required)
391            }
392        }
393    }
394
395    /// Makes schema nullable
396    pub fn nullable(mut self) -> Self {
397        self.nullable = Some(true);
398        self
399    }
400
401    /// Sets the title
402    pub fn with_title(mut self, title: impl Into<String>) -> Self {
403        self.title = Some(title.into());
404        self
405    }
406
407    /// Sets the property schema
408    pub fn with_property(mut self, name: impl Into<String>, schema: OpenApiSchema) -> Self {
409        self.properties
410            .get_or_insert_with(BTreeMap::new)
411            .insert(name.into(), schema);
412        self
413    }
414
415    /// Sets the format
416    pub fn with_format(mut self, fmt: impl Into<String>) -> Self {
417        self.format = Some(fmt.into());
418        self
419    }
420
421    /// Sets the required fields for schema
422    pub fn with_required<I, T>(mut self, required: I) -> Self
423    where
424        I: IntoIterator<Item = T>,
425        T: Into<String>,
426    {
427        self.required = Some(required.into_iter().map(Into::into).collect());
428        self
429    }
430
431    /// Returns `true` if this schema is a reference
432    pub fn is_ref(&self) -> bool {
433        self.schema_ref.is_some()
434    }
435
436    pub(super) fn collect_component_refs(&self, out: &mut BTreeSet<String>) {
437        if let Some(name) = self.component_ref_name()
438            && out.insert(name.clone())
439        {
440            // refs are terminal here; recursive expansion happens via component lookup.
441        }
442
443        if let Some(properties) = &self.properties {
444            for schema in properties.values() {
445                schema.collect_component_refs(out);
446            }
447        }
448
449        if let Some(items) = &self.items {
450            items.collect_component_refs(out);
451        }
452    }
453
454    pub(super) fn component_ref_name(&self) -> Option<String> {
455        const PREFIX: &str = "#/components/schemas/";
456        self.schema_ref
457            .as_deref()
458            .and_then(|r| r.strip_prefix(PREFIX))
459            .map(str::to_string)
460    }
461}
462
463impl Default for OpenApiSchema {
464    fn default() -> Self {
465        Self::object()
466    }
467}
468
469#[derive(Debug)]
470pub(super) struct ProbeError(String);
471
472impl std::fmt::Display for ProbeError {
473    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
474        f.write_str(&self.0)
475    }
476}
477impl std::error::Error for ProbeError {}
478impl DeError for ProbeError {
479    fn custom<M: std::fmt::Display>(msg: M) -> Self {
480        ProbeError(msg.to_string())
481    }
482}
483
484pub(super) struct Probe {
485    root: Option<(OpenApiSchema, Value)>,
486}
487
488impl Probe {
489    pub(super) fn new() -> Self {
490        Self { root: None }
491    }
492
493    pub(super) fn finish(self) -> Option<(OpenApiSchema, Value)> {
494        self.root
495    }
496}
497
498impl<'de> Deserializer<'de> for &mut Probe {
499    type Error = ProbeError;
500
501    fn deserialize_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
502    where
503        V: Visitor<'de>,
504    {
505        visitor.visit_unit()
506    }
507
508    fn deserialize_bool<V>(self, visitor: V) -> Result<V::Value, Self::Error>
509    where
510        V: Visitor<'de>,
511    {
512        self.root = Some((OpenApiSchema::boolean(), Value::Bool(false)));
513        visitor.visit_bool(false)
514    }
515
516    fn deserialize_i8<V>(self, visitor: V) -> Result<V::Value, Self::Error>
517    where
518        V: Visitor<'de>,
519    {
520        self.root = Some((OpenApiSchema::integer(), Value::Number(0.into())));
521        visitor.visit_i8(0)
522    }
523
524    fn deserialize_u8<V>(self, visitor: V) -> Result<V::Value, Self::Error>
525    where
526        V: Visitor<'de>,
527    {
528        self.root = Some((OpenApiSchema::integer(), Value::Number(0.into())));
529        visitor.visit_u8(0)
530    }
531
532    fn deserialize_i16<V>(self, visitor: V) -> Result<V::Value, Self::Error>
533    where
534        V: Visitor<'de>,
535    {
536        self.root = Some((OpenApiSchema::integer(), Value::Number(0.into())));
537        visitor.visit_i16(0)
538    }
539
540    fn deserialize_u16<V>(self, visitor: V) -> Result<V::Value, Self::Error>
541    where
542        V: Visitor<'de>,
543    {
544        self.root = Some((OpenApiSchema::integer(), Value::Number(0.into())));
545        visitor.visit_u16(0)
546    }
547
548    fn deserialize_i32<V>(self, visitor: V) -> Result<V::Value, Self::Error>
549    where
550        V: Visitor<'de>,
551    {
552        self.root = Some((OpenApiSchema::integer(), Value::Number(0.into())));
553        visitor.visit_i32(0)
554    }
555
556    fn deserialize_u32<V>(self, visitor: V) -> Result<V::Value, Self::Error>
557    where
558        V: Visitor<'de>,
559    {
560        self.root = Some((OpenApiSchema::integer(), Value::Number(0.into())));
561        visitor.visit_u32(0)
562    }
563
564    fn deserialize_f32<V>(self, visitor: V) -> Result<V::Value, Self::Error>
565    where
566        V: Visitor<'de>,
567    {
568        self.root = Some((
569            OpenApiSchema::number().with_format(FLOAT_FORMAT),
570            json!(0.0),
571        ));
572        visitor.visit_f32(0.0)
573    }
574
575    fn deserialize_i64<V>(self, visitor: V) -> Result<V::Value, Self::Error>
576    where
577        V: Visitor<'de>,
578    {
579        self.root = Some((OpenApiSchema::integer(), Value::Number(0.into())));
580        visitor.visit_i64(0)
581    }
582
583    fn deserialize_u64<V>(self, visitor: V) -> Result<V::Value, Self::Error>
584    where
585        V: Visitor<'de>,
586    {
587        self.root = Some((OpenApiSchema::integer(), Value::Number(0.into())));
588        visitor.visit_u64(0)
589    }
590
591    fn deserialize_i128<V>(self, visitor: V) -> Result<V::Value, Self::Error>
592    where
593        V: Visitor<'de>,
594    {
595        self.root = Some((OpenApiSchema::integer(), Value::Number(0.into())));
596        visitor.visit_i128(0)
597    }
598
599    fn deserialize_u128<V>(self, visitor: V) -> Result<V::Value, Self::Error>
600    where
601        V: Visitor<'de>,
602    {
603        self.root = Some((OpenApiSchema::integer(), Value::Number(0.into())));
604        visitor.visit_u128(0)
605    }
606
607    fn deserialize_f64<V>(self, visitor: V) -> Result<V::Value, Self::Error>
608    where
609        V: Visitor<'de>,
610    {
611        self.root = Some((
612            OpenApiSchema::number().with_format(DOUBLE_FORMAT),
613            json!(0.0),
614        ));
615        visitor.visit_f64(0.0)
616    }
617
618    fn deserialize_str<V>(self, visitor: V) -> Result<V::Value, Self::Error>
619    where
620        V: Visitor<'de>,
621    {
622        self.root = Some((OpenApiSchema::string(), Value::String(String::new())));
623        visitor.visit_borrowed_str("")
624    }
625
626    fn deserialize_string<V>(self, visitor: V) -> Result<V::Value, Self::Error>
627    where
628        V: Visitor<'de>,
629    {
630        self.root = Some((OpenApiSchema::string(), Value::String(String::new())));
631        visitor.visit_string(String::new())
632    }
633
634    fn deserialize_option<V>(self, visitor: V) -> Result<V::Value, Self::Error>
635    where
636        V: Visitor<'de>,
637    {
638        // The inner type is probed by this very `Probe`: forwarding it to `deserialize_any`
639        // instead would answer every inner type with a unit, which a `String` (or any other
640        // typed) visitor rejects - and one rejected field drops the whole schema.
641        let out = visitor.visit_some(&mut *self)?;
642
643        if let Some((schema, example)) = self.root.take() {
644            self.root = Some((schema.nullable(), example));
645        } else {
646            self.root = Some((OpenApiSchema::object().nullable(), Value::Null));
647        }
648
649        Ok(out)
650    }
651
652    fn deserialize_seq<V>(self, visitor: V) -> Result<V::Value, Self::Error>
653    where
654        V: Visitor<'de>,
655    {
656        visitor.visit_seq(SeqProbeAccess {
657            probe: self,
658            yielded: false,
659        })
660    }
661
662    fn deserialize_map<V>(self, visitor: V) -> Result<V::Value, Self::Error>
663    where
664        V: Visitor<'de>,
665    {
666        self.root = Some((OpenApiSchema::object(), Value::Object(Map::new())));
667        visitor.visit_map(EmptyMapAccess)
668    }
669
670    fn deserialize_struct<V>(
671        self,
672        _name: &'static str,
673        fields: &'static [&'static str],
674        visitor: V,
675    ) -> Result<V::Value, Self::Error>
676    where
677        V: Visitor<'de>,
678    {
679        let access = StructProbeAccess {
680            fields,
681            idx: 0,
682            parent: self,
683            obj_schema: OpenApiSchema::object(),
684            example: Map::new(),
685            required: Vec::new(),
686        };
687
688        visitor.visit_map(access)
689    }
690
691    fn deserialize_enum<V>(
692        self,
693        _name: &'static str,
694        _variants: &'static [&'static str],
695        _visitor: V,
696    ) -> Result<V::Value, Self::Error>
697    where
698        V: Visitor<'de>,
699    {
700        Err(ProbeError(
701            "enums are not supported for automatic schema inference".into(),
702        ))
703    }
704
705    serde::forward_to_deserialize_any! {
706        char bytes byte_buf unit unit_struct
707        newtype_struct tuple tuple_struct identifier ignored_any
708    }
709}
710
711struct SeqProbeAccess<'a> {
712    probe: &'a mut Probe,
713    yielded: bool,
714}
715
716impl<'de, 'a> SeqAccess<'de> for SeqProbeAccess<'a> {
717    type Error = ProbeError;
718
719    fn next_element_seed<T>(&mut self, seed: T) -> Result<Option<T::Value>, Self::Error>
720    where
721        T: DeserializeSeed<'de>,
722    {
723        if self.yielded {
724            if let Some((item_schema, item_example)) = self.probe.root.take() {
725                self.probe.root = Some((
726                    OpenApiSchema::array(item_schema),
727                    Value::Array(vec![item_example]),
728                ));
729            } else {
730                self.probe.root = Some((
731                    OpenApiSchema::array(OpenApiSchema::object()),
732                    Value::Array(vec![]),
733                ));
734            }
735            return Ok(None);
736        }
737
738        self.yielded = true;
739        let v = seed.deserialize(&mut *self.probe)?;
740        Ok(Some(v))
741    }
742}
743
744struct StructProbeAccess<'a> {
745    fields: &'static [&'static str],
746    idx: usize,
747    parent: &'a mut Probe,
748
749    obj_schema: OpenApiSchema,
750    example: Map<String, Value>,
751    required: Vec<String>,
752}
753
754impl<'de, 'a> MapAccess<'de> for StructProbeAccess<'a> {
755    type Error = ProbeError;
756
757    fn next_key_seed<K>(&mut self, seed: K) -> Result<Option<K::Value>, Self::Error>
758    where
759        K: DeserializeSeed<'de>,
760    {
761        if self.idx >= self.fields.len() {
762            let schema = self.obj_schema.clone().with_required(self.required.clone());
763            let ex = Value::Object(std::mem::take(&mut self.example));
764            self.parent.root = Some((schema, ex));
765            return Ok(None);
766        }
767
768        let key = self.fields[self.idx];
769        let key_de = key.into_deserializer();
770        let k = seed.deserialize(key_de)?;
771        Ok(Some(k))
772    }
773
774    fn next_value_seed<V>(&mut self, seed: V) -> Result<V::Value, Self::Error>
775    where
776        V: DeserializeSeed<'de>,
777    {
778        let field = self.fields[self.idx];
779        self.idx += 1;
780
781        let v = seed.deserialize(&mut *self.parent)?;
782
783        if let Some((field_schema, field_example)) = self.parent.root.take() {
784            let is_required = field_schema.nullable != Some(true) && field_example != Value::Null;
785            self.obj_schema = self
786                .obj_schema
787                .clone()
788                .with_property(field.to_string(), field_schema);
789
790            self.example.insert(field.to_string(), field_example);
791            if is_required {
792                self.required.push(field.to_string());
793            }
794        } else {
795            self.obj_schema = self
796                .obj_schema
797                .clone()
798                .with_property(field.to_string(), OpenApiSchema::object());
799
800            self.example.insert(field.to_string(), Value::Null);
801        }
802
803        Ok(v)
804    }
805}
806
807struct EmptyMapAccess;
808impl<'de> MapAccess<'de> for EmptyMapAccess {
809    type Error = ProbeError;
810
811    fn next_key_seed<K>(&mut self, _seed: K) -> Result<Option<K::Value>, Self::Error>
812    where
813        K: DeserializeSeed<'de>,
814    {
815        Ok(None)
816    }
817
818    fn next_value_seed<V>(&mut self, _seed: V) -> Result<V::Value, Self::Error>
819    where
820        V: DeserializeSeed<'de>,
821    {
822        Err(ProbeError("no values".into()))
823    }
824}
825
826#[cfg(test)]
827mod tests {
828    use super::*;
829    use std::collections::BTreeSet;
830
831    #[test]
832    fn schema_from_example_object_keeps_properties() {
833        let schema = OpenApiSchema::from_example(&serde_json::json!({
834            "name": "Alice",
835            "age": 42
836        }));
837
838        let props = schema.properties.expect("schema properties");
839        assert_eq!(
840            props.get("name").expect("name").schema_type.as_deref(),
841            Some("string")
842        );
843        assert_eq!(
844            props.get("age").expect("age").schema_type.as_deref(),
845            Some("integer")
846        );
847    }
848
849    #[test]
850    fn schema_from_example_handles_null_and_arrays() {
851        let null_schema = OpenApiSchema::from_example(&Value::Null);
852        assert_eq!(null_schema.schema_type.as_deref(), Some("object"));
853        assert_eq!(null_schema.nullable, Some(true));
854
855        let array_schema = OpenApiSchema::from_example(&serde_json::json!([1, 2, 3]));
856        assert_eq!(array_schema.schema_type.as_deref(), Some("array"));
857        assert_eq!(
858            array_schema
859                .items
860                .expect("array items")
861                .schema_type
862                .as_deref(),
863            Some("integer")
864        );
865    }
866
867    #[test]
868    fn multipart_schema_contains_expected_fields() {
869        let schema = OpenApiSchema::multipart();
870        let props = schema.properties.expect("properties");
871
872        assert!(props.contains_key("file"));
873        assert!(props.contains_key("meta"));
874        assert_eq!(schema.required.expect("required"), vec!["file"]);
875    }
876
877    #[test]
878    fn schema_reference_marks_ref_and_path() {
879        let schema = OpenApiSchema::reference("Payload");
880        assert!(schema.is_ref());
881        assert_eq!(
882            schema.schema_ref.as_deref(),
883            Some("#/components/schemas/Payload")
884        );
885    }
886
887    #[test]
888    fn number_and_boolean_builders_set_expected_types() {
889        assert_eq!(
890            OpenApiSchema::number().schema_type.as_deref(),
891            Some("number")
892        );
893        assert_eq!(
894            OpenApiSchema::boolean().schema_type.as_deref(),
895            Some("boolean")
896        );
897    }
898
899    #[test]
900    fn with_format_sets_schema_format() {
901        let schema = OpenApiSchema::string().with_format("uuid");
902        assert_eq!(schema.format.as_deref(), Some("uuid"));
903    }
904
905    #[test]
906    fn default_schema_is_object() {
907        let schema = OpenApiSchema::default();
908        assert_eq!(schema.schema_type.as_deref(), Some("object"));
909    }
910
911    #[test]
912    fn probe_does_not_mark_option_fields_as_required() {
913        #[derive(Deserialize)]
914        #[allow(dead_code)]
915        struct Input {
916            required_name: String,
917            optional_age: Option<()>,
918        }
919
920        let mut probe = Probe::new();
921        let _ = Input::deserialize(&mut probe);
922        let (schema, _) = probe.finish().expect("schema should be produced");
923
924        let required = schema.required.expect("required list");
925        assert!(required.contains(&"required_name".to_string()));
926        assert!(!required.contains(&"optional_age".to_string()));
927    }
928
929    #[test]
930    fn probe_supports_i32_fields_for_schema_inference() {
931        #[derive(Deserialize)]
932        #[allow(dead_code)]
933        struct NumericInput {
934            value: i32,
935        }
936
937        let mut probe = Probe::new();
938        let _ = NumericInput::deserialize(&mut probe);
939        let (schema, _) = probe.finish().expect("schema should be produced");
940
941        let props = schema.properties.expect("properties");
942        assert_eq!(props["value"].schema_type.as_deref(), Some("integer"));
943    }
944
945    #[test]
946    fn probe_supports_u32_fields_for_schema_inference() {
947        #[derive(Deserialize)]
948        #[allow(dead_code)]
949        struct NumericInput {
950            value: u32,
951        }
952
953        let mut probe = Probe::new();
954        let _ = NumericInput::deserialize(&mut probe);
955        let (schema, _) = probe.finish().expect("schema should be produced");
956
957        let props = schema.properties.expect("properties");
958        assert_eq!(props["value"].schema_type.as_deref(), Some("integer"));
959    }
960
961    #[test]
962    fn probe_supports_f32_fields_for_schema_inference() {
963        #[derive(Deserialize)]
964        #[allow(dead_code)]
965        struct NumericInput {
966            value: f32,
967        }
968
969        let mut probe = Probe::new();
970        let _ = NumericInput::deserialize(&mut probe);
971        let (schema, _) = probe.finish().expect("schema should be produced");
972
973        let props = schema.properties.expect("properties");
974        assert_eq!(props["value"].schema_type.as_deref(), Some("number"));
975    }
976
977    #[test]
978    fn probe_supports_i128_fields_for_schema_inference() {
979        #[derive(Deserialize)]
980        #[allow(dead_code)]
981        struct NumericInput {
982            value: i128,
983        }
984
985        let mut probe = Probe::new();
986        let _ = NumericInput::deserialize(&mut probe);
987        let (schema, _) = probe.finish().expect("schema should be produced");
988
989        let props = schema.properties.expect("properties");
990        assert_eq!(props["value"].schema_type.as_deref(), Some("integer"));
991    }
992
993    #[test]
994    fn probe_supports_u128_fields_for_schema_inference() {
995        #[derive(Deserialize)]
996        #[allow(dead_code)]
997        struct NumericInput {
998            value: u128,
999        }
1000
1001        let mut probe = Probe::new();
1002        let _ = NumericInput::deserialize(&mut probe);
1003        let (schema, _) = probe.finish().expect("schema should be produced");
1004
1005        let props = schema.properties.expect("properties");
1006        assert_eq!(props["value"].schema_type.as_deref(), Some("integer"));
1007    }
1008
1009    #[test]
1010    fn probe_supports_i16_fields_for_schema_inference() {
1011        #[derive(Deserialize)]
1012        #[allow(dead_code)]
1013        struct NumericInput {
1014            value: i16,
1015        }
1016
1017        let mut probe = Probe::new();
1018        let _ = NumericInput::deserialize(&mut probe);
1019        let (schema, _) = probe.finish().expect("schema should be produced");
1020
1021        let props = schema.properties.expect("properties");
1022        assert_eq!(props["value"].schema_type.as_deref(), Some("integer"));
1023    }
1024
1025    #[test]
1026    fn probe_supports_u16_fields_for_schema_inference() {
1027        #[derive(Deserialize)]
1028        #[allow(dead_code)]
1029        struct NumericInput {
1030            value: u16,
1031        }
1032
1033        let mut probe = Probe::new();
1034        let _ = NumericInput::deserialize(&mut probe);
1035        let (schema, _) = probe.finish().expect("schema should be produced");
1036
1037        let props = schema.properties.expect("properties");
1038        assert_eq!(props["value"].schema_type.as_deref(), Some("integer"));
1039    }
1040
1041    #[test]
1042    fn probe_supports_i8_fields_for_schema_inference() {
1043        #[derive(Deserialize)]
1044        #[allow(dead_code)]
1045        struct NumericInput {
1046            value: i8,
1047        }
1048
1049        let mut probe = Probe::new();
1050        let _ = NumericInput::deserialize(&mut probe);
1051        let (schema, _) = probe.finish().expect("schema should be produced");
1052
1053        let props = schema.properties.expect("properties");
1054        assert_eq!(props["value"].schema_type.as_deref(), Some("integer"));
1055    }
1056
1057    #[test]
1058    fn probe_supports_u8_fields_for_schema_inference() {
1059        #[derive(Deserialize)]
1060        #[allow(dead_code)]
1061        struct NumericInput {
1062            value: u8,
1063        }
1064
1065        let mut probe = Probe::new();
1066        let _ = NumericInput::deserialize(&mut probe);
1067        let (schema, _) = probe.finish().expect("schema should be produced");
1068
1069        let props = schema.properties.expect("properties");
1070        assert_eq!(props["value"].schema_type.as_deref(), Some("integer"));
1071    }
1072
1073    #[test]
1074    fn collect_component_refs_walks_nested_refs() {
1075        let schema = OpenApiSchema::object()
1076            .with_property("user", OpenApiSchema::reference("User"))
1077            .with_property(
1078                "items",
1079                OpenApiSchema::array(OpenApiSchema::reference("Item")),
1080            );
1081
1082        let mut refs = BTreeSet::new();
1083        schema.collect_component_refs(&mut refs);
1084
1085        assert!(refs.contains("User"));
1086        assert!(refs.contains("Item"));
1087        assert_eq!(refs.len(), 2);
1088    }
1089}