1use 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#[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#[derive(Clone, Debug, PartialEq)]
76pub struct FieldConstraint {
77 pub field: String,
79
80 pub constraint: SchemaConstraint,
82}
83
84impl FieldConstraint {
85 pub fn new(field: impl Into<String>, constraint: SchemaConstraint) -> Self {
87 Self {
88 field: field.into(),
89 constraint,
90 }
91 }
92}
93
94#[derive(Clone, Debug, PartialEq)]
96#[non_exhaustive]
97pub enum SchemaConstraint {
98 MinSize(usize),
103 MaxSize(usize),
105 Minimum(Number),
107 Maximum(Number),
109 Nested(Vec<FieldConstraint>),
111 Each(Vec<FieldConstraint>),
113}
114
115fn 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
126fn 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
134fn 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
145fn 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
156fn 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
167const FLOAT_FORMAT: &str = "float";
169
170const DOUBLE_FORMAT: &str = "double";
172
173enum SizeKeyword {
175 Length,
177 Items,
179 Properties,
181}
182
183impl OpenApiSchema {
184 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 pub fn with_constraint(mut self, constraint: SchemaConstraint) -> Self {
210 self.apply_constraint(&constraint);
211 self
212 }
213
214 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 fn narrow(&self, value: &Number) -> Number {
254 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 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 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 pub fn object() -> Self {
295 Self {
296 schema_type: Some("object".to_string()),
297 ..Self::empty()
298 }
299 }
300
301 pub fn string() -> Self {
303 Self {
304 schema_type: Some("string".to_string()),
305 ..Self::empty()
306 }
307 }
308
309 pub fn integer() -> Self {
311 Self {
312 schema_type: Some("integer".to_string()),
313 ..Self::empty()
314 }
315 }
316
317 pub fn number() -> Self {
319 Self {
320 schema_type: Some("number".to_string()),
321 ..Self::empty()
322 }
323 }
324
325 pub fn boolean() -> Self {
327 Self {
328 schema_type: Some("boolean".to_string()),
329 ..Self::empty()
330 }
331 }
332
333 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 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 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 pub fn reference(name: &str) -> Self {
361 Self {
362 schema_ref: Some(format!("#/components/schemas/{name}")),
363 ..Self::empty()
364 }
365 }
366
367 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 pub fn nullable(mut self) -> Self {
401 self.nullable = Some(true);
402 self
403 }
404
405 pub fn with_title(mut self, title: impl Into<String>) -> Self {
407 self.title = Some(title.into());
408 self
409 }
410
411 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 pub fn with_format(mut self, fmt: impl Into<String>) -> Self {
421 self.format = Some(fmt.into());
422 self
423 }
424
425 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 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 }
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#[derive(Debug)]
492#[allow(clippy::large_enum_variant)]
493pub(super) enum Probed {
494 Shape(OpenApiSchema, Value),
496 ReadAsMap(ReadAsMap),
498 Nothing,
500}
501
502#[derive(Clone, Debug, PartialEq, Eq)]
521pub(super) struct ReadAsMap {
522 pub(super) expecting: String,
524 pub(super) at_root: bool,
526}
527
528pub(super) struct Probe {
529 root: Option<(OpenApiSchema, Value)>,
530 depth: usize,
532 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 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 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 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 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
884const STRUCT_EXPECTING: &str = "struct ";
886
887struct MapProbeAccess<'a> {
889 probe: &'a mut Probe,
890 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 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 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
935struct 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 #[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 #[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}