Skip to main content

salvo_oapi_macros/
lib.rs

1//! This is **private** salvo_oapi codegen library and is not used alone.
2//!
3//! The library contains macro implementations for salvo_oapi library. Content
4//! of the library documentation is available through **salvo_oapi** library itself.
5//! Consider browsing via the **salvo_oapi** crate so all links will work correctly.
6
7#![doc(html_favicon_url = "https://salvo.rs/favicon-32x32.png")]
8#![doc(html_logo_url = "https://salvo.rs/images/logo.svg")]
9#![cfg_attr(docsrs, feature(doc_cfg))]
10
11use proc_macro::TokenStream;
12use quote::ToTokens;
13use syn::parse::{Parse, ParseStream};
14use syn::token::Bracket;
15use syn::{Ident, Item, Token, bracketed, parse_macro_input};
16
17#[macro_use]
18mod cfg;
19mod attribute;
20pub(crate) mod bound;
21mod component;
22mod doc_comment;
23mod endpoint;
24pub(crate) mod feature;
25mod operation;
26mod parameter;
27pub(crate) mod parse_utils;
28mod response;
29mod schema;
30mod schema_type;
31mod security_requirement;
32mod server;
33mod shared;
34mod type_tree;
35
36pub(crate) use proc_macro2_diagnostics::{Diagnostic, Level as DiagLevel};
37pub(crate) use salvo_serde_util::{self as serde_util, RenameRule, SerdeContainer, SerdeValue};
38
39pub(crate) use self::component::{ComponentSchema, ComponentSchemaProps};
40pub(crate) use self::endpoint::EndpointAttr;
41pub(crate) use self::feature::Feature;
42pub(crate) use self::operation::Operation;
43pub(crate) use self::parameter::Parameter;
44pub(crate) use self::response::Response;
45pub(crate) use self::server::Server;
46pub(crate) use self::shared::*;
47pub(crate) use self::type_tree::TypeTree;
48
49/// Enhanced of [handler][handler] for generate OpenAPI documentation, [Read more][more].
50///
51/// [handler]: ../salvo_core/attr.handler.html
52/// [more]: ../salvo_oapi/endpoint/index.html
53#[proc_macro_attribute]
54pub fn endpoint(attr: TokenStream, input: TokenStream) -> TokenStream {
55    let attr = syn::parse_macro_input!(attr as EndpointAttr);
56    let item = parse_macro_input!(input as Item);
57    match endpoint::generate(attr, item) {
58        Ok(stream) => stream.into(),
59        Err(e) => e.to_compile_error().into(),
60    }
61}
62/// This is `#[derive]` implementation for [`ToSchema`][to_schema] trait, [Read more][more].
63///
64/// [to_schema]: ../salvo_oapi/trait.ToSchema.html
65/// [more]: ../salvo_oapi/derive.ToSchema.html
66#[proc_macro_derive(ToSchema, attributes(salvo))] //attributes(schema)
67pub fn derive_to_schema(input: TokenStream) -> TokenStream {
68    match schema::to_schema(syn::parse_macro_input!(input)) {
69        Ok(stream) => stream.into(),
70        Err(e) => e.emit_as_item_tokens().into(),
71    }
72}
73
74/// Generate parameters from struct's fields, [Read more][more].
75///
76/// [more]: ../salvo_oapi/derive.ToParameters.html
77#[proc_macro_derive(ToParameters, attributes(salvo))] //attributes(parameter, parameters)
78pub fn derive_to_parameters(input: TokenStream) -> TokenStream {
79    match parameter::to_parameters(syn::parse_macro_input!(input)) {
80        Ok(stream) => stream.into(),
81        Err(e) => e.emit_as_item_tokens().into(),
82    }
83}
84
85/// Generate reusable [OpenApi][openapi] response, [Read more][more].
86///
87/// [openapi]: ../salvo_oapi/struct.OpenApi.html
88/// [more]: ../salvo_oapi/derive.ToResponse.html
89#[proc_macro_derive(ToResponse, attributes(salvo))] //attributes(response, content, schema))
90pub fn derive_to_response(input: TokenStream) -> TokenStream {
91    match response::to_response(syn::parse_macro_input!(input)) {
92        Ok(stream) => stream.into(),
93        Err(e) => e.emit_as_item_tokens().into(),
94    }
95}
96
97/// Generate responses with status codes what can be used in [OpenAPI][openapi], [Read more][more].
98///
99/// [openapi]: ../salvo_oapi/struct.OpenApi.html
100/// [more]: ../salvo_oapi/derive.ToResponses.html
101#[proc_macro_derive(ToResponses, attributes(salvo))] //attributes(response, schema, ref_response, response))
102pub fn to_responses(input: TokenStream) -> TokenStream {
103    match response::to_responses(syn::parse_macro_input!(input)) {
104        Ok(stream) => stream.into(),
105        Err(e) => e.emit_as_item_tokens().into(),
106    }
107}
108
109#[doc(hidden)]
110#[proc_macro]
111pub fn schema(input: TokenStream) -> TokenStream {
112    struct Schema {
113        inline: bool,
114        ty: syn::Type,
115    }
116    impl Parse for Schema {
117        fn parse(input: ParseStream) -> syn::Result<Self> {
118            let inline = if input.peek(Token![#]) && input.peek2(Bracket) {
119                input.parse::<Token![#]>()?;
120
121                let inline;
122                bracketed!(inline in input);
123                let i = inline.parse::<Ident>()?;
124                i == "inline"
125            } else {
126                false
127            };
128
129            let ty = input.parse()?;
130            Ok(Self { inline, ty })
131        }
132    }
133
134    let schema = syn::parse_macro_input!(input as Schema);
135    let type_tree = match TypeTree::from_type(&schema.ty) {
136        Ok(type_tree) => type_tree,
137        Err(diag) => return diag.emit_as_item_tokens().into(),
138    };
139
140    let stream = ComponentSchema::new(ComponentSchemaProps {
141        features: Some(vec![Feature::Inline(schema.inline.into())]),
142        type_tree: &type_tree,
143        deprecated: None,
144        description: None,
145        object_name: "",
146    })
147    .map(|s| s.to_token_stream());
148    match stream {
149        Ok(stream) => stream.into(),
150        Err(diag) => diag.emit_as_item_tokens().into(),
151    }
152}
153
154pub(crate) trait IntoInner<T> {
155    fn into_inner(self) -> T;
156}
157
158#[cfg(test)]
159mod tests {
160    use quote::quote;
161    use syn::parse2;
162
163    use super::*;
164
165    #[test]
166    fn test_endpoint_for_fn() {
167        let input = quote! {
168            #[endpoint]
169            async fn hello() {
170                res.render_plain_text("Hello World");
171            }
172        };
173        let item = parse2(input).unwrap();
174        assert_eq!(
175            endpoint::generate(parse2(quote! {}).unwrap(), item)
176                .unwrap()
177                .to_string(),
178            quote! {
179                #[allow(non_camel_case_types)]
180                #[derive(Debug)]
181                struct hello;
182                impl hello {
183                    async fn hello() {
184                        {res.render_plain_text("Hello World");}
185                    }
186                }
187                #[salvo::async_trait]
188                impl salvo::Handler for hello {
189                    async fn handle(
190                        &self,
191                        __macro_gen_req: &mut salvo::Request,
192                        __macro_gen_depot: &mut salvo::Depot,
193                        __macro_gen_res: &mut salvo::Response,
194                        __macro_gen_ctrl: &mut salvo::FlowCtrl
195                    ) {
196                        Self::hello().await
197                    }
198                }
199                fn __macro_gen_oapi_endpoint_type_id_hello() -> ::std::any::TypeId {
200                    ::std::any::TypeId::of::<hello>()
201                }
202                fn __macro_gen_oapi_endpoint_creator_hello() -> salvo::oapi::Endpoint {
203                    let mut components = salvo::oapi::Components::new();
204                    let status_codes: &[salvo::http::StatusCode] = &[];
205                    let mut operation = salvo::oapi::Operation::new();
206                    if operation.operation_id.is_none() {
207                        operation.operation_id = Some(salvo::oapi::naming::assign_name::<hello>(salvo::oapi::naming::NameRule::Auto));
208                    }
209                    if !status_codes.is_empty() {
210                        let responses = std::ops::DerefMut::deref_mut(&mut operation.responses);
211                        responses.retain(|k, _| {
212                            if let Ok(code) = <salvo::http::StatusCode as std::str::FromStr>::from_str(k) {
213                                status_codes.contains(&code)
214                            } else {
215                                true
216                            }
217                        });
218                    }
219                    salvo::oapi::Endpoint {
220                        operation,
221                        components,
222                    }
223                }
224                salvo::oapi::__private::inventory::submit! {
225                    salvo::oapi::EndpointRegistry::save(__macro_gen_oapi_endpoint_type_id_hello, __macro_gen_oapi_endpoint_creator_hello)
226                }
227            }
228            .to_string()
229        );
230    }
231
232    #[test]
233    fn test_to_schema_struct() {
234        let input = quote! {
235            /// This is user.
236            ///
237            /// This is user description.
238            #[derive(ToSchema)]
239            struct User {
240                #[salvo(schema(examples("chris"), min_length = 1, max_length = 100, required))]
241                name: String,
242                #[salvo(schema(example = 16, default = 0, maximum=100, minimum=0,format = "int32"))]
243                age: i32,
244                #[deprecated = "There is deprecated"]
245                high: u32,
246            }
247        };
248        assert_eq!(
249            schema::to_schema(parse2(input).unwrap()).unwrap()
250                .to_string(),
251            quote! {
252                impl salvo::oapi::ToSchema for User {
253                    fn to_schema(components: &mut salvo::oapi::Components) -> salvo::oapi::RefOr<salvo::oapi::schema::Schema> {
254                        let name = salvo::oapi::naming::assign_name::<User>(salvo::oapi::naming::NameRule::Auto);
255                        let ref_or = salvo::oapi::RefOr::Ref(salvo::oapi::Ref::new(format!("#/components/schemas/{}", name)));
256                        if !components.schemas.contains_key(&name) {
257                            components.schemas.insert(name.clone(), ref_or.clone());
258                            let schema = salvo::oapi::Object::new()
259                                .property(
260                                    "name",
261                                    salvo::oapi::Object::new()
262                                        .schema_type(salvo::oapi::schema::SchemaType::basic(salvo::oapi::schema::BasicType::String))
263                                        .examples([salvo::oapi::__private::serde_json::json!("chris"),])
264                                        .min_length(1usize)
265                                        .max_length(100usize)
266                                )
267                                .required("name")
268                                .property(
269                                    "age",
270                                    salvo::oapi::Object::new()
271                                        .schema_type(salvo::oapi::schema::SchemaType::basic(salvo::oapi::schema::BasicType::Integer))
272                                        .format(salvo::oapi::SchemaFormat::KnownFormat(salvo::oapi::KnownFormat::Int32))
273                                        .example(salvo::oapi::__private::serde_json::json!(16))
274                                        .default_value(salvo::oapi::__private::serde_json::json!(0))
275                                        .maximum(100f64)
276                                        .minimum(0f64)
277                                        .format(salvo::oapi::SchemaFormat::Custom(String::from("int32")))
278                                )
279                                .required("age")
280                                .property(
281                                    "high",
282                                    salvo::oapi::Object::new()
283                                        .schema_type(salvo::oapi::schema::SchemaType::basic(salvo::oapi::schema::BasicType::Integer))
284                                        .format(salvo::oapi::SchemaFormat::KnownFormat(salvo::oapi::KnownFormat::UInt32))
285                                        .deprecated(salvo::oapi::Deprecated::True)
286                                        .minimum(0f64)
287                                )
288                                .required("high")
289                                .description("This is user.\n\nThis is user description.");
290                            components.schemas.insert(name, schema);
291                        }
292                        ref_or
293                    }
294                }
295            } .to_string()
296        );
297    }
298
299    #[test]
300    fn test_to_schema_generics() {
301        let input = quote! {
302            #[derive(Serialize, Deserialize, ToSchema, Debug)]
303            #[salvo(schema(aliases(MyI32 = MyObject<i32>, MyStr = MyObject<String>)))]
304            struct MyObject<T: ToSchema + std::fmt::Debug + 'static> {
305                value: T,
306            }
307        };
308        assert_eq!(
309            schema::to_schema(parse2(input).unwrap()).unwrap()
310                .to_string().replace("< ", "<").replace("> ", ">"),
311            quote! {
312                impl<T: ToSchema + std::fmt::Debug + 'static> salvo::oapi::ToSchema for MyObject<T>
313                where
314                    T: salvo::oapi::ToSchema + 'static
315                {
316                    fn to_schema(components: &mut salvo::oapi::Components) -> salvo::oapi::RefOr<salvo::oapi::schema::Schema> {
317                        let mut name = None;
318                        if ::std::any::TypeId::of::<Self>() == ::std::any::TypeId::of::<MyObject<i32>>() {
319                            name = Some(salvo::oapi::naming::assign_name::<MyObject<i32>>(
320                                salvo::oapi::naming::NameRule::Force("MyI32")
321                            ));
322                        }
323                        if ::std::any::TypeId::of::<Self>() == ::std::any::TypeId::of::<MyObject<String>>() {
324                            name = Some(salvo::oapi::naming::assign_name::<MyObject<String>>(
325                                salvo::oapi::naming::NameRule::Force("MyStr")
326                            ));
327                        }
328                        let name = name
329                            .unwrap_or_else(|| salvo::oapi::naming::assign_name::<MyObject<T>>(salvo::oapi::naming::NameRule::Auto));
330                        let ref_or = salvo::oapi::RefOr::Ref(salvo::oapi::Ref::new(format!("#/components/schemas/{}", name)));
331                        if !components.schemas.contains_key(&name) {
332                            components.schemas.insert(name.clone(), ref_or.clone());
333                            let schema = salvo::oapi::Object::new()
334                                .property(
335                                    "value",
336                                    salvo::oapi::RefOr::from(<T as salvo::oapi::ToSchema>::to_schema(components))
337                                )
338                                .required("value");
339                            components.schemas.insert(name, schema);
340                        }
341                        ref_or
342                    }
343                }
344            } .to_string().replace("< ", "<").replace("> ", ">")
345        );
346    }
347
348    #[test]
349    fn test_to_schema_enum() {
350        let input = quote! {
351            #[derive(Serialize, Deserialize, ToSchema, Debug)]
352            #[salvo(schema(rename_all = "camelCase"))]
353            enum People {
354                Man,
355                Woman,
356            }
357        };
358        assert_eq!(
359            schema::to_schema(parse2(input).unwrap()).unwrap()
360                .to_string(),
361            quote! {
362                impl salvo::oapi::ToSchema for People {
363                    fn to_schema(components: &mut salvo::oapi::Components) -> salvo::oapi::RefOr<salvo::oapi::schema::Schema> {
364                        let name = salvo::oapi::naming::assign_name::<People>(salvo::oapi::naming::NameRule::Auto);
365                        let ref_or = salvo::oapi::RefOr::Ref(salvo::oapi::Ref::new(format!("#/components/schemas/{}", name)));
366                        if !components.schemas.contains_key(&name) {
367                            components.schemas.insert(name.clone(), ref_or.clone());
368                            let schema = salvo::oapi::Object::new()
369                                .schema_type(salvo::oapi::schema::SchemaType::basic(salvo::oapi::schema::BasicType::String))
370                                .enum_values::<[&str; 2usize], &str>(["man", "woman",]);
371                            components.schemas.insert(name, schema);
372                        }
373                        ref_or
374                    }
375                }
376            } .to_string()
377        );
378    }
379
380    #[test]
381    fn test_to_response() {
382        let input = quote! {
383            #[derive(ToResponse)]
384            #[salvo(response(description = "Person response returns single Person entity"))]
385            struct User{
386                name: String,
387                age: i32,
388            }
389        };
390        assert_eq!(
391            response::to_response(parse2(input).unwrap()).unwrap()
392                .to_string(),
393            quote! {
394                impl salvo::oapi::ToResponse for User {
395                    fn to_response(
396                        components: &mut salvo::oapi::Components
397                    ) -> salvo::oapi::RefOr<salvo::oapi::Response> {
398                        let response = salvo::oapi::Response::new("Person response returns single Person entity").add_content(
399                            "application/json",
400                            salvo::oapi::Content::new(
401                                salvo::oapi::Object::new()
402                                    .property(
403                                        "name",
404                                        salvo::oapi::Object::new().schema_type(salvo::oapi::schema::SchemaType::basic(salvo::oapi::schema::BasicType::String))
405                                    )
406                                    .required("name")
407                                    .property(
408                                        "age",
409                                        salvo::oapi::Object::new()
410                                            .schema_type(salvo::oapi::schema::SchemaType::basic(salvo::oapi::schema::BasicType::Integer))
411                                            .format(salvo::oapi::SchemaFormat::KnownFormat(
412                                                salvo::oapi::KnownFormat::Int32
413                                            ))
414                                    )
415                                    .required("age")
416                            )
417                        );
418                        components.responses.insert("User", response);
419                        salvo::oapi::RefOr::Ref(salvo::oapi::Ref::new(format!("#/components/responses/{}", "User")))
420                    }
421                }
422                impl salvo::oapi::EndpointOutRegister for User {
423                    fn register(components: &mut salvo::oapi::Components, operation: &mut salvo::oapi::Operation) {
424                        operation
425                            .responses
426                            .insert("200", <Self as salvo::oapi::ToResponse>::to_response(components))
427                    }
428                }
429            } .to_string()
430        );
431    }
432
433    #[test]
434    fn test_to_responses() {
435        let input = quote! {
436            #[derive(salvo_oapi::ToResponses)]
437            enum UserResponses {
438                /// Success response description.
439                #[salvo(response(status_code = 200))]
440                Success { value: String },
441
442                #[salvo(response(status_code = 404))]
443                NotFound,
444
445                #[salvo(response(status_code = 400))]
446                BadRequest(BadRequest),
447
448                #[salvo(response(status_code = 500))]
449                ServerError(Response),
450
451                #[salvo(response(status_code = 418))]
452                TeaPot(Response),
453            }
454        };
455        assert_eq!(
456            response::to_responses(parse2(input).unwrap()).unwrap().to_string(),
457            quote! {
458                impl salvo::oapi::ToResponses for UserResponses {
459                    fn to_responses(components: &mut salvo::oapi::Components) -> salvo::oapi::response::Responses {
460                        [
461                            (
462                                "200",
463                                salvo::oapi::RefOr::from(
464                                    salvo::oapi::Response::new("Success response description.").add_content(
465                                        "application/json",
466                                        salvo::oapi::Content::new(
467                                            salvo::oapi::Object::new()
468                                                .property(
469                                                    "value",
470                                                    salvo::oapi::Object::new().schema_type(salvo::oapi::schema::SchemaType::basic(salvo::oapi::schema::BasicType::String))
471                                                )
472                                                .required("value")
473                                                .description("Success response description.")
474                                        )
475                                    )
476                                )
477                            ),
478                            (
479                                "404",
480                                salvo::oapi::RefOr::from(salvo::oapi::Response::new(""))
481                            ),
482                            (
483                                "400",
484                                salvo::oapi::RefOr::from(salvo::oapi::Response::new("").add_content(
485                                    "application/json",
486                                    salvo::oapi::Content::new(salvo::oapi::RefOr::from(
487                                        <BadRequest as salvo::oapi::ToSchema>::to_schema(components)
488                                    ))
489                                ))
490                            ),
491                            (
492                                "500",
493                                salvo::oapi::RefOr::from(salvo::oapi::Response::new("").add_content(
494                                    "application/json",
495                                    salvo::oapi::Content::new(salvo::oapi::RefOr::from(
496                                        <Response as salvo::oapi::ToSchema>::to_schema(components)
497                                    ))
498                                ))
499                            ),
500                            (
501                                "418",
502                                salvo::oapi::RefOr::from(salvo::oapi::Response::new("").add_content(
503                                    "application/json",
504                                    salvo::oapi::Content::new(salvo::oapi::RefOr::from(
505                                        <Response as salvo::oapi::ToSchema>::to_schema(components)
506                                    ))
507                                ))
508                            ),
509                        ]
510                        .into()
511                    }
512                }
513                impl salvo::oapi::EndpointOutRegister for UserResponses {
514                    fn register(components: &mut salvo::oapi::Components, operation: &mut salvo::oapi::Operation) {
515                        operation
516                            .responses
517                            .append(&mut <Self as salvo::oapi::ToResponses>::to_responses(components));
518                    }
519                }
520            }
521            .to_string()
522        );
523    }
524
525    #[test]
526    fn test_to_parameters() {
527        let input = quote! {
528            #[derive(Deserialize, ToParameters)]
529            struct PetQuery {
530                /// Name of pet
531                name: Option<String>,
532                /// Age of pet
533                age: Option<i32>,
534                /// Kind of pet
535                #[salvo(parameter(inline))]
536                kind: PetKind
537            }
538        };
539        assert_eq!(
540            parameter::to_parameters(parse2(input).unwrap()).unwrap().to_string(),
541            quote! {
542                impl<'__macro_gen_ex> salvo::oapi::ToParameters<'__macro_gen_ex> for PetQuery {
543                    fn to_parameters(components: &mut salvo::oapi::Components) -> salvo::oapi::Parameters {
544                        salvo::oapi::Parameters(
545                            [
546                                salvo::oapi::parameter::Parameter::new("name")
547                                    .description("Name of pet")
548                                    .required(salvo::oapi::Required::False)
549                                    .schema(
550                                        salvo::oapi::Object::new()
551                                            .schema_type(salvo::oapi::schema::SchemaType::from_iter([salvo::oapi::schema::BasicType::String, salvo::oapi::schema::BasicType::Null]))
552                                    ),
553                                salvo::oapi::parameter::Parameter::new("age")
554                                    .description("Age of pet")
555                                    .required(salvo::oapi::Required::False)
556                                    .schema(
557                                        salvo::oapi::Object::new()
558                                            .schema_type(salvo::oapi::schema::SchemaType::from_iter([salvo::oapi::schema::BasicType::Integer, salvo::oapi::schema::BasicType::Null]))
559                                            .format(salvo::oapi::SchemaFormat::KnownFormat(
560                                                salvo::oapi::KnownFormat::Int32
561                                            ))
562                                    ),
563                                salvo::oapi::parameter::Parameter::new("kind")
564                                    .description("Kind of pet")
565                                    .required(salvo::oapi::Required::True)
566                                    .schema(<PetKind as salvo::oapi::ToSchema>::to_schema(components)),
567                            ]
568                            .to_vec()
569                        )
570                    }
571                }
572                impl salvo::oapi::EndpointArgRegister for PetQuery {
573                    fn register(
574                        components: &mut salvo::oapi::Components,
575                        operation: &mut salvo::oapi::Operation,
576                        _arg: &str
577                    ) {
578                        for parameter in <Self as salvo::oapi::ToParameters>::to_parameters(components) {
579                            operation.parameters.insert(parameter);
580                        }
581                    }
582                }
583                impl<'__macro_gen_ex> salvo::Extractible<'__macro_gen_ex> for PetQuery {
584                    fn metadata() -> &'static salvo::extract::Metadata {
585                        static METADATA: ::std::sync::OnceLock<salvo::extract::Metadata> = ::std::sync::OnceLock::new();
586                        METADATA.get_or_init(||
587                            salvo::extract::Metadata::new("PetQuery")
588                                .default_sources(vec![salvo::extract::metadata::Source::new(
589                                    salvo::extract::metadata::SourceFrom::Query,
590                                    salvo::extract::metadata::SourceParser::MultiMap
591                                )])
592                                .fields(vec![
593                                    salvo::extract::metadata::Field::new("name"),
594                                    salvo::extract::metadata::Field::new("age"),
595                                    salvo::extract::metadata::Field::new("kind")
596                                ])
597                        )
598                    }
599                    async fn extract(
600                        req: &'__macro_gen_ex mut salvo::Request,
601                        depot: &'__macro_gen_ex mut salvo::Depot
602                    ) -> ::std::result::Result<Self, impl salvo::Writer + Send + std::fmt::Debug + 'static> {
603                        salvo::serde::from_request(req, depot, Self::metadata()).await
604                    }
605                    async fn extract_with_arg(
606                        req: &'__macro_gen_ex mut salvo::Request,
607                        depot: &'__macro_gen_ex mut salvo::Depot,
608                        _arg: &str
609                    ) -> ::std::result::Result<Self, impl salvo::Writer + Send + std::fmt::Debug + 'static> {
610                        Self::extract(req, depot).await
611                    }
612                }
613            }
614            .to_string()
615        );
616    }
617}