salvo_oapi/
lib.rs

1#![doc = include_str!("../docs/lib.md")]
2#![doc(html_favicon_url = "https://salvo.rs/favicon-32x32.png")]
3#![doc(html_logo_url = "https://salvo.rs/images/logo.svg")]
4#![cfg_attr(docsrs, feature(doc_cfg))]
5
6#[macro_use]
7mod cfg;
8
9mod openapi;
10pub use openapi::*;
11
12#[doc = include_str!("../docs/endpoint.md")]
13pub mod endpoint;
14pub use endpoint::{Endpoint, EndpointArgRegister, EndpointOutRegister, EndpointRegistry};
15pub mod extract;
16mod routing;
17pub use routing::RouterExt;
18/// Module for name schemas.
19pub mod naming;
20
21cfg_feature! {
22    #![feature ="swagger-ui"]
23    pub mod swagger_ui;
24}
25cfg_feature! {
26    #![feature ="scalar"]
27    pub mod scalar;
28}
29cfg_feature! {
30    #![feature ="rapidoc"]
31    pub mod rapidoc;
32}
33cfg_feature! {
34    #![feature ="redoc"]
35    pub mod redoc;
36}
37
38#[doc = include_str!("../docs/derive_to_parameters.md")]
39pub use salvo_oapi_macros::ToParameters;
40#[doc = include_str!("../docs/derive_to_response.md")]
41pub use salvo_oapi_macros::ToResponse;
42#[doc = include_str!("../docs/derive_to_responses.md")]
43pub use salvo_oapi_macros::ToResponses;
44#[doc = include_str!("../docs/derive_to_schema.md")]
45pub use salvo_oapi_macros::ToSchema;
46#[doc = include_str!("../docs/endpoint.md")]
47pub use salvo_oapi_macros::endpoint;
48pub(crate) use salvo_oapi_macros::schema;
49
50use std::collections::{BTreeMap, HashMap, LinkedList};
51use std::marker::PhantomData;
52
53use salvo_core::http::StatusError;
54use salvo_core::{extract::Extractible, writing};
55
56use crate::oapi::openapi::schema::OneOf;
57
58// https://github.com/bkchr/proc-macro-crate/issues/10
59extern crate self as salvo_oapi;
60
61/// Trait for implementing OpenAPI Schema object.
62///
63/// Generated schemas can be referenced or reused in path operations.
64///
65/// This trait is derivable and can be used with `[#derive]` attribute. For a details of
66/// `#[derive(ToSchema)]` refer to [derive documentation][derive].
67///
68/// [derive]: derive.ToSchema.html
69///
70/// # Examples
71///
72/// Use `#[derive]` to implement `ToSchema` trait.
73/// ```
74/// use salvo_oapi::ToSchema;
75/// #[derive(ToSchema)]
76/// #[salvo(schema(example = json!({"name": "bob the cat", "id": 1})))]
77/// struct Pet {
78///     id: u64,
79///     name: String,
80///     age: Option<i32>,
81/// }
82/// ```
83///
84/// Following manual implementation is equal to above derive one.
85/// ```
86/// use salvo_oapi::{Components, ToSchema, RefOr, Schema, SchemaFormat, BasicType, SchemaType, KnownFormat, Object};
87/// # struct Pet {
88/// #     id: u64,
89/// #     name: String,
90/// #     age: Option<i32>,
91/// # }
92/// #
93/// impl ToSchema for Pet {
94///     fn to_schema(components: &mut Components) -> RefOr<Schema> {
95///         Object::new()
96///             .property(
97///                 "id",
98///                 Object::new()
99///                     .schema_type(BasicType::Integer)
100///                     .format(SchemaFormat::KnownFormat(
101///                         KnownFormat::Int64,
102///                     )),
103///             )
104///             .required("id")
105///             .property(
106///                 "name",
107///                 Object::new()
108///                     .schema_type(BasicType::String),
109///             )
110///             .required("name")
111///             .property(
112///                 "age",
113///                 Object::new()
114///                     .schema_type(BasicType::Integer)
115///                     .format(SchemaFormat::KnownFormat(
116///                         KnownFormat::Int32,
117///                     )),
118///             )
119///             .example(serde_json::json!({
120///               "name":"bob the cat","id":1
121///             }))
122///             .into()
123///     }
124/// }
125/// ```
126pub trait ToSchema {
127    /// Returns a tuple of name and schema or reference to a schema that can be referenced by the
128    /// name or inlined directly to responses, request bodies or parameters.
129    fn to_schema(components: &mut Components) -> RefOr<schema::Schema>;
130
131    // /// Optional set of alias schemas for the [`ToSchema::schema`].
132    // ///
133    // /// Typically there is no need to manually implement this method but it is instead implemented
134    // /// by derive [`macro@ToSchema`] when `#[aliases(...)]` attribute is defined.
135    // fn aliases() -> Vec<schema::Schema> {
136    //     Vec::new()
137    // }
138}
139
140/// Represents _`nullable`_ type.
141///
142/// This can be used anywhere where "nothing" needs to be evaluated.
143/// This will serialize to _`null`_ in JSON and [`schema::empty`] is used to create the
144/// [`schema::Schema`] for the type.
145pub type TupleUnit = ();
146
147impl ToSchema for TupleUnit {
148    fn to_schema(_components: &mut Components) -> RefOr<schema::Schema> {
149        schema::empty().into()
150    }
151}
152
153macro_rules! impl_to_schema {
154    ($ty:path) => {
155        impl_to_schema!( @impl_schema $ty );
156    };
157    (&$ty:path) => {
158        impl_to_schema!( @impl_schema &$ty );
159    };
160    (@impl_schema $($tt:tt)*) => {
161        impl ToSchema for $($tt)* {
162            fn to_schema(_components: &mut Components) -> crate::RefOr<crate::schema::Schema> {
163                 schema!( $($tt)* ).into()
164            }
165        }
166    };
167}
168
169macro_rules! impl_to_schema_primitive {
170    ($($tt:path),*) => {
171        $( impl_to_schema!( $tt ); )*
172    };
173}
174
175// Create `salvo-oapi` module so we can use `salvo-oapi-macros` directly
176// from `salvo-oapi` crate. ONLY FOR INTERNAL USE!
177#[doc(hidden)]
178pub mod oapi {
179    pub use super::*;
180}
181
182#[doc(hidden)]
183pub mod __private {
184    pub use inventory;
185    pub use serde_json;
186}
187
188#[rustfmt::skip]
189impl_to_schema_primitive!(
190    i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize, bool, f32, f64, String, str, char
191);
192impl_to_schema!(&str);
193
194impl_to_schema!(std::net::Ipv4Addr);
195impl_to_schema!(std::net::Ipv6Addr);
196
197impl ToSchema for std::net::IpAddr {
198    fn to_schema(components: &mut Components) -> RefOr<schema::Schema> {
199        crate::RefOr::Type(Schema::OneOf(
200            OneOf::default()
201                .item(std::net::Ipv4Addr::to_schema(components))
202                .item(std::net::Ipv6Addr::to_schema(components)),
203        ))
204    }
205}
206
207#[cfg(feature = "chrono")]
208impl_to_schema_primitive!(chrono::NaiveDate, chrono::Duration, chrono::NaiveDateTime);
209#[cfg(feature = "chrono")]
210impl<T: chrono::TimeZone> ToSchema for chrono::DateTime<T> {
211    fn to_schema(_components: &mut Components) -> RefOr<schema::Schema> {
212        schema!(#[inline] DateTime<T>).into()
213    }
214}
215#[cfg(feature = "compact_str")]
216impl_to_schema_primitive!(compact_str::CompactString);
217#[cfg(any(feature = "decimal", feature = "decimal-float"))]
218impl_to_schema!(rust_decimal::Decimal);
219#[cfg(feature = "url")]
220impl_to_schema!(url::Url);
221#[cfg(feature = "uuid")]
222impl_to_schema!(uuid::Uuid);
223#[cfg(feature = "ulid")]
224impl_to_schema!(ulid::Ulid);
225#[cfg(feature = "time")]
226impl_to_schema_primitive!(
227    time::Date,
228    time::PrimitiveDateTime,
229    time::OffsetDateTime,
230    time::Duration
231);
232#[cfg(feature = "smallvec")]
233impl<T: ToSchema + smallvec::Array> ToSchema for smallvec::SmallVec<T> {
234    fn to_schema(components: &mut Components) -> RefOr<schema::Schema> {
235        schema!(#[inline] smallvec::SmallVec<T>).into()
236    }
237}
238#[cfg(feature = "indexmap")]
239impl<K: ToSchema, V: ToSchema> ToSchema for indexmap::IndexMap<K, V> {
240    fn to_schema(_components: &mut Components) -> RefOr<schema::Schema> {
241        schema!(#[inline] indexmap::IndexMap<K, V>).into()
242    }
243}
244
245impl<T: ToSchema> ToSchema for Vec<T> {
246    fn to_schema(components: &mut Components) -> RefOr<schema::Schema> {
247        schema!(#[inline] Vec<T>).into()
248    }
249}
250
251impl<T: ToSchema> ToSchema for LinkedList<T> {
252    fn to_schema(components: &mut Components) -> RefOr<schema::Schema> {
253        schema!(#[inline] LinkedList<T>).into()
254    }
255}
256
257impl<T: ToSchema> ToSchema for [T] {
258    fn to_schema(components: &mut Components) -> RefOr<schema::Schema> {
259        schema!(
260            #[inline]
261            [T]
262        )
263        .into()
264    }
265}
266impl<T: ToSchema, const N: usize> ToSchema for [T; N] {
267    fn to_schema(components: &mut Components) -> RefOr<schema::Schema> {
268        schema!(
269            #[inline]
270            [T; N]
271        )
272        .into()
273    }
274}
275
276impl<T: ToSchema> ToSchema for &[T] {
277    fn to_schema(components: &mut Components) -> RefOr<schema::Schema> {
278        schema!(
279            #[inline]
280            &[T]
281        )
282        .into()
283    }
284}
285
286impl<T: ToSchema> ToSchema for Option<T> {
287    fn to_schema(components: &mut Components) -> RefOr<schema::Schema> {
288        schema!(#[inline] Option<T>).into()
289    }
290}
291
292impl<T> ToSchema for PhantomData<T> {
293    fn to_schema(_components: &mut Components) -> RefOr<schema::Schema> {
294        Schema::Object(Box::default()).into()
295    }
296}
297
298impl<K: ToSchema, V: ToSchema> ToSchema for BTreeMap<K, V> {
299    fn to_schema(_components: &mut Components) -> RefOr<schema::Schema> {
300        schema!(#[inline]BTreeMap<K, V>).into()
301    }
302}
303
304impl<K: ToSchema, V: ToSchema> ToSchema for HashMap<K, V> {
305    fn to_schema(_components: &mut Components) -> RefOr<schema::Schema> {
306        schema!(#[inline]HashMap<K, V>).into()
307    }
308}
309
310impl ToSchema for StatusError {
311    fn to_schema(components: &mut Components) -> RefOr<schema::Schema> {
312        let name = crate::naming::assign_name::<Self>(Default::default());
313        let ref_or = crate::RefOr::Ref(crate::Ref::new(format!("#/components/schemas/{name}")));
314        if !components.schemas.contains_key(&name) {
315            components.schemas.insert(name.clone(), ref_or.clone());
316            let schema = Schema::from(
317                Object::new()
318                    .property("code", u16::to_schema(components))
319                    .required("code")
320                    .required("name")
321                    .property("name", String::to_schema(components))
322                    .required("brief")
323                    .property("brief", String::to_schema(components))
324                    .required("detail")
325                    .property("detail", String::to_schema(components))
326                    .property("cause", String::to_schema(components)),
327            );
328            components.schemas.insert(name, schema);
329        }
330        ref_or
331    }
332}
333impl ToSchema for salvo_core::Error {
334    fn to_schema(components: &mut Components) -> RefOr<schema::Schema> {
335        StatusError::to_schema(components)
336    }
337}
338
339impl<T, E> ToSchema for Result<T, E>
340where
341    T: ToSchema,
342    E: ToSchema,
343{
344    fn to_schema(components: &mut Components) -> RefOr<schema::Schema> {
345        let name = crate::naming::assign_name::<StatusError>(Default::default());
346        let ref_or = crate::RefOr::Ref(crate::Ref::new(format!("#/components/schemas/{name}")));
347        if !components.schemas.contains_key(&name) {
348            components.schemas.insert(name.clone(), ref_or.clone());
349            let schema = OneOf::new()
350                .item(T::to_schema(components))
351                .item(E::to_schema(components));
352            components.schemas.insert(name, schema);
353        }
354        ref_or
355    }
356}
357
358impl ToSchema for serde_json::Value {
359    fn to_schema(_components: &mut Components) -> RefOr<schema::Schema> {
360        Schema::Object(Box::default()).into()
361    }
362}
363impl ToSchema for serde_json::Map<String, serde_json::Value> {
364    fn to_schema(_components: &mut Components) -> RefOr<schema::Schema> {
365        schema!(#[inline]HashMap<K, V>).into()
366    }
367}
368
369/// Trait used to convert implementing type to OpenAPI parameters.
370///
371/// This trait is [derivable][derive] for structs which are used to describe `path` or `query` parameters.
372/// For more details of `#[derive(ToParameters)]` refer to [derive documentation][derive].
373///
374/// # Examples
375///
376/// Derive [`ToParameters`] implementation. This example will fail to compile because [`ToParameters`] cannot
377/// be used alone and it need to be used together with endpoint using the params as well. See
378/// [derive documentation][derive] for more details.
379/// ```
380/// use serde::Deserialize;
381/// use salvo_oapi::{ToParameters, EndpointArgRegister, Components, Operation};
382/// use salvo_core::prelude::*;
383///
384/// #[derive(Deserialize, ToParameters)]
385/// struct PetParams {
386///     /// Id of pet
387///     id: i64,
388///     /// Name of pet
389///     name: String,
390/// }
391/// ```
392///
393/// Roughly equal manual implementation of [`ToParameters`] trait.
394/// ```
395/// # use serde::Deserialize;
396/// # use salvo_oapi::{ToParameters, EndpointArgRegister, Components, Operation};
397/// # use salvo_core::prelude::*;
398/// # use salvo_core::extract::{Metadata, Extractible};
399/// #[derive(Deserialize)]
400/// # struct PetParams {
401/// #    /// Id of pet
402/// #    id: i64,
403/// #    /// Name of pet
404/// #    name: String,
405/// # }
406/// impl<'de> salvo_oapi::ToParameters<'de> for PetParams {
407///     fn to_parameters(_components: &mut Components) -> salvo_oapi::Parameters {
408///         salvo_oapi::Parameters::new().parameter(
409///             salvo_oapi::Parameter::new("id")
410///                 .required(salvo_oapi::Required::True)
411///                 .parameter_in(salvo_oapi::ParameterIn::Path)
412///                 .description("Id of pet")
413///                 .schema(
414///                     salvo_oapi::Object::new()
415///                         .schema_type(salvo_oapi::schema::BasicType::Integer)
416///                         .format(salvo_oapi::SchemaFormat::KnownFormat(salvo_oapi::schema::KnownFormat::Int64)),
417///                 ),
418///         ).parameter(
419///             salvo_oapi::Parameter::new("name")
420///                 .required(salvo_oapi::Required::True)
421///                 .parameter_in(salvo_oapi::ParameterIn::Query)
422///                 .description("Name of pet")
423///                 .schema(
424///                     salvo_oapi::Object::new()
425///                         .schema_type(salvo_oapi::schema::BasicType::String),
426///                 ),
427///         )
428///     }
429/// }
430///
431/// impl<'ex> Extractible<'ex> for PetParams {
432///    fn metadata() -> &'static Metadata {
433///      static METADATA: Metadata = Metadata::new("");
434///      &METADATA
435///    }
436///    #[allow(refining_impl_trait)]
437///    async fn extract(req: &'ex mut Request) -> Result<Self, salvo_core::http::ParseError> {
438///        salvo_core::serde::from_request(req, Self::metadata()).await
439///    }
440///    #[allow(refining_impl_trait)]
441///    async fn extract_with_arg(req: &'ex mut Request, _arg: &str) -> Result<Self, salvo_core::http::ParseError> {
442///        Self::extract(req).await
443///    }
444/// }
445///
446/// impl EndpointArgRegister for PetParams {
447///     fn register(components: &mut Components, operation: &mut Operation, _arg: &str) {
448///         operation.parameters.append(&mut PetParams::to_parameters(components));
449///     }
450/// }
451/// ```
452/// [derive]: derive.ToParameters.html
453pub trait ToParameters<'de>: Extractible<'de> {
454    /// Provide [`Vec`] of [`Parameter`]s to caller. The result is used in `salvo-oapi-macros` library to
455    /// provide OpenAPI parameter information for the endpoint using the parameters.
456    fn to_parameters(components: &mut Components) -> Parameters;
457}
458
459/// Trait used to give [`Parameter`] information for OpenAPI.
460pub trait ToParameter {
461    /// Returns a `Parameter`.
462    fn to_parameter(components: &mut Components) -> Parameter;
463}
464
465/// This trait is implemented to document a type (like an enum) which can represent
466/// request body, to be used in operation.
467///
468/// # Examples
469///
470/// ```
471/// use std::collections::BTreeMap;
472/// use serde::Deserialize;
473/// use salvo_oapi::{ToRequestBody, ToSchema, Components, Content, EndpointArgRegister, Operation, RequestBody };
474///
475/// #[derive(ToSchema, Deserialize, Debug)]
476/// struct MyPayload {
477///     name: String,
478/// }
479///
480/// impl ToRequestBody for MyPayload {
481///     fn to_request_body(components: &mut Components) -> RequestBody {
482///         RequestBody::new()
483///             .add_content("application/json", Content::new(MyPayload::to_schema(components)))
484///     }
485/// }
486/// impl EndpointArgRegister for MyPayload {
487///     fn register(components: &mut Components, operation: &mut Operation, _arg: &str) {
488///         operation.request_body = Some(Self::to_request_body(components));
489///     }
490/// }
491/// ```
492pub trait ToRequestBody {
493    /// Returns `RequestBody`.
494    fn to_request_body(components: &mut Components) -> RequestBody;
495}
496
497/// This trait is implemented to document a type (like an enum) which can represent multiple
498/// responses, to be used in operation.
499///
500/// # Examples
501///
502/// ```
503/// use std::collections::BTreeMap;
504/// use salvo_oapi::{Components, Response, Responses, RefOr, ToResponses };
505///
506/// enum MyResponse {
507///     Ok,
508///     NotFound,
509/// }
510///
511/// impl ToResponses for MyResponse {
512///     fn to_responses(_components: &mut Components) -> Responses {
513///         Responses::new()
514///             .response("200", Response::new("Ok"))
515///             .response("404", Response::new("Not Found"))
516///     }
517/// }
518/// ```
519pub trait ToResponses {
520    /// Returns an ordered map of response codes to responses.
521    fn to_responses(components: &mut Components) -> Responses;
522}
523
524impl<C> ToResponses for writing::Json<C>
525where
526    C: ToSchema,
527{
528    fn to_responses(components: &mut Components) -> Responses {
529        Responses::new().response(
530            "200",
531            Response::new("Response json format data")
532                .add_content("application/json", Content::new(C::to_schema(components))),
533        )
534    }
535}
536
537impl ToResponses for StatusError {
538    fn to_responses(components: &mut Components) -> Responses {
539        let mut responses = Responses::new();
540        let errors = vec![
541            Self::bad_request(),
542            Self::unauthorized(),
543            Self::payment_required(),
544            Self::forbidden(),
545            Self::not_found(),
546            Self::method_not_allowed(),
547            Self::not_acceptable(),
548            Self::proxy_authentication_required(),
549            Self::request_timeout(),
550            Self::conflict(),
551            Self::gone(),
552            Self::length_required(),
553            Self::precondition_failed(),
554            Self::payload_too_large(),
555            Self::uri_too_long(),
556            Self::unsupported_media_type(),
557            Self::range_not_satisfiable(),
558            Self::expectation_failed(),
559            Self::im_a_teapot(),
560            Self::misdirected_request(),
561            Self::unprocessable_entity(),
562            Self::locked(),
563            Self::failed_dependency(),
564            Self::upgrade_required(),
565            Self::precondition_required(),
566            Self::too_many_requests(),
567            Self::request_header_fields_toolarge(),
568            Self::unavailable_for_legalreasons(),
569            Self::internal_server_error(),
570            Self::not_implemented(),
571            Self::bad_gateway(),
572            Self::service_unavailable(),
573            Self::gateway_timeout(),
574            Self::http_version_not_supported(),
575            Self::variant_also_negotiates(),
576            Self::insufficient_storage(),
577            Self::loop_detected(),
578            Self::not_extended(),
579            Self::network_authentication_required(),
580        ];
581        for Self { code, brief, .. } in errors {
582            responses.insert(
583                code.as_str(),
584                Response::new(brief).add_content(
585                    "application/json",
586                    Content::new(Self::to_schema(components)),
587                ),
588            )
589        }
590        responses
591    }
592}
593impl ToResponses for salvo_core::Error {
594    fn to_responses(components: &mut Components) -> Responses {
595        StatusError::to_responses(components)
596    }
597}
598
599/// This trait is implemented to document a type which represents a single response which can be
600/// referenced or reused as a component in multiple operations.
601///
602/// _`ToResponse`_ trait can also be derived with [`#[derive(ToResponse)]`][derive].
603///
604/// # Examples
605///
606/// ```
607/// use salvo_oapi::{RefOr, Response, Components, ToResponse};
608///
609/// struct MyResponse;
610/// impl ToResponse for MyResponse {
611///     fn to_response(_components: &mut Components) -> RefOr<Response> {
612///         Response::new("My Response").into()
613///     }
614/// }
615/// ```
616///
617/// [derive]: derive.ToResponse.html
618pub trait ToResponse {
619    /// Returns a tuple of response component name (to be referenced) to a response.
620    fn to_response(components: &mut Components) -> RefOr<crate::Response>;
621}
622
623impl<C> ToResponse for writing::Json<C>
624where
625    C: ToSchema,
626{
627    fn to_response(components: &mut Components) -> RefOr<Response> {
628        let schema = <C as ToSchema>::to_schema(components);
629        Response::new("Response with json format data")
630            .add_content("application/json", Content::new(schema))
631            .into()
632    }
633}
634
635#[cfg(test)]
636mod tests {
637    use assert_json_diff::assert_json_eq;
638    use serde_json::json;
639
640    use super::*;
641
642    #[test]
643    fn test_primitive_schema() {
644        let mut components = Components::new();
645        for (name, schema, value) in [
646            (
647                "i8",
648                i8::to_schema(&mut components),
649                json!({"type": "integer", "format": "int8"}),
650            ),
651            (
652                "i16",
653                i16::to_schema(&mut components),
654                json!({"type": "integer", "format": "int16"}),
655            ),
656            (
657                "i32",
658                i32::to_schema(&mut components),
659                json!({"type": "integer", "format": "int32"}),
660            ),
661            (
662                "i64",
663                i64::to_schema(&mut components),
664                json!({"type": "integer", "format": "int64"}),
665            ),
666            (
667                "i128",
668                i128::to_schema(&mut components),
669                json!({"type": "integer"}),
670            ),
671            (
672                "isize",
673                isize::to_schema(&mut components),
674                json!({"type": "integer"}),
675            ),
676            (
677                "u8",
678                u8::to_schema(&mut components),
679                json!({"type": "integer", "format": "uint8", "minimum": 0.0}),
680            ),
681            (
682                "u16",
683                u16::to_schema(&mut components),
684                json!({"type": "integer", "format": "uint16", "minimum": 0.0}),
685            ),
686            (
687                "u32",
688                u32::to_schema(&mut components),
689                json!({"type": "integer", "format": "uint32", "minimum": 0.0}),
690            ),
691            (
692                "u64",
693                u64::to_schema(&mut components),
694                json!({"type": "integer", "format": "uint64", "minimum": 0.0}),
695            ),
696            (
697                "u128",
698                u128::to_schema(&mut components),
699                json!({"type": "integer", "minimum": 0.0}),
700            ),
701            (
702                "usize",
703                usize::to_schema(&mut components),
704                json!({"type": "integer", "minimum": 0.0 }),
705            ),
706            (
707                "bool",
708                bool::to_schema(&mut components),
709                json!({"type": "boolean"}),
710            ),
711            (
712                "str",
713                str::to_schema(&mut components),
714                json!({"type": "string"}),
715            ),
716            (
717                "String",
718                String::to_schema(&mut components),
719                json!({"type": "string"}),
720            ),
721            (
722                "char",
723                char::to_schema(&mut components),
724                json!({"type": "string"}),
725            ),
726            (
727                "f32",
728                f32::to_schema(&mut components),
729                json!({"type": "number", "format": "float"}),
730            ),
731            (
732                "f64",
733                f64::to_schema(&mut components),
734                json!({"type": "number", "format": "double"}),
735            ),
736        ] {
737            println!(
738                "{name}: {json}",
739                json = serde_json::to_string(&schema).unwrap()
740            );
741            let schema = serde_json::to_value(schema).unwrap();
742            assert_json_eq!(schema, value);
743        }
744    }
745}