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