progenitor_impl/
to_schema.rs

1// Copyright 2024 Oxide Computer Company
2
3use indexmap::IndexMap;
4use openapiv3::AnySchema;
5use schemars::schema::SingleOrVec;
6use serde_json::Value;
7
8pub trait ToSchema {
9    fn to_schema(&self) -> schemars::schema::Schema;
10}
11
12trait Convert<T> {
13    fn convert(&self) -> T;
14}
15
16impl ToSchema for openapiv3::Schema {
17    fn to_schema(&self) -> schemars::schema::Schema {
18        self.convert()
19    }
20}
21
22impl ToSchema for openapiv3::ReferenceOr<openapiv3::Schema> {
23    fn to_schema(&self) -> schemars::schema::Schema {
24        self.convert()
25    }
26}
27
28impl<I, T> Convert<Vec<T>> for Vec<I>
29where
30    I: Convert<T>,
31{
32    fn convert(&self) -> Vec<T> {
33        self.iter().map(Convert::convert).collect()
34    }
35}
36
37impl<I, T> Convert<Option<Vec<T>>> for Vec<I>
38where
39    I: Convert<T>,
40{
41    fn convert(&self) -> Option<Vec<T>> {
42        if self.is_empty() {
43            None
44        } else {
45            Some(self.iter().map(Convert::convert).collect())
46        }
47    }
48}
49
50impl Convert<schemars::schema::Schema> for openapiv3::ReferenceOr<openapiv3::Schema> {
51    fn convert(&self) -> schemars::schema::Schema {
52        match self {
53            openapiv3::ReferenceOr::Reference { reference } => {
54                schemars::schema::SchemaObject::new_ref(reference.clone()).into()
55            }
56            openapiv3::ReferenceOr::Item(schema) => schema.convert(),
57        }
58    }
59}
60
61impl<T, TT> Convert<TT> for openapiv3::ReferenceOr<Box<T>>
62where
63    openapiv3::ReferenceOr<T>: Convert<TT>,
64    T: Clone,
65{
66    fn convert(&self) -> TT {
67        self.clone().unbox().convert()
68    }
69}
70
71impl Convert<schemars::schema::Schema> for openapiv3::Schema {
72    fn convert(&self) -> schemars::schema::Schema {
73        // TODO the discriminator field is used in a way that seems both
74        // important and unfortunately redundant. It corresponds to the same
75        // regime as internally tagged enums in the serde sense: a field that
76        // the discriminator defines is used to determine which schema is
77        // valid. This can base used in two ways:
78
79        // 1. It can be used within a struct to identify a particular, required
80        // field. This is typically done on a "base" class in an OOP hierarchy.
81        // Child class structs "extend" that base class by using an allOf
82        // construction where the parent is one of the subschemas. To recognize
83        // this case, we need to check all subschemas in an allOf to see if any
84        // of them have a discriminator. If they do, we can simply construct an
85        // additional structure for the allOf that has a fixed value for that
86        // field.
87
88        // 2. It can be used within a oneOf or anyOf schema to determine which
89        // subschema is relevant. This is easier to detect because it doesn't
90        // require chasing references. For each subschema we can then make it
91        // an allOf union of the actual subschema along with a fixed-field
92        // structure.
93
94        let openapiv3::SchemaData {
95            nullable,
96            discriminator: _, // TODO: see above
97            external_docs: _, // TODO: append to description?
98
99            title,
100            description,
101            default,
102            deprecated,
103            read_only,
104            write_only,
105            example,
106            extensions,
107        } = self.schema_data.clone();
108
109        let metadata = schemars::schema::Metadata {
110            id: None,
111            title,
112            description,
113            default,
114            deprecated,
115            read_only,
116            write_only,
117            examples: example.into_iter().collect::<Vec<_>>(),
118        };
119
120        let metadata = Some(Box::new(metadata)).reduce();
121        let extensions = extensions.into_iter().collect();
122
123        match &self.schema_kind {
124            openapiv3::SchemaKind::Type(openapiv3::Type::String(openapiv3::StringType {
125                format,
126                pattern,
127                enumeration,
128                min_length,
129                max_length,
130            })) => schemars::schema::SchemaObject {
131                metadata,
132                instance_type: instance_type(schemars::schema::InstanceType::String, nullable),
133                format: format.convert(),
134                enum_values: enumeration.convert(),
135                string: Some(Box::new(schemars::schema::StringValidation {
136                    max_length: max_length.convert(),
137                    min_length: min_length.convert(),
138                    pattern: pattern.clone(),
139                }))
140                .reduce(),
141                extensions,
142                ..Default::default()
143            },
144            openapiv3::SchemaKind::Type(openapiv3::Type::Number(openapiv3::NumberType {
145                format,
146                multiple_of,
147                exclusive_minimum,
148                exclusive_maximum,
149                minimum,
150                maximum,
151                enumeration,
152            })) => {
153                let (maximum, exclusive_maximum) = match (maximum, exclusive_maximum) {
154                    (v, true) => (None, *v),
155                    (v, false) => (*v, None),
156                };
157                let (minimum, exclusive_minimum) = match (minimum, exclusive_minimum) {
158                    (v, true) => (None, *v),
159                    (v, false) => (*v, None),
160                };
161                schemars::schema::SchemaObject {
162                    metadata,
163                    instance_type: instance_type(schemars::schema::InstanceType::Number, nullable),
164                    format: format.convert(),
165                    enum_values: enumeration.convert(),
166                    number: Some(Box::new(schemars::schema::NumberValidation {
167                        multiple_of: *multiple_of,
168                        maximum,
169                        exclusive_maximum,
170                        minimum,
171                        exclusive_minimum,
172                    }))
173                    .reduce(),
174                    extensions,
175                    ..Default::default()
176                }
177            }
178
179            openapiv3::SchemaKind::Type(openapiv3::Type::Integer(openapiv3::IntegerType {
180                format,
181                multiple_of,
182                exclusive_minimum,
183                exclusive_maximum,
184                minimum,
185                maximum,
186                enumeration,
187            })) => {
188                let (maximum, exclusive_maximum) = match (maximum, exclusive_maximum) {
189                    (Some(v), true) => (None, Some(*v as f64)),
190                    (Some(v), false) => (Some(*v as f64), None),
191                    (None, _) => (None, None),
192                };
193                let (minimum, exclusive_minimum) = match (minimum, exclusive_minimum) {
194                    (Some(v), true) => (None, Some(*v as f64)),
195                    (Some(v), false) => (Some(*v as f64), None),
196                    (None, _) => (None, None),
197                };
198                schemars::schema::SchemaObject {
199                    metadata,
200                    instance_type: instance_type(schemars::schema::InstanceType::Integer, nullable),
201                    format: format.convert(),
202                    enum_values: enumeration.convert(),
203                    number: Some(Box::new(schemars::schema::NumberValidation {
204                        multiple_of: multiple_of.map(|v| v as f64).convert(),
205                        maximum,
206                        exclusive_maximum,
207                        minimum,
208                        exclusive_minimum,
209                    }))
210                    .reduce(),
211                    extensions,
212                    ..Default::default()
213                }
214            }
215
216            openapiv3::SchemaKind::Type(openapiv3::Type::Object(openapiv3::ObjectType {
217                properties,
218                required,
219                additional_properties,
220                min_properties,
221                max_properties,
222            })) => schemars::schema::SchemaObject {
223                metadata,
224                instance_type: instance_type(schemars::schema::InstanceType::Object, nullable),
225                object: Some(Box::new(schemars::schema::ObjectValidation {
226                    max_properties: max_properties.convert(),
227                    min_properties: min_properties.convert(),
228                    required: required.convert(),
229                    properties: properties.convert(),
230                    pattern_properties: schemars::Map::default(),
231                    additional_properties: additional_properties.convert(),
232                    property_names: None,
233                }))
234                .reduce(),
235                extensions,
236                ..Default::default()
237            },
238
239            openapiv3::SchemaKind::Type(openapiv3::Type::Array(openapiv3::ArrayType {
240                items,
241                min_items,
242                max_items,
243                unique_items,
244            })) => schemars::schema::SchemaObject {
245                metadata,
246                instance_type: instance_type(schemars::schema::InstanceType::Array, nullable),
247                array: Some(Box::new(schemars::schema::ArrayValidation {
248                    items: items.as_ref().map(|items| {
249                        schemars::schema::SingleOrVec::Single(Box::new(items.convert()))
250                    }),
251                    additional_items: None,
252                    max_items: max_items.convert(),
253                    min_items: min_items.convert(),
254                    unique_items: if *unique_items { Some(true) } else { None },
255                    contains: None,
256                }))
257                .reduce(),
258                extensions,
259                ..Default::default()
260            },
261
262            openapiv3::SchemaKind::Type(openapiv3::Type::Boolean(openapiv3::BooleanType {
263                enumeration,
264            })) => schemars::schema::SchemaObject {
265                metadata,
266                instance_type: instance_type(schemars::schema::InstanceType::Boolean, nullable),
267                enum_values: enumeration.convert(),
268                extensions,
269                ..Default::default()
270            },
271
272            openapiv3::SchemaKind::OneOf { one_of } => oneof_nullable_wrapper(
273                schemars::schema::SchemaObject {
274                    metadata,
275                    subschemas: Some(Box::new(schemars::schema::SubschemaValidation {
276                        one_of: Some(one_of.convert()),
277                        ..Default::default()
278                    })),
279                    extensions,
280                    ..Default::default()
281                },
282                nullable,
283            ),
284
285            openapiv3::SchemaKind::AllOf { all_of } => oneof_nullable_wrapper(
286                schemars::schema::SchemaObject {
287                    metadata,
288                    subschemas: Some(Box::new(schemars::schema::SubschemaValidation {
289                        all_of: Some(all_of.convert()),
290                        ..Default::default()
291                    })),
292                    extensions,
293                    ..Default::default()
294                },
295                nullable,
296            ),
297
298            openapiv3::SchemaKind::AnyOf { any_of } => oneof_nullable_wrapper(
299                schemars::schema::SchemaObject {
300                    metadata,
301                    subschemas: Some(Box::new(schemars::schema::SubschemaValidation {
302                        any_of: Some(any_of.convert()),
303                        ..Default::default()
304                    })),
305                    extensions,
306                    ..Default::default()
307                },
308                nullable,
309            ),
310
311            openapiv3::SchemaKind::Not { not } => oneof_nullable_wrapper(
312                schemars::schema::SchemaObject {
313                    metadata,
314                    subschemas: Some(Box::new(schemars::schema::SubschemaValidation {
315                        not: Some(Box::new(not.convert())),
316                        ..Default::default()
317                    })),
318                    extensions,
319                    ..Default::default()
320                },
321                nullable,
322            ),
323
324            // This is the permissive schema that allows anything to match.
325            // We can ignore `nullable` because--sure--null matches already.
326            openapiv3::SchemaKind::Any(AnySchema {
327                typ: None,
328                pattern: None,
329                multiple_of: None,
330                exclusive_minimum: None,
331                exclusive_maximum: None,
332                minimum: None,
333                maximum: None,
334                properties,
335                required,
336                additional_properties: None,
337                min_properties: None,
338                max_properties: None,
339                items: None,
340                min_items: None,
341                max_items: None,
342                unique_items: None,
343                format: None,
344                enumeration,
345                min_length: None,
346                max_length: None,
347                one_of,
348                all_of,
349                any_of,
350                not: None,
351            }) if properties.is_empty()
352                && required.is_empty()
353                && enumeration.is_empty()
354                && one_of.is_empty()
355                && all_of.is_empty()
356                && any_of.is_empty() =>
357            {
358                schemars::schema::SchemaObject {
359                    metadata,
360                    extensions,
361                    ..Default::default()
362                }
363            }
364
365            // A simple null value. (We can ignore `nullable` as it would be
366            // redundant)
367            openapiv3::SchemaKind::Any(AnySchema {
368                typ: None,
369                pattern: None,
370                multiple_of: None,
371                exclusive_minimum: None,
372                exclusive_maximum: None,
373                minimum: None,
374                maximum: None,
375                properties,
376                required,
377                additional_properties: None,
378                min_properties: None,
379                max_properties: None,
380                items: None,
381                min_items: None,
382                max_items: None,
383                unique_items: None,
384                format: None,
385                enumeration,
386                min_length: None,
387                max_length: None,
388                one_of,
389                all_of,
390                any_of,
391                not: None,
392            }) if properties.is_empty()
393                && required.is_empty()
394                && enumeration.len() == 1
395                && enumeration[0] == serde_json::Value::Null
396                && one_of.is_empty()
397                && all_of.is_empty()
398                && any_of.is_empty() =>
399            {
400                schemars::schema::SchemaObject {
401                    metadata,
402                    instance_type: Some(schemars::schema::InstanceType::Null.into()),
403                    extensions,
404                    ..Default::default()
405                }
406            }
407
408            openapiv3::SchemaKind::Any(AnySchema {
409                typ,
410                pattern,
411                multiple_of,
412                exclusive_minimum,
413                exclusive_maximum,
414                minimum,
415                maximum,
416                properties,
417                required,
418                additional_properties,
419                min_properties,
420                max_properties,
421                items,
422                min_items,
423                max_items,
424                unique_items,
425                enumeration,
426                format,
427                min_length,
428                max_length,
429                one_of,
430                all_of,
431                any_of,
432                not,
433            }) => {
434                let mut so = schemars::schema::SchemaObject {
435                    metadata,
436                    extensions,
437                    ..Default::default()
438                };
439
440                // General
441                if let Some(format) = format {
442                    so.format = Some(format.clone());
443                }
444                if !enumeration.is_empty() {
445                    so.enum_values = Some(enumeration.clone());
446                }
447
448                // String
449                if let Some(pattern) = pattern {
450                    so.string().pattern = Some(pattern.clone());
451                }
452                if let Some(min_length) = min_length {
453                    so.string().min_length = Some(*min_length as u32);
454                }
455                if let Some(max_length) = max_length {
456                    so.string().max_length = Some(*max_length as u32);
457                }
458
459                // Number
460                if let Some(multiple_of) = multiple_of {
461                    so.number().multiple_of = Some(*multiple_of);
462                }
463                match (minimum, exclusive_minimum) {
464                    (None, Some(true)) => {
465                        todo!("exclusive_minimum set without minimum");
466                    }
467                    (None, _) => (),
468                    (Some(minimum), Some(true)) => {
469                        so.number().exclusive_minimum = Some(*minimum);
470                    }
471                    (Some(minimum), _) => {
472                        so.number().minimum = Some(*minimum);
473                    }
474                }
475                match (maximum, exclusive_maximum) {
476                    (None, Some(true)) => {
477                        todo!("exclusive_maximum set without maximum");
478                    }
479                    (None, _) => (),
480                    (Some(maximum), Some(true)) => {
481                        so.number().exclusive_maximum = Some(*maximum);
482                    }
483                    (Some(maximum), _) => {
484                        so.number().maximum = Some(*maximum);
485                    }
486                }
487
488                // Object
489                if !properties.is_empty() {
490                    so.object().properties = properties.convert();
491                }
492                if !required.is_empty() {
493                    so.object().required = required.convert();
494                }
495                if additional_properties.is_some() {
496                    so.object().additional_properties = additional_properties.convert();
497                }
498                if let Some(min_properties) = min_properties {
499                    so.object().min_properties = Some(*min_properties as u32);
500                }
501                if let Some(max_properties) = max_properties {
502                    so.object().max_properties = Some(*max_properties as u32);
503                }
504
505                // Array
506                if items.is_some() {
507                    so.array().items = items.convert().clone().map(SingleOrVec::Single);
508                }
509                if let Some(min_items) = min_items {
510                    so.array().min_items = Some(*min_items as u32);
511                }
512                if let Some(max_items) = max_items {
513                    so.array().max_items = Some(*max_items as u32);
514                }
515                if let Some(unique_items) = unique_items {
516                    so.array().unique_items = Some(*unique_items);
517                }
518
519                // Subschemas
520                if !one_of.is_empty() {
521                    so.subschemas().one_of = one_of.convert();
522                }
523                if !all_of.is_empty() {
524                    so.subschemas().all_of = all_of.convert();
525                }
526                if !any_of.is_empty() {
527                    so.subschemas().any_of = any_of.convert();
528                }
529                if not.is_some() {
530                    so.subschemas().not = not.convert();
531                }
532
533                // We do this last so we can infer types if none are specified.
534                match (typ.as_deref(), nullable) {
535                    (Some("boolean"), _) => {
536                        so.instance_type =
537                            instance_type(schemars::schema::InstanceType::Boolean, nullable);
538                    }
539                    (Some("object"), _) => {
540                        so.instance_type =
541                            instance_type(schemars::schema::InstanceType::Object, nullable);
542                    }
543                    (Some("array"), _) => {
544                        so.instance_type =
545                            instance_type(schemars::schema::InstanceType::Array, nullable);
546                    }
547                    (Some("number"), _) => {
548                        so.instance_type =
549                            instance_type(schemars::schema::InstanceType::Number, nullable);
550                    }
551                    (Some("string"), _) => {
552                        so.instance_type =
553                            instance_type(schemars::schema::InstanceType::String, nullable);
554                    }
555                    (Some("integer"), _) => {
556                        so.instance_type =
557                            instance_type(schemars::schema::InstanceType::Integer, nullable);
558                    }
559
560                    (Some(typ), _) => todo!("invalid type: {}", typ),
561
562                    // No types
563                    (None, false) => (),
564
565                    // We only try to infer types if we need to add in an
566                    // additional null type; otherwise we can leave the type
567                    // implied.
568                    (None, true) => {
569                        let instance_types = [
570                            so.object
571                                .is_some()
572                                .then_some(schemars::schema::InstanceType::Object),
573                            so.array
574                                .is_some()
575                                .then_some(schemars::schema::InstanceType::Array),
576                            so.number
577                                .is_some()
578                                .then_some(schemars::schema::InstanceType::Array),
579                            so.string
580                                .is_some()
581                                .then_some(schemars::schema::InstanceType::Array),
582                            nullable.then_some(schemars::schema::InstanceType::Null),
583                        ]
584                        .into_iter()
585                        .flatten()
586                        .collect::<Vec<_>>();
587
588                        // TODO we could also look at enumerated values to
589                        // infer a type.
590
591                        so.instance_type = match (instance_types.first(), instance_types.len()) {
592                            (Some(typ), 1) => Some(SingleOrVec::Single(Box::new(*typ))),
593                            (Some(_), _) => Some(SingleOrVec::Vec(instance_types)),
594                            (None, _) => None,
595                        };
596                    }
597                };
598
599                // If we have exactly one type, and it's null, and we have
600                // subschemas that means that we must have had a bunch of
601                // subschemas *and* nullable = true. In such a case, we remove
602                // the type and inject the oneof wrapper.
603                match &so.instance_type {
604                    Some(SingleOrVec::Single(it))
605                        if **it == schemars::schema::InstanceType::Null
606                            && so.subschemas.is_some() =>
607                    {
608                        oneof_nullable_wrapper(
609                            schemars::schema::SchemaObject {
610                                instance_type: None,
611                                ..so
612                            },
613                            nullable,
614                        )
615                    }
616                    _ => so,
617                }
618            }
619        }
620        .into()
621    }
622}
623
624impl<T> Convert<Option<String>> for openapiv3::VariantOrUnknownOrEmpty<T>
625where
626    T: Convert<String>,
627{
628    fn convert(&self) -> Option<String> {
629        match self {
630            openapiv3::VariantOrUnknownOrEmpty::Item(i) => Some(i.convert()),
631            openapiv3::VariantOrUnknownOrEmpty::Unknown(s) => Some(s.clone()),
632            openapiv3::VariantOrUnknownOrEmpty::Empty => None,
633        }
634    }
635}
636
637impl Convert<String> for openapiv3::StringFormat {
638    fn convert(&self) -> String {
639        match self {
640            openapiv3::StringFormat::Date => "date",
641            openapiv3::StringFormat::DateTime => "date-time",
642            openapiv3::StringFormat::Password => "password",
643            openapiv3::StringFormat::Byte => "byte",
644            openapiv3::StringFormat::Binary => "binary",
645        }
646        .to_string()
647    }
648}
649
650impl Convert<String> for openapiv3::NumberFormat {
651    fn convert(&self) -> String {
652        match self {
653            openapiv3::NumberFormat::Float => "float",
654            openapiv3::NumberFormat::Double => "double",
655        }
656        .to_string()
657    }
658}
659
660impl Convert<String> for openapiv3::IntegerFormat {
661    fn convert(&self) -> String {
662        match self {
663            openapiv3::IntegerFormat::Int32 => "int32",
664            openapiv3::IntegerFormat::Int64 => "int64",
665        }
666        .to_string()
667    }
668}
669
670impl Convert<Value> for Option<String> {
671    fn convert(&self) -> Value {
672        match self {
673            Some(value) => Value::String(value.clone()),
674            None => Value::Null,
675        }
676    }
677}
678
679impl Convert<Value> for Option<f64> {
680    fn convert(&self) -> Value {
681        match self {
682            Some(value) => Value::Number(serde_json::Number::from_f64(*value).unwrap()),
683            None => Value::Null,
684        }
685    }
686}
687impl Convert<Value> for Option<i64> {
688    fn convert(&self) -> Value {
689        match self {
690            Some(value) => Value::Number(serde_json::Number::from(*value)),
691            None => Value::Null,
692        }
693    }
694}
695impl Convert<Value> for Option<bool> {
696    fn convert(&self) -> Value {
697        match self {
698            Some(value) => Value::Bool(*value),
699            None => Value::Null,
700        }
701    }
702}
703
704fn instance_type(
705    instance_type: schemars::schema::InstanceType,
706    nullable: bool,
707) -> Option<schemars::schema::SingleOrVec<schemars::schema::InstanceType>> {
708    if nullable {
709        Some(vec![instance_type, schemars::schema::InstanceType::Null].into())
710    } else {
711        Some(instance_type.into())
712    }
713}
714
715fn oneof_nullable_wrapper(
716    mut schema: schemars::schema::SchemaObject,
717    nullable: bool,
718) -> schemars::schema::SchemaObject {
719    if nullable {
720        let metadata = schema.metadata;
721        let extensions = schema.extensions;
722
723        schema.metadata = None;
724        schema.extensions = Default::default();
725
726        schemars::schema::SchemaObject {
727            metadata,
728            extensions,
729            subschemas: Some(Box::new(schemars::schema::SubschemaValidation {
730                one_of: Some(vec![
731                    schemars::schema::SchemaObject {
732                        instance_type: Some(schemars::schema::InstanceType::Null.into()),
733                        ..Default::default()
734                    }
735                    .into(),
736                    schema.into(),
737                ]),
738                ..Default::default()
739            })),
740            ..Default::default()
741        }
742    } else {
743        schema
744    }
745}
746
747impl Convert<Option<u32>> for Option<usize> {
748    fn convert(&self) -> Option<u32> {
749        (*self).map(|m| m as u32)
750    }
751}
752
753impl Convert<Option<f64>> for Option<f64> {
754    fn convert(&self) -> Option<f64> {
755        *self
756    }
757}
758
759impl Convert<schemars::Set<String>> for Vec<String> {
760    fn convert(&self) -> schemars::Set<String> {
761        self.iter().cloned().collect()
762    }
763}
764
765impl Convert<schemars::Map<String, schemars::schema::Schema>>
766    for IndexMap<String, openapiv3::ReferenceOr<Box<openapiv3::Schema>>>
767{
768    fn convert(&self) -> schemars::Map<String, schemars::schema::Schema> {
769        self.iter().map(|(k, v)| (k.clone(), v.convert())).collect()
770    }
771}
772
773impl<T, TT> Convert<TT> for Box<T>
774where
775    T: Convert<TT>,
776{
777    fn convert(&self) -> TT {
778        self.as_ref().convert()
779    }
780}
781
782impl<T, TT> Convert<Option<Box<TT>>> for Option<T>
783where
784    T: Convert<TT>,
785{
786    fn convert(&self) -> Option<Box<TT>> {
787        self.as_ref().map(|t| Box::new(t.convert()))
788    }
789}
790
791impl Convert<schemars::schema::Schema> for openapiv3::AdditionalProperties {
792    fn convert(&self) -> schemars::schema::Schema {
793        match self {
794            openapiv3::AdditionalProperties::Any(b) => schemars::schema::Schema::Bool(*b),
795            openapiv3::AdditionalProperties::Schema(schema) => schema.convert(),
796        }
797    }
798}
799
800trait OptionReduce {
801    fn reduce(self) -> Self;
802}
803
804// If an Option is `Some` of it's default value, we can simplify that to `None`
805impl<T> OptionReduce for Option<T>
806where
807    T: Default + PartialEq + std::fmt::Debug,
808{
809    fn reduce(self) -> Self {
810        match &self {
811            Some(s) if s != &T::default() => self,
812            _ => None,
813        }
814    }
815}
816
817#[cfg(test)]
818mod tests {
819    use serde_json::json;
820
821    use crate::to_schema::Convert;
822
823    #[test]
824    fn test_null() {
825        let schema_value = json!({ "enum": [null] });
826        let oa_schema = serde_json::from_value::<openapiv3::Schema>(schema_value).unwrap();
827
828        let schema = oa_schema.convert();
829        assert_eq!(
830            schema.into_object().instance_type,
831            Some(schemars::schema::SingleOrVec::Single(Box::new(
832                schemars::schema::InstanceType::Null
833            )))
834        );
835    }
836
837    #[test]
838    fn test_weird_integer() {
839        let schema_value = json!({
840            "type": "integer",
841            "minimum": 0.0,
842        });
843        let oa_schema = serde_json::from_value::<openapiv3::Schema>(schema_value.clone()).unwrap();
844        let js_schema = serde_json::from_value::<schemars::schema::Schema>(schema_value).unwrap();
845
846        let conv_schema = oa_schema.convert();
847        assert_eq!(conv_schema, js_schema);
848    }
849
850    #[test]
851    fn test_object_no_type() {
852        let schema_value = json!({
853            "properties": {
854                "foo": {}
855            }
856        });
857        let oa_schema = serde_json::from_value::<openapiv3::Schema>(schema_value.clone()).unwrap();
858        let js_schema = serde_json::from_value::<schemars::schema::Schema>(schema_value).unwrap();
859
860        let conv_schema = oa_schema.convert();
861        assert_eq!(conv_schema, js_schema);
862    }
863
864    #[test]
865    fn test_array_no_type() {
866        let schema_value = json!({
867            "items": {}
868        });
869        let oa_schema = serde_json::from_value::<openapiv3::Schema>(schema_value.clone()).unwrap();
870        let js_schema = serde_json::from_value::<schemars::schema::Schema>(schema_value).unwrap();
871
872        let conv_schema = oa_schema.convert();
873        assert_eq!(conv_schema, js_schema);
874    }
875
876    #[test]
877    fn test_number_no_type() {
878        let schema_value = json!({
879            "minimum": 100.0
880        });
881        let oa_schema = serde_json::from_value::<openapiv3::Schema>(schema_value.clone()).unwrap();
882        let js_schema = serde_json::from_value::<schemars::schema::Schema>(schema_value).unwrap();
883
884        let conv_schema = oa_schema.convert();
885        assert_eq!(conv_schema, js_schema);
886    }
887
888    #[test]
889    fn test_solo_enum() {
890        let schema_value = json!({
891            "enum": ["one"]
892        });
893        let oa_schema = serde_json::from_value::<openapiv3::Schema>(schema_value.clone()).unwrap();
894        let js_schema = serde_json::from_value::<schemars::schema::Schema>(schema_value).unwrap();
895
896        let conv_schema = oa_schema.convert();
897        assert_eq!(conv_schema, js_schema);
898    }
899}