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::{
6        DeserializeSeed, Error as DeError, Expected, IntoDeserializer, MapAccess, SeqAccess,
7        Visitor,
8    },
9};
10use serde_json::{Map, Number, Value, json};
11use std::{
12    cmp::Ordering,
13    collections::{BTreeMap, BTreeSet},
14};
15
16/// Represents OpenAPI schema.
17#[derive(Clone, Debug, Serialize, Deserialize)]
18pub struct OpenApiSchema {
19    #[serde(rename = "$ref", skip_serializing_if = "Option::is_none")]
20    pub(super) schema_ref: Option<String>,
21
22    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
23    pub(super) schema_type: Option<String>,
24
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub(super) format: Option<String>,
27
28    #[serde(skip_serializing_if = "Option::is_none")]
29    pub(super) title: Option<String>,
30
31    #[serde(skip_serializing_if = "Option::is_none")]
32    pub(super) properties: Option<BTreeMap<String, OpenApiSchema>>,
33
34    #[serde(skip_serializing_if = "Option::is_none")]
35    pub(super) required: Option<Vec<String>>,
36
37    #[serde(
38        rename = "additionalProperties",
39        skip_serializing_if = "Option::is_none"
40    )]
41    pub(super) additional_properties: Option<bool>,
42
43    #[serde(skip_serializing_if = "Option::is_none")]
44    pub(super) items: Option<Box<OpenApiSchema>>,
45
46    #[serde(skip_serializing_if = "Option::is_none")]
47    pub(super) nullable: Option<bool>,
48
49    #[serde(rename = "minLength", skip_serializing_if = "Option::is_none")]
50    pub(super) min_length: Option<usize>,
51
52    #[serde(rename = "maxLength", skip_serializing_if = "Option::is_none")]
53    pub(super) max_length: Option<usize>,
54
55    #[serde(rename = "minItems", skip_serializing_if = "Option::is_none")]
56    pub(super) min_items: Option<usize>,
57
58    #[serde(rename = "maxItems", skip_serializing_if = "Option::is_none")]
59    pub(super) max_items: Option<usize>,
60
61    #[serde(rename = "minProperties", skip_serializing_if = "Option::is_none")]
62    pub(super) min_properties: Option<usize>,
63
64    #[serde(rename = "maxProperties", skip_serializing_if = "Option::is_none")]
65    pub(super) max_properties: Option<usize>,
66
67    #[serde(skip_serializing_if = "Option::is_none")]
68    pub(super) minimum: Option<Number>,
69
70    #[serde(skip_serializing_if = "Option::is_none")]
71    pub(super) maximum: Option<Number>,
72}
73
74/// A constraint bound to the field it describes
75#[derive(Clone, Debug, PartialEq)]
76pub struct FieldConstraint {
77    /// The name of the field, as it appears on the wire
78    pub field: String,
79
80    /// What the constraint says
81    pub constraint: SchemaConstraint,
82}
83
84impl FieldConstraint {
85    /// Creates a new [`FieldConstraint`] for `field`
86    pub fn new(field: impl Into<String>, constraint: SchemaConstraint) -> Self {
87        Self {
88            field: field.into(),
89            constraint,
90        }
91    }
92}
93
94/// A constraint on a schema, named the way OpenAPI names it.
95#[derive(Clone, Debug, PartialEq)]
96#[non_exhaustive]
97pub enum SchemaConstraint {
98    /// A lower bound on the size of the value, published under the keyword its schema type
99    /// calls for: `minLength` for a string, `minItems` for an array, `minProperties` for an
100    /// object. The keywords are not interchangeable - one published against the wrong type
101    /// reads as no constraint at all - and only the schema knows which type this is.
102    MinSize(usize),
103    /// An upper bound on the size of the value; see [`SchemaConstraint::MinSize`]
104    MaxSize(usize),
105    /// `minimum`
106    Minimum(Number),
107    /// `maximum`
108    Maximum(Number),
109    /// The field is an object, whose own fields carry these
110    Nested(Vec<FieldConstraint>),
111    /// The field is an array, whose elements carry these
112    Each(Vec<FieldConstraint>),
113}
114
115/// Keeps the tighter of two lower bounds.
116///
117/// A field carrying a rule twice is checked against both at runtime, so the published bound
118/// is their intersection - which also makes the order the rules arrive in irrelevant.
119fn keep_greatest(slot: &mut Option<usize>, value: usize) {
120    *slot = Some(match *slot {
121        Some(current) => current.max(value),
122        None => value,
123    });
124}
125
126/// Keeps the tighter of two upper bounds; see [`keep_greatest`]
127fn keep_least(slot: &mut Option<usize>, value: usize) {
128    *slot = Some(match *slot {
129        Some(current) => current.min(value),
130        None => value,
131    });
132}
133
134/// Keeps the tighter of two numeric lower bounds; see [`keep_greatest`]
135fn keep_greatest_number(slot: &mut Option<Number>, value: &Number) {
136    let replace = match slot.as_ref() {
137        Some(current) => compare_numbers(value, current) == Some(Ordering::Greater),
138        None => true,
139    };
140    if replace {
141        *slot = Some(value.clone());
142    }
143}
144
145/// Keeps the tighter of two numeric upper bounds; see [`keep_greatest`]
146fn keep_least_number(slot: &mut Option<Number>, value: &Number) {
147    let replace = match slot.as_ref() {
148        Some(current) => compare_numbers(value, current) == Some(Ordering::Less),
149        None => true,
150    };
151    if replace {
152        *slot = Some(value.clone());
153    }
154}
155
156/// Orders two JSON numbers, exactly wherever both are whole
157fn compare_numbers(left: &Number, right: &Number) -> Option<Ordering> {
158    if let (Some(left), Some(right)) = (left.as_u64(), right.as_u64()) {
159        return Some(left.cmp(&right));
160    }
161    if let (Some(left), Some(right)) = (left.as_i64(), right.as_i64()) {
162        return Some(left.cmp(&right));
163    }
164    left.as_f64()?.partial_cmp(&right.as_f64()?)
165}
166
167/// The OpenAPI name for a number carried as an `f32` (registered `format`)
168const FLOAT_FORMAT: &str = "float";
169
170/// The OpenAPI name for a number carried as an `f64`
171const DOUBLE_FORMAT: &str = "double";
172
173/// The pair of OpenAPI keywords that bounds the size of one kind of schema
174enum SizeKeyword {
175    /// `minLength` / `maxLength`
176    Length,
177    /// `minItems` / `maxItems`
178    Items,
179    /// `minProperties` / `maxProperties`
180    Properties,
181}
182
183impl OpenApiSchema {
184    /// An entirely unset schema, which every constructor below starts from - and, published
185    /// as it is, one that any value satisfies
186    pub(super) fn empty() -> Self {
187        Self {
188            schema_ref: None,
189            schema_type: None,
190            format: None,
191            title: None,
192            properties: None,
193            required: None,
194            additional_properties: None,
195            items: None,
196            nullable: None,
197            min_length: None,
198            max_length: None,
199            min_items: None,
200            max_items: None,
201            min_properties: None,
202            max_properties: None,
203            minimum: None,
204            maximum: None,
205        }
206    }
207
208    /// Applies a constraint to this schema
209    pub fn with_constraint(mut self, constraint: SchemaConstraint) -> Self {
210        self.apply_constraint(&constraint);
211        self
212    }
213
214    /// Applies a constraint to this schema in place.
215    ///
216    /// A constraint describing a shape this schema does not have is dropped rather than
217    /// forced on: `Nested` needs properties to descend into, `Each` an element schema.
218    pub(super) fn apply_constraint(&mut self, constraint: &SchemaConstraint) {
219        match constraint {
220            SchemaConstraint::MinSize(value) => match self.size_keyword() {
221                SizeKeyword::Items => keep_greatest(&mut self.min_items, *value),
222                SizeKeyword::Properties => keep_greatest(&mut self.min_properties, *value),
223                SizeKeyword::Length => keep_greatest(&mut self.min_length, *value),
224            },
225            SchemaConstraint::MaxSize(value) => match self.size_keyword() {
226                SizeKeyword::Items => keep_least(&mut self.max_items, *value),
227                SizeKeyword::Properties => keep_least(&mut self.max_properties, *value),
228                SizeKeyword::Length => keep_least(&mut self.max_length, *value),
229            },
230            SchemaConstraint::Minimum(value) => {
231                let value = self.narrow(value);
232                keep_greatest_number(&mut self.minimum, &value);
233            }
234            SchemaConstraint::Maximum(value) => {
235                let value = self.narrow(value);
236                keep_least_number(&mut self.maximum, &value);
237            }
238            SchemaConstraint::Nested(fields) => self.apply_field_constraints(fields),
239            SchemaConstraint::Each(fields) => {
240                if let Some(items) = self.items.as_mut() {
241                    items.apply_field_constraints(fields);
242                }
243            }
244        }
245    }
246
247    /// Reads a bound at the width this schema is compared at.
248    ///
249    /// A `0.7` written against an `f32` field is not the `0.7` its digits spell - the check
250    /// rounds it to the nearer `f32` - and the schema is the only place that knows the width,
251    /// since the field's type may be spelled through an alias. Rounding here is the same
252    /// round-to-nearest the literal went through, so the two land on the same number.
253    fn narrow(&self, value: &Number) -> Number {
254        // Only `f32` moves a bound: an `f64` field is compared against the number the
255        // digits already spell, and an integer bound cannot reach a float field at all -
256        // `f64: PartialOrd<{integer}>` does not exist, so such a rule fails to compile
257        let narrowed = match self.format.as_deref() {
258            Some(FLOAT_FORMAT) => value.as_f64().map(|value| f64::from(value as f32)),
259            _ => None,
260        };
261
262        narrowed
263            .and_then(Number::from_f64)
264            .unwrap_or_else(|| value.clone())
265    }
266
267    /// Reports which pair of size keywords describes this schema.
268    ///
269    /// A schema that names no type is taken for a string, which is what a size rule is
270    /// written against unless the field says otherwise.
271    fn size_keyword(&self) -> SizeKeyword {
272        match self.schema_type.as_deref() {
273            Some("array") => SizeKeyword::Items,
274            Some("object") => SizeKeyword::Properties,
275            _ => SizeKeyword::Length,
276        }
277    }
278
279    /// Applies the constraints of the fields this schema declares as properties.
280    ///
281    /// A field this schema does not describe is ignored.
282    pub(super) fn apply_field_constraints(&mut self, constraints: &[FieldConstraint]) {
283        let Some(properties) = self.properties.as_mut() else {
284            return;
285        };
286        for constraint in constraints {
287            if let Some(property) = properties.get_mut(&constraint.field) {
288                property.apply_constraint(&constraint.constraint);
289            }
290        }
291    }
292
293    /// Generates schema for object field
294    pub fn object() -> Self {
295        Self {
296            schema_type: Some("object".to_string()),
297            ..Self::empty()
298        }
299    }
300
301    /// Generates schema for string field
302    pub fn string() -> Self {
303        Self {
304            schema_type: Some("string".to_string()),
305            ..Self::empty()
306        }
307    }
308
309    /// Generates schema for integer field
310    pub fn integer() -> Self {
311        Self {
312            schema_type: Some("integer".to_string()),
313            ..Self::empty()
314        }
315    }
316
317    /// Generates schema for number field
318    pub fn number() -> Self {
319        Self {
320            schema_type: Some("number".to_string()),
321            ..Self::empty()
322        }
323    }
324
325    /// Generates schema for boolean field
326    pub fn boolean() -> Self {
327        Self {
328            schema_type: Some("boolean".to_string()),
329            ..Self::empty()
330        }
331    }
332
333    /// Generates schema for binary field
334    pub fn binary() -> Self {
335        Self {
336            schema_type: Some("string".to_string()),
337            format: Some("binary".to_string()),
338            ..Self::empty()
339        }
340    }
341
342    /// Generates schema for multipart form data
343    pub fn multipart() -> Self {
344        Self::object()
345            .with_property("file", OpenApiSchema::binary())
346            .with_property("meta", OpenApiSchema::string())
347            .with_required(["file"])
348    }
349
350    /// Generates schema for an array of items
351    pub fn array(items: OpenApiSchema) -> Self {
352        Self {
353            schema_type: Some("array".to_string()),
354            items: Some(Box::new(items)),
355            ..Self::empty()
356        }
357    }
358
359    /// Generates schema reference
360    pub fn reference(name: &str) -> Self {
361        Self {
362            schema_ref: Some(format!("#/components/schemas/{name}")),
363            ..Self::empty()
364        }
365    }
366
367    /// Generates schema from example
368    pub fn from_example(example: &Value) -> Self {
369        match example {
370            Value::Null => OpenApiSchema::object().nullable(),
371            Value::Bool(_) => OpenApiSchema::boolean(),
372            Value::Number(number) => {
373                if number.is_i64() || number.is_u64() {
374                    OpenApiSchema::integer()
375                } else {
376                    OpenApiSchema::number()
377                }
378            }
379            Value::String(_) => OpenApiSchema::string(),
380            Value::Array(items) => {
381                let item_schema = items
382                    .first()
383                    .map(OpenApiSchema::from_example)
384                    .unwrap_or_else(OpenApiSchema::object);
385                OpenApiSchema::array(item_schema)
386            }
387            Value::Object(map) => {
388                let mut schema = OpenApiSchema::object();
389                let mut required = Vec::new();
390                for (key, value) in map {
391                    schema = schema.with_property(key.clone(), OpenApiSchema::from_example(value));
392                    required.push(key.clone());
393                }
394                schema.with_required(required)
395            }
396        }
397    }
398
399    /// Makes schema nullable
400    pub fn nullable(mut self) -> Self {
401        self.nullable = Some(true);
402        self
403    }
404
405    /// Sets the title
406    pub fn with_title(mut self, title: impl Into<String>) -> Self {
407        self.title = Some(title.into());
408        self
409    }
410
411    /// Sets the property schema
412    pub fn with_property(mut self, name: impl Into<String>, schema: OpenApiSchema) -> Self {
413        self.properties
414            .get_or_insert_with(BTreeMap::new)
415            .insert(name.into(), schema);
416        self
417    }
418
419    /// Sets the format
420    pub fn with_format(mut self, fmt: impl Into<String>) -> Self {
421        self.format = Some(fmt.into());
422        self
423    }
424
425    /// Sets the required fields for schema
426    pub fn with_required<I, T>(mut self, required: I) -> Self
427    where
428        I: IntoIterator<Item = T>,
429        T: Into<String>,
430    {
431        self.required = Some(required.into_iter().map(Into::into).collect());
432        self
433    }
434
435    /// Returns `true` if this schema is a reference
436    pub fn is_ref(&self) -> bool {
437        self.schema_ref.is_some()
438    }
439
440    pub(super) fn collect_component_refs(&self, out: &mut BTreeSet<String>) {
441        if let Some(name) = self.component_ref_name()
442            && out.insert(name.clone())
443        {
444            // refs are terminal here; recursive expansion happens via component lookup.
445        }
446
447        if let Some(properties) = &self.properties {
448            for schema in properties.values() {
449                schema.collect_component_refs(out);
450            }
451        }
452
453        if let Some(items) = &self.items {
454            items.collect_component_refs(out);
455        }
456    }
457
458    pub(super) fn component_ref_name(&self) -> Option<String> {
459        const PREFIX: &str = "#/components/schemas/";
460        self.schema_ref
461            .as_deref()
462            .and_then(|r| r.strip_prefix(PREFIX))
463            .map(str::to_string)
464    }
465}
466
467impl Default for OpenApiSchema {
468    fn default() -> Self {
469        Self::object()
470    }
471}
472
473#[derive(Debug)]
474pub(super) struct ProbeError(String);
475
476impl std::fmt::Display for ProbeError {
477    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
478        f.write_str(&self.0)
479    }
480}
481impl std::error::Error for ProbeError {}
482impl DeError for ProbeError {
483    fn custom<M: std::fmt::Display>(msg: M) -> Self {
484        ProbeError(msg.to_string())
485    }
486}
487
488/// What probing a type found out about its shape
489///
490/// Made once per extractor described and taken apart right away, so the shape is not boxed.
491#[derive(Debug)]
492#[allow(clippy::large_enum_variant)]
493pub(super) enum Probed {
494    /// The shape of the type, and an example of it
495    Shape(OpenApiSchema, Value),
496    /// The type is, or contains, a struct serde reads as a map - see [`ReadAsMap`]
497    ReadAsMap(ReadAsMap),
498    /// Nothing: the type asked for something the probe does not describe, such as an enum
499    Nothing,
500}
501
502/// A struct serde reads as a map, whose fields the probe cannot see.
503///
504/// serde reads a struct with a `#[serde(flatten)]` field as a map, so that the keys the
505/// struct does not name itself can be collected for the flattened member - and a map does
506/// not say which keys it takes. No [`Deserializer`] can learn them, so the probe reports the
507/// struct instead of describing it as an object without properties, which is what it would
508/// otherwise look like.
509///
510/// Such a struct is told from an actual map by two things serde's derive gives it: its
511/// visitor expects `struct Name`, and it reads a key as a field identifier. Neither is
512/// enough alone. A map whose key type is an identifier of its own - a
513/// `#[serde(field_identifier)]` enum - reads its keys the same way, but its visitor
514/// expects `a map`; and a visitor says nothing of how the keys are read.
515///
516/// A struct that renames what it expects, with `#[serde(expecting = "..")]`, no longer says
517/// it is one, and is taken for the map it is read as. That is the safer mistake: taking a
518/// map for such a struct would describe whatever holds it as any value, while taking the
519/// struct for a map describes it as an object without properties, as it always was.
520#[derive(Clone, Debug, PartialEq, Eq)]
521pub(super) struct ReadAsMap {
522    /// What the struct's `Visitor` says it expects: `struct Name`, for a derived one
523    pub(super) expecting: String,
524    /// Whether the struct is the probed type itself, rather than one inside it
525    pub(super) at_root: bool,
526}
527
528pub(super) struct Probe {
529    root: Option<(OpenApiSchema, Value)>,
530    /// How far below the probed type the probe is: its fields are one level down
531    depth: usize,
532    /// The struct read as a map the probe stopped at, if it met one
533    read_as_map: Option<ReadAsMap>,
534}
535
536impl Probe {
537    pub(super) fn new() -> Self {
538        Self {
539            root: None,
540            depth: 0,
541            read_as_map: None,
542        }
543    }
544
545    pub(super) fn finish(self) -> Probed {
546        // A struct read as a map anywhere inside the type leaves its shape incomplete,
547        // whatever the probe gathered around it
548        if let Some(read_as_map) = self.read_as_map {
549            return Probed::ReadAsMap(read_as_map);
550        }
551        match self.root {
552            Some((schema, example)) => Probed::Shape(schema, example),
553            None => Probed::Nothing,
554        }
555    }
556
557    /// Probes a value one level below the current one
558    fn descend<R>(&mut self, probe: impl FnOnce(&mut Self) -> R) -> R {
559        self.depth += 1;
560        let value = probe(self);
561        self.depth -= 1;
562        value
563    }
564}
565
566impl<'de> Deserializer<'de> for &mut Probe {
567    type Error = ProbeError;
568
569    fn deserialize_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
570    where
571        V: Visitor<'de>,
572    {
573        visitor.visit_unit()
574    }
575
576    fn deserialize_bool<V>(self, visitor: V) -> Result<V::Value, Self::Error>
577    where
578        V: Visitor<'de>,
579    {
580        self.root = Some((OpenApiSchema::boolean(), Value::Bool(false)));
581        visitor.visit_bool(false)
582    }
583
584    fn deserialize_i8<V>(self, visitor: V) -> Result<V::Value, Self::Error>
585    where
586        V: Visitor<'de>,
587    {
588        self.root = Some((OpenApiSchema::integer(), Value::Number(0.into())));
589        visitor.visit_i8(0)
590    }
591
592    fn deserialize_u8<V>(self, visitor: V) -> Result<V::Value, Self::Error>
593    where
594        V: Visitor<'de>,
595    {
596        self.root = Some((OpenApiSchema::integer(), Value::Number(0.into())));
597        visitor.visit_u8(0)
598    }
599
600    fn deserialize_i16<V>(self, visitor: V) -> Result<V::Value, Self::Error>
601    where
602        V: Visitor<'de>,
603    {
604        self.root = Some((OpenApiSchema::integer(), Value::Number(0.into())));
605        visitor.visit_i16(0)
606    }
607
608    fn deserialize_u16<V>(self, visitor: V) -> Result<V::Value, Self::Error>
609    where
610        V: Visitor<'de>,
611    {
612        self.root = Some((OpenApiSchema::integer(), Value::Number(0.into())));
613        visitor.visit_u16(0)
614    }
615
616    fn deserialize_i32<V>(self, visitor: V) -> Result<V::Value, Self::Error>
617    where
618        V: Visitor<'de>,
619    {
620        self.root = Some((OpenApiSchema::integer(), Value::Number(0.into())));
621        visitor.visit_i32(0)
622    }
623
624    fn deserialize_u32<V>(self, visitor: V) -> Result<V::Value, Self::Error>
625    where
626        V: Visitor<'de>,
627    {
628        self.root = Some((OpenApiSchema::integer(), Value::Number(0.into())));
629        visitor.visit_u32(0)
630    }
631
632    fn deserialize_f32<V>(self, visitor: V) -> Result<V::Value, Self::Error>
633    where
634        V: Visitor<'de>,
635    {
636        self.root = Some((
637            OpenApiSchema::number().with_format(FLOAT_FORMAT),
638            json!(0.0),
639        ));
640        visitor.visit_f32(0.0)
641    }
642
643    fn deserialize_i64<V>(self, visitor: V) -> Result<V::Value, Self::Error>
644    where
645        V: Visitor<'de>,
646    {
647        self.root = Some((OpenApiSchema::integer(), Value::Number(0.into())));
648        visitor.visit_i64(0)
649    }
650
651    fn deserialize_u64<V>(self, visitor: V) -> Result<V::Value, Self::Error>
652    where
653        V: Visitor<'de>,
654    {
655        self.root = Some((OpenApiSchema::integer(), Value::Number(0.into())));
656        visitor.visit_u64(0)
657    }
658
659    fn deserialize_i128<V>(self, visitor: V) -> Result<V::Value, Self::Error>
660    where
661        V: Visitor<'de>,
662    {
663        self.root = Some((OpenApiSchema::integer(), Value::Number(0.into())));
664        visitor.visit_i128(0)
665    }
666
667    fn deserialize_u128<V>(self, visitor: V) -> Result<V::Value, Self::Error>
668    where
669        V: Visitor<'de>,
670    {
671        self.root = Some((OpenApiSchema::integer(), Value::Number(0.into())));
672        visitor.visit_u128(0)
673    }
674
675    fn deserialize_f64<V>(self, visitor: V) -> Result<V::Value, Self::Error>
676    where
677        V: Visitor<'de>,
678    {
679        self.root = Some((
680            OpenApiSchema::number().with_format(DOUBLE_FORMAT),
681            json!(0.0),
682        ));
683        visitor.visit_f64(0.0)
684    }
685
686    fn deserialize_str<V>(self, visitor: V) -> Result<V::Value, Self::Error>
687    where
688        V: Visitor<'de>,
689    {
690        self.root = Some((OpenApiSchema::string(), Value::String(String::new())));
691        visitor.visit_borrowed_str("")
692    }
693
694    fn deserialize_string<V>(self, visitor: V) -> Result<V::Value, Self::Error>
695    where
696        V: Visitor<'de>,
697    {
698        self.root = Some((OpenApiSchema::string(), Value::String(String::new())));
699        visitor.visit_string(String::new())
700    }
701
702    fn deserialize_option<V>(self, visitor: V) -> Result<V::Value, Self::Error>
703    where
704        V: Visitor<'de>,
705    {
706        // The inner type is probed by this very `Probe`: forwarding it to `deserialize_any`
707        // instead would answer every inner type with a unit, which a `String` (or any other
708        // typed) visitor rejects - and one rejected field drops the whole schema.
709        let out = self.descend(|probe| visitor.visit_some(probe))?;
710
711        if let Some((schema, example)) = self.root.take() {
712            self.root = Some((schema.nullable(), example));
713        } else {
714            self.root = Some((OpenApiSchema::object().nullable(), Value::Null));
715        }
716
717        Ok(out)
718    }
719
720    fn deserialize_seq<V>(self, visitor: V) -> Result<V::Value, Self::Error>
721    where
722        V: Visitor<'de>,
723    {
724        visitor.visit_seq(SeqProbeAccess {
725            probe: self,
726            yielded: false,
727        })
728    }
729
730    fn deserialize_map<V>(self, visitor: V) -> Result<V::Value, Self::Error>
731    where
732        V: Visitor<'de>,
733    {
734        // Rendered up front: the visitor is gone by the time a key shows it is a struct.
735        // A visitor that does not say it expects a struct is taken at its word, and its
736        // keys are not asked about at all - see [`ReadAsMap`]
737        let expecting = (&visitor as &dyn Expected).to_string();
738        let expecting = expecting.starts_with(STRUCT_EXPECTING).then_some(expecting);
739
740        self.root = Some((OpenApiSchema::object(), Value::Object(Map::new())));
741        visitor.visit_map(MapProbeAccess {
742            probe: self,
743            expecting,
744        })
745    }
746
747    fn deserialize_struct<V>(
748        self,
749        _name: &'static str,
750        fields: &'static [&'static str],
751        visitor: V,
752    ) -> Result<V::Value, Self::Error>
753    where
754        V: Visitor<'de>,
755    {
756        let access = StructProbeAccess {
757            fields,
758            idx: 0,
759            parent: self,
760            obj_schema: OpenApiSchema::object(),
761            example: Map::new(),
762            required: Vec::new(),
763        };
764
765        visitor.visit_map(access)
766    }
767
768    fn deserialize_enum<V>(
769        self,
770        _name: &'static str,
771        _variants: &'static [&'static str],
772        _visitor: V,
773    ) -> Result<V::Value, Self::Error>
774    where
775        V: Visitor<'de>,
776    {
777        Err(ProbeError(
778            "enums are not supported for automatic schema inference".into(),
779        ))
780    }
781
782    serde::forward_to_deserialize_any! {
783        char bytes byte_buf unit unit_struct
784        newtype_struct tuple tuple_struct identifier ignored_any
785    }
786}
787
788struct SeqProbeAccess<'a> {
789    probe: &'a mut Probe,
790    yielded: bool,
791}
792
793impl<'de, 'a> SeqAccess<'de> for SeqProbeAccess<'a> {
794    type Error = ProbeError;
795
796    fn next_element_seed<T>(&mut self, seed: T) -> Result<Option<T::Value>, Self::Error>
797    where
798        T: DeserializeSeed<'de>,
799    {
800        if self.yielded {
801            if let Some((item_schema, item_example)) = self.probe.root.take() {
802                self.probe.root = Some((
803                    OpenApiSchema::array(item_schema),
804                    Value::Array(vec![item_example]),
805                ));
806            } else {
807                self.probe.root = Some((
808                    OpenApiSchema::array(OpenApiSchema::object()),
809                    Value::Array(vec![]),
810                ));
811            }
812            return Ok(None);
813        }
814
815        self.yielded = true;
816        let v = self.probe.descend(|probe| seed.deserialize(probe))?;
817        Ok(Some(v))
818    }
819}
820
821struct StructProbeAccess<'a> {
822    fields: &'static [&'static str],
823    idx: usize,
824    parent: &'a mut Probe,
825
826    obj_schema: OpenApiSchema,
827    example: Map<String, Value>,
828    required: Vec<String>,
829}
830
831impl<'de, 'a> MapAccess<'de> for StructProbeAccess<'a> {
832    type Error = ProbeError;
833
834    fn next_key_seed<K>(&mut self, seed: K) -> Result<Option<K::Value>, Self::Error>
835    where
836        K: DeserializeSeed<'de>,
837    {
838        if self.idx >= self.fields.len() {
839            let schema = self.obj_schema.clone().with_required(self.required.clone());
840            let ex = Value::Object(std::mem::take(&mut self.example));
841            self.parent.root = Some((schema, ex));
842            return Ok(None);
843        }
844
845        let key = self.fields[self.idx];
846        let key_de = key.into_deserializer();
847        let k = seed.deserialize(key_de)?;
848        Ok(Some(k))
849    }
850
851    fn next_value_seed<V>(&mut self, seed: V) -> Result<V::Value, Self::Error>
852    where
853        V: DeserializeSeed<'de>,
854    {
855        let field = self.fields[self.idx];
856        self.idx += 1;
857
858        let v = self.parent.descend(|probe| seed.deserialize(probe))?;
859
860        if let Some((field_schema, field_example)) = self.parent.root.take() {
861            let is_required = field_schema.nullable != Some(true) && field_example != Value::Null;
862            self.obj_schema = self
863                .obj_schema
864                .clone()
865                .with_property(field.to_string(), field_schema);
866
867            self.example.insert(field.to_string(), field_example);
868            if is_required {
869                self.required.push(field.to_string());
870            }
871        } else {
872            self.obj_schema = self
873                .obj_schema
874                .clone()
875                .with_property(field.to_string(), OpenApiSchema::object());
876
877            self.example.insert(field.to_string(), Value::Null);
878        }
879
880        Ok(v)
881    }
882}
883
884/// What the visitor serde's derive writes for a struct expects, up to the struct's name
885const STRUCT_EXPECTING: &str = "struct ";
886
887/// An empty map, which asks for one key first to learn whether it is a struct read as one
888struct MapProbeAccess<'a> {
889    probe: &'a mut Probe,
890    /// What the map's visitor expects, until its first key is asked for - `None` from the
891    /// start for a visitor that does not expect a struct
892    expecting: Option<String>,
893}
894
895impl<'de, 'a> MapAccess<'de> for MapProbeAccess<'a> {
896    type Error = ProbeError;
897
898    fn next_key_seed<K>(&mut self, seed: K) -> Result<Option<K::Value>, Self::Error>
899    where
900        K: DeserializeSeed<'de>,
901    {
902        let Some(expecting) = self.expecting.take() else {
903            return Ok(None);
904        };
905
906        // No key is produced either way - the key probe refuses every request - so all
907        // that comes of this is learning how the map reads its keys
908        let mut reads_fields = false;
909        let _ = seed.deserialize(KeyProbe {
910            reads_fields: &mut reads_fields,
911        });
912
913        if !reads_fields {
914            return Ok(None);
915        }
916
917        self.probe.read_as_map.get_or_insert(ReadAsMap {
918            expecting,
919            at_root: self.probe.depth == 0,
920        });
921
922        // The struct cannot be completed without its fields, and neither can anything it
923        // sits in, so the probe stops here rather than guess at the rest
924        Err(ProbeError("a struct read as a map".into()))
925    }
926
927    fn next_value_seed<V>(&mut self, _seed: V) -> Result<V::Value, Self::Error>
928    where
929        V: DeserializeSeed<'de>,
930    {
931        Err(ProbeError("no values".into()))
932    }
933}
934
935/// Reads the first key of a map, reporting whether it is read as a struct's field
936/// identifier - which is how serde reads the keys of a struct it reads as a map
937struct KeyProbe<'a> {
938    reads_fields: &'a mut bool,
939}
940
941impl<'de, 'a> Deserializer<'de> for KeyProbe<'a> {
942    type Error = ProbeError;
943
944    fn deserialize_any<V>(self, _visitor: V) -> Result<V::Value, Self::Error>
945    where
946        V: Visitor<'de>,
947    {
948        Err(ProbeError("no keys".into()))
949    }
950
951    fn deserialize_identifier<V>(self, _visitor: V) -> Result<V::Value, Self::Error>
952    where
953        V: Visitor<'de>,
954    {
955        *self.reads_fields = true;
956        Err(ProbeError("no keys".into()))
957    }
958
959    serde::forward_to_deserialize_any! {
960        bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 f32 f64 char str string
961        bytes byte_buf option unit unit_struct newtype_struct seq tuple
962        tuple_struct map struct enum ignored_any
963    }
964}
965
966#[cfg(test)]
967mod tests {
968    use super::*;
969    use std::collections::{BTreeSet, HashMap};
970
971    impl Probed {
972        fn shape(self) -> Option<(OpenApiSchema, Value)> {
973            match self {
974                Probed::Shape(schema, example) => Some((schema, example)),
975                _ => None,
976            }
977        }
978    }
979
980    fn probe<T: serde::de::DeserializeOwned>() -> Probed {
981        let mut probe = Probe::new();
982        let _ = T::deserialize(&mut probe);
983        probe.finish()
984    }
985
986    #[derive(Deserialize)]
987    #[allow(dead_code)]
988    struct Inner {
989        name: String,
990    }
991
992    #[derive(Deserialize)]
993    #[allow(dead_code)]
994    struct Flat {
995        #[serde(flatten)]
996        inner: Inner,
997        page: u32,
998    }
999
1000    #[test]
1001    fn probe_reports_a_struct_with_a_flattened_field() {
1002        let Probed::ReadAsMap(found) = probe::<Flat>() else {
1003            panic!("a struct read as a map should be reported");
1004        };
1005
1006        assert_eq!(found.expecting, "struct Flat");
1007        assert!(found.at_root);
1008    }
1009
1010    #[test]
1011    fn probe_reports_a_struct_read_as_a_map_inside_the_type() {
1012        #[derive(Deserialize)]
1013        #[allow(dead_code)]
1014        struct Outer {
1015            id: u32,
1016            flat: Flat,
1017        }
1018
1019        for probed in [
1020            probe::<Outer>(),
1021            probe::<Vec<Flat>>(),
1022            probe::<Option<Flat>>(),
1023        ] {
1024            let Probed::ReadAsMap(found) = probed else {
1025                panic!("a struct read as a map should be reported: {probed:?}");
1026            };
1027
1028            assert_eq!(found.expecting, "struct Flat");
1029            assert!(!found.at_root);
1030        }
1031    }
1032
1033    #[test]
1034    fn probe_reports_a_struct_with_only_optional_fields_around_a_flattened_one() {
1035        #[derive(Deserialize)]
1036        #[allow(dead_code)]
1037        struct Optional {
1038            #[serde(flatten)]
1039            rest: HashMap<String, String>,
1040            page: Option<u32>,
1041        }
1042
1043        assert!(matches!(probe::<Optional>(), Probed::ReadAsMap(_)));
1044    }
1045
1046    #[test]
1047    fn probe_describes_a_map_as_an_object() {
1048        let Probed::Shape(schema, example) = probe::<HashMap<String, u32>>() else {
1049            panic!("a map should be described");
1050        };
1051
1052        assert_eq!(schema.schema_type.as_deref(), Some("object"));
1053        assert!(schema.properties.is_none());
1054        assert_eq!(example, json!({}));
1055    }
1056
1057    #[test]
1058    fn probe_describes_a_map_inside_a_struct() {
1059        #[derive(Deserialize)]
1060        #[allow(dead_code)]
1061        struct Tagged {
1062            tags: HashMap<String, String>,
1063            page: u32,
1064        }
1065
1066        let (schema, _) = probe::<Tagged>()
1067            .shape()
1068            .expect("schema should be produced");
1069
1070        let props = schema.properties.expect("properties");
1071        assert_eq!(props["tags"].schema_type.as_deref(), Some("object"));
1072        assert_eq!(props["page"].schema_type.as_deref(), Some("integer"));
1073    }
1074
1075    /// A key type of its own may be read as an identifier too, the way a struct reads its
1076    /// keys - the map around it is still a map
1077    #[derive(Deserialize, PartialEq, Eq, Hash)]
1078    #[serde(field_identifier, rename_all = "snake_case")]
1079    #[allow(dead_code)]
1080    enum Label {
1081        Name,
1082        Tag,
1083    }
1084
1085    #[test]
1086    fn probe_describes_a_map_with_identifier_keys_as_an_object() {
1087        let Probed::Shape(schema, _) = probe::<HashMap<Label, String>>() else {
1088            panic!("a map should be described");
1089        };
1090
1091        assert_eq!(schema.schema_type.as_deref(), Some("object"));
1092    }
1093
1094    #[test]
1095    fn probe_describes_a_map_with_identifier_keys_inside_a_struct() {
1096        #[derive(Deserialize)]
1097        #[allow(dead_code)]
1098        struct Labelled {
1099            labels: HashMap<Label, String>,
1100            page: u32,
1101        }
1102
1103        let (schema, _) = probe::<Labelled>()
1104            .shape()
1105            .expect("schema should be produced");
1106
1107        let props = schema.properties.expect("properties");
1108        assert_eq!(props["labels"].schema_type.as_deref(), Some("object"));
1109        assert_eq!(props["page"].schema_type.as_deref(), Some("integer"));
1110    }
1111
1112    /// A struct that renames what it expects no longer says it is one, so it is taken for
1113    /// the map it is read as - described as an object without properties, and not reported
1114    #[test]
1115    fn probe_takes_a_struct_expecting_something_else_for_a_map() {
1116        #[derive(Deserialize)]
1117        #[serde(expecting = "a search request")]
1118        #[allow(dead_code)]
1119        struct Search {
1120            #[serde(flatten)]
1121            inner: Inner,
1122            page: u32,
1123        }
1124
1125        let Probed::Shape(schema, _) = probe::<Search>() else {
1126            panic!("the struct should be taken for a map");
1127        };
1128
1129        assert_eq!(schema.schema_type.as_deref(), Some("object"));
1130        assert!(schema.properties.is_none());
1131    }
1132
1133    #[test]
1134    fn probe_reports_nothing_for_an_enum() {
1135        #[derive(Deserialize)]
1136        #[allow(dead_code)]
1137        enum Kind {
1138            One,
1139        }
1140
1141        assert!(matches!(probe::<Kind>(), Probed::Nothing));
1142    }
1143
1144    #[test]
1145    fn schema_from_example_object_keeps_properties() {
1146        let schema = OpenApiSchema::from_example(&serde_json::json!({
1147            "name": "Alice",
1148            "age": 42
1149        }));
1150
1151        let props = schema.properties.expect("schema properties");
1152        assert_eq!(
1153            props.get("name").expect("name").schema_type.as_deref(),
1154            Some("string")
1155        );
1156        assert_eq!(
1157            props.get("age").expect("age").schema_type.as_deref(),
1158            Some("integer")
1159        );
1160    }
1161
1162    #[test]
1163    fn schema_from_example_handles_null_and_arrays() {
1164        let null_schema = OpenApiSchema::from_example(&Value::Null);
1165        assert_eq!(null_schema.schema_type.as_deref(), Some("object"));
1166        assert_eq!(null_schema.nullable, Some(true));
1167
1168        let array_schema = OpenApiSchema::from_example(&serde_json::json!([1, 2, 3]));
1169        assert_eq!(array_schema.schema_type.as_deref(), Some("array"));
1170        assert_eq!(
1171            array_schema
1172                .items
1173                .expect("array items")
1174                .schema_type
1175                .as_deref(),
1176            Some("integer")
1177        );
1178    }
1179
1180    #[test]
1181    fn multipart_schema_contains_expected_fields() {
1182        let schema = OpenApiSchema::multipart();
1183        let props = schema.properties.expect("properties");
1184
1185        assert!(props.contains_key("file"));
1186        assert!(props.contains_key("meta"));
1187        assert_eq!(schema.required.expect("required"), vec!["file"]);
1188    }
1189
1190    #[test]
1191    fn schema_reference_marks_ref_and_path() {
1192        let schema = OpenApiSchema::reference("Payload");
1193        assert!(schema.is_ref());
1194        assert_eq!(
1195            schema.schema_ref.as_deref(),
1196            Some("#/components/schemas/Payload")
1197        );
1198    }
1199
1200    #[test]
1201    fn number_and_boolean_builders_set_expected_types() {
1202        assert_eq!(
1203            OpenApiSchema::number().schema_type.as_deref(),
1204            Some("number")
1205        );
1206        assert_eq!(
1207            OpenApiSchema::boolean().schema_type.as_deref(),
1208            Some("boolean")
1209        );
1210    }
1211
1212    #[test]
1213    fn with_format_sets_schema_format() {
1214        let schema = OpenApiSchema::string().with_format("uuid");
1215        assert_eq!(schema.format.as_deref(), Some("uuid"));
1216    }
1217
1218    #[test]
1219    fn default_schema_is_object() {
1220        let schema = OpenApiSchema::default();
1221        assert_eq!(schema.schema_type.as_deref(), Some("object"));
1222    }
1223
1224    #[test]
1225    fn probe_does_not_mark_option_fields_as_required() {
1226        #[derive(Deserialize)]
1227        #[allow(dead_code)]
1228        struct Input {
1229            required_name: String,
1230            optional_age: Option<()>,
1231        }
1232
1233        let mut probe = Probe::new();
1234        let _ = Input::deserialize(&mut probe);
1235        let (schema, _) = probe.finish().shape().expect("schema should be produced");
1236
1237        let required = schema.required.expect("required list");
1238        assert!(required.contains(&"required_name".to_string()));
1239        assert!(!required.contains(&"optional_age".to_string()));
1240    }
1241
1242    #[test]
1243    fn probe_supports_i32_fields_for_schema_inference() {
1244        #[derive(Deserialize)]
1245        #[allow(dead_code)]
1246        struct NumericInput {
1247            value: i32,
1248        }
1249
1250        let mut probe = Probe::new();
1251        let _ = NumericInput::deserialize(&mut probe);
1252        let (schema, _) = probe.finish().shape().expect("schema should be produced");
1253
1254        let props = schema.properties.expect("properties");
1255        assert_eq!(props["value"].schema_type.as_deref(), Some("integer"));
1256    }
1257
1258    #[test]
1259    fn probe_supports_u32_fields_for_schema_inference() {
1260        #[derive(Deserialize)]
1261        #[allow(dead_code)]
1262        struct NumericInput {
1263            value: u32,
1264        }
1265
1266        let mut probe = Probe::new();
1267        let _ = NumericInput::deserialize(&mut probe);
1268        let (schema, _) = probe.finish().shape().expect("schema should be produced");
1269
1270        let props = schema.properties.expect("properties");
1271        assert_eq!(props["value"].schema_type.as_deref(), Some("integer"));
1272    }
1273
1274    #[test]
1275    fn probe_supports_f32_fields_for_schema_inference() {
1276        #[derive(Deserialize)]
1277        #[allow(dead_code)]
1278        struct NumericInput {
1279            value: f32,
1280        }
1281
1282        let mut probe = Probe::new();
1283        let _ = NumericInput::deserialize(&mut probe);
1284        let (schema, _) = probe.finish().shape().expect("schema should be produced");
1285
1286        let props = schema.properties.expect("properties");
1287        assert_eq!(props["value"].schema_type.as_deref(), Some("number"));
1288    }
1289
1290    #[test]
1291    fn probe_supports_i128_fields_for_schema_inference() {
1292        #[derive(Deserialize)]
1293        #[allow(dead_code)]
1294        struct NumericInput {
1295            value: i128,
1296        }
1297
1298        let mut probe = Probe::new();
1299        let _ = NumericInput::deserialize(&mut probe);
1300        let (schema, _) = probe.finish().shape().expect("schema should be produced");
1301
1302        let props = schema.properties.expect("properties");
1303        assert_eq!(props["value"].schema_type.as_deref(), Some("integer"));
1304    }
1305
1306    #[test]
1307    fn probe_supports_u128_fields_for_schema_inference() {
1308        #[derive(Deserialize)]
1309        #[allow(dead_code)]
1310        struct NumericInput {
1311            value: u128,
1312        }
1313
1314        let mut probe = Probe::new();
1315        let _ = NumericInput::deserialize(&mut probe);
1316        let (schema, _) = probe.finish().shape().expect("schema should be produced");
1317
1318        let props = schema.properties.expect("properties");
1319        assert_eq!(props["value"].schema_type.as_deref(), Some("integer"));
1320    }
1321
1322    #[test]
1323    fn probe_supports_i16_fields_for_schema_inference() {
1324        #[derive(Deserialize)]
1325        #[allow(dead_code)]
1326        struct NumericInput {
1327            value: i16,
1328        }
1329
1330        let mut probe = Probe::new();
1331        let _ = NumericInput::deserialize(&mut probe);
1332        let (schema, _) = probe.finish().shape().expect("schema should be produced");
1333
1334        let props = schema.properties.expect("properties");
1335        assert_eq!(props["value"].schema_type.as_deref(), Some("integer"));
1336    }
1337
1338    #[test]
1339    fn probe_supports_u16_fields_for_schema_inference() {
1340        #[derive(Deserialize)]
1341        #[allow(dead_code)]
1342        struct NumericInput {
1343            value: u16,
1344        }
1345
1346        let mut probe = Probe::new();
1347        let _ = NumericInput::deserialize(&mut probe);
1348        let (schema, _) = probe.finish().shape().expect("schema should be produced");
1349
1350        let props = schema.properties.expect("properties");
1351        assert_eq!(props["value"].schema_type.as_deref(), Some("integer"));
1352    }
1353
1354    #[test]
1355    fn probe_supports_i8_fields_for_schema_inference() {
1356        #[derive(Deserialize)]
1357        #[allow(dead_code)]
1358        struct NumericInput {
1359            value: i8,
1360        }
1361
1362        let mut probe = Probe::new();
1363        let _ = NumericInput::deserialize(&mut probe);
1364        let (schema, _) = probe.finish().shape().expect("schema should be produced");
1365
1366        let props = schema.properties.expect("properties");
1367        assert_eq!(props["value"].schema_type.as_deref(), Some("integer"));
1368    }
1369
1370    #[test]
1371    fn probe_supports_u8_fields_for_schema_inference() {
1372        #[derive(Deserialize)]
1373        #[allow(dead_code)]
1374        struct NumericInput {
1375            value: u8,
1376        }
1377
1378        let mut probe = Probe::new();
1379        let _ = NumericInput::deserialize(&mut probe);
1380        let (schema, _) = probe.finish().shape().expect("schema should be produced");
1381
1382        let props = schema.properties.expect("properties");
1383        assert_eq!(props["value"].schema_type.as_deref(), Some("integer"));
1384    }
1385
1386    #[test]
1387    fn collect_component_refs_walks_nested_refs() {
1388        let schema = OpenApiSchema::object()
1389            .with_property("user", OpenApiSchema::reference("User"))
1390            .with_property(
1391                "items",
1392                OpenApiSchema::array(OpenApiSchema::reference("Item")),
1393            );
1394
1395        let mut refs = BTreeSet::new();
1396        schema.collect_component_refs(&mut refs);
1397
1398        assert!(refs.contains("User"));
1399        assert!(refs.contains("Item"));
1400        assert_eq!(refs.len(), 2);
1401    }
1402}