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        compose_context: None,
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        let result = schema::to_schema(parse2(input).unwrap())
250            .unwrap()
251            .to_string();
252        // Should contain both ComposeSchema and ToSchema impls
253        assert!(
254            result.contains("impl salvo :: oapi :: ComposeSchema for User"),
255            "Expected ComposeSchema impl in output"
256        );
257        assert!(
258            result.contains("impl salvo :: oapi :: ToSchema for User"),
259            "Expected ToSchema impl in output"
260        );
261        // Verify schema body content
262        assert!(result.contains("\"name\""), "Expected 'name' property");
263        assert!(result.contains("\"age\""), "Expected 'age' property");
264        assert!(result.contains("\"high\""), "Expected 'high' property");
265        assert!(
266            result.contains("This is user.\\n\\nThis is user description."),
267            "Expected description"
268        );
269    }
270
271    #[test]
272    fn test_to_schema_generics() {
273        let input = quote! {
274            #[derive(Serialize, Deserialize, ToSchema, Debug)]
275            #[salvo(schema(aliases(MyI32 = MyObject<i32>, MyStr = MyObject<String>)))]
276            struct MyObject<T: ToSchema + std::fmt::Debug + 'static> {
277                value: T,
278            }
279        };
280        let result = schema::to_schema(parse2(input).unwrap())
281            .unwrap()
282            .to_string()
283            .replace("< ", "<")
284            .replace("> ", ">");
285        // Should contain both ComposeSchema and ToSchema impls
286        assert!(
287            result.contains("salvo :: oapi :: ComposeSchema for MyObject"),
288            "Expected ComposeSchema impl in output"
289        );
290        assert!(
291            result.contains("salvo :: oapi :: ToSchema for MyObject"),
292            "Expected ToSchema impl in output"
293        );
294        // ComposeSchema should use __compose_generics for generic param T
295        assert!(
296            result.contains("__compose_generics"),
297            "Expected __compose_generics usage in ComposeSchema impl"
298        );
299        // ToSchema should still use ToSchema::to_schema for type aliases
300        assert!(result.contains("MyI32"), "Expected MyI32 alias");
301        assert!(result.contains("MyStr"), "Expected MyStr alias");
302    }
303
304    #[test]
305    fn test_to_schema_enum() {
306        let input = quote! {
307            #[derive(Serialize, Deserialize, ToSchema, Debug)]
308            #[salvo(schema(rename_all = "camelCase"))]
309            enum People {
310                Man,
311                Woman,
312            }
313        };
314        let result = schema::to_schema(parse2(input).unwrap())
315            .unwrap()
316            .to_string();
317        // Should contain both ComposeSchema and ToSchema impls
318        assert!(
319            result.contains("impl salvo :: oapi :: ComposeSchema for People"),
320            "Expected ComposeSchema impl in output"
321        );
322        assert!(
323            result.contains("impl salvo :: oapi :: ToSchema for People"),
324            "Expected ToSchema impl in output"
325        );
326        // Verify enum values
327        assert!(result.contains("\"man\""), "Expected 'man' variant");
328        assert!(result.contains("\"woman\""), "Expected 'woman' variant");
329    }
330
331    #[test]
332    fn test_to_response() {
333        let input = quote! {
334            #[derive(ToResponse)]
335            #[salvo(response(description = "Person response returns single Person entity"))]
336            struct User{
337                name: String,
338                age: i32,
339            }
340        };
341        assert_eq!(
342            response::to_response(parse2(input).unwrap()).unwrap()
343                .to_string(),
344            quote! {
345                impl salvo::oapi::ToResponse for User {
346                    fn to_response(
347                        components: &mut salvo::oapi::Components
348                    ) -> salvo::oapi::RefOr<salvo::oapi::Response> {
349                        let response = salvo::oapi::Response::new("Person response returns single Person entity").add_content(
350                            "application/json",
351                            salvo::oapi::Content::new(
352                                salvo::oapi::Object::new()
353                                    .property(
354                                        "name",
355                                        salvo::oapi::Object::new().schema_type(salvo::oapi::schema::SchemaType::basic(salvo::oapi::schema::BasicType::String))
356                                    )
357                                    .required("name")
358                                    .property(
359                                        "age",
360                                        salvo::oapi::Object::new()
361                                            .schema_type(salvo::oapi::schema::SchemaType::basic(salvo::oapi::schema::BasicType::Integer))
362                                            .format(salvo::oapi::SchemaFormat::KnownFormat(
363                                                salvo::oapi::KnownFormat::Int32
364                                            ))
365                                    )
366                                    .required("age")
367                            )
368                        );
369                        components.responses.insert("User", response);
370                        salvo::oapi::RefOr::Ref(salvo::oapi::Ref::new(format!("#/components/responses/{}", "User")))
371                    }
372                }
373                impl salvo::oapi::EndpointOutRegister for User {
374                    fn register(components: &mut salvo::oapi::Components, operation: &mut salvo::oapi::Operation) {
375                        operation
376                            .responses
377                            .insert("200", <Self as salvo::oapi::ToResponse>::to_response(components))
378                    }
379                }
380            } .to_string()
381        );
382    }
383
384    #[test]
385    fn test_to_responses() {
386        let input = quote! {
387            #[derive(salvo_oapi::ToResponses)]
388            enum UserResponses {
389                /// Success response description.
390                #[salvo(response(status_code = 200))]
391                Success { value: String },
392
393                #[salvo(response(status_code = 404))]
394                NotFound,
395
396                #[salvo(response(status_code = 400))]
397                BadRequest(BadRequest),
398
399                #[salvo(response(status_code = 500))]
400                ServerError(Response),
401
402                #[salvo(response(status_code = 418))]
403                TeaPot(Response),
404            }
405        };
406        assert_eq!(
407            response::to_responses(parse2(input).unwrap()).unwrap().to_string(),
408            quote! {
409                impl salvo::oapi::ToResponses for UserResponses {
410                    fn to_responses(components: &mut salvo::oapi::Components) -> salvo::oapi::response::Responses {
411                        [
412                            (
413                                "200",
414                                salvo::oapi::RefOr::from(
415                                    salvo::oapi::Response::new("Success response description.").add_content(
416                                        "application/json",
417                                        salvo::oapi::Content::new(
418                                            salvo::oapi::Object::new()
419                                                .property(
420                                                    "value",
421                                                    salvo::oapi::Object::new().schema_type(salvo::oapi::schema::SchemaType::basic(salvo::oapi::schema::BasicType::String))
422                                                )
423                                                .required("value")
424                                                .description("Success response description.")
425                                        )
426                                    )
427                                )
428                            ),
429                            (
430                                "404",
431                                salvo::oapi::RefOr::from(salvo::oapi::Response::new(""))
432                            ),
433                            (
434                                "400",
435                                salvo::oapi::RefOr::from(salvo::oapi::Response::new("").add_content(
436                                    "application/json",
437                                    salvo::oapi::Content::new(salvo::oapi::RefOr::from(
438                                        <BadRequest as salvo::oapi::ToSchema>::to_schema(components)
439                                    ))
440                                ))
441                            ),
442                            (
443                                "500",
444                                salvo::oapi::RefOr::from(salvo::oapi::Response::new("").add_content(
445                                    "application/json",
446                                    salvo::oapi::Content::new(salvo::oapi::RefOr::from(
447                                        <Response as salvo::oapi::ToSchema>::to_schema(components)
448                                    ))
449                                ))
450                            ),
451                            (
452                                "418",
453                                salvo::oapi::RefOr::from(salvo::oapi::Response::new("").add_content(
454                                    "application/json",
455                                    salvo::oapi::Content::new(salvo::oapi::RefOr::from(
456                                        <Response as salvo::oapi::ToSchema>::to_schema(components)
457                                    ))
458                                ))
459                            ),
460                        ]
461                        .into()
462                    }
463                }
464                impl salvo::oapi::EndpointOutRegister for UserResponses {
465                    fn register(components: &mut salvo::oapi::Components, operation: &mut salvo::oapi::Operation) {
466                        operation
467                            .responses
468                            .append(&mut <Self as salvo::oapi::ToResponses>::to_responses(components));
469                    }
470                }
471            }
472            .to_string()
473        );
474    }
475
476    #[test]
477    fn test_to_parameters() {
478        let input = quote! {
479            #[derive(Deserialize, ToParameters)]
480            struct PetQuery {
481                /// Name of pet
482                name: Option<String>,
483                /// Age of pet
484                age: Option<i32>,
485                /// Kind of pet
486                #[salvo(parameter(inline))]
487                kind: PetKind
488            }
489        };
490        assert_eq!(
491            parameter::to_parameters(parse2(input).unwrap()).unwrap().to_string(),
492            quote! {
493                impl<'__macro_gen_ex> salvo::oapi::ToParameters<'__macro_gen_ex> for PetQuery {
494                    fn to_parameters(components: &mut salvo::oapi::Components) -> salvo::oapi::Parameters {
495                        salvo::oapi::Parameters(
496                            [
497                                salvo::oapi::parameter::Parameter::new("name")
498                                    .description("Name of pet")
499                                    .required(salvo::oapi::Required::False)
500                                    .schema(
501                                        salvo::oapi::Object::new()
502                                            .schema_type(salvo::oapi::schema::SchemaType::basic(salvo::oapi::schema::BasicType::String))
503                                    ),
504                                salvo::oapi::parameter::Parameter::new("age")
505                                    .description("Age of pet")
506                                    .required(salvo::oapi::Required::False)
507                                    .schema(
508                                        salvo::oapi::Object::new()
509                                            .schema_type(salvo::oapi::schema::SchemaType::basic(salvo::oapi::schema::BasicType::Integer))
510                                            .format(salvo::oapi::SchemaFormat::KnownFormat(
511                                                salvo::oapi::KnownFormat::Int32
512                                            ))
513                                    ),
514                                salvo::oapi::parameter::Parameter::new("kind")
515                                    .description("Kind of pet")
516                                    .required(salvo::oapi::Required::True)
517                                    .schema(salvo::oapi::RefOr::from(<PetKind as salvo::oapi::ToSchema>::to_schema(components))),
518                            ]
519                            .to_vec()
520                        )
521                    }
522                }
523                impl salvo::oapi::EndpointArgRegister for PetQuery {
524                    fn register(
525                        components: &mut salvo::oapi::Components,
526                        operation: &mut salvo::oapi::Operation,
527                        _arg: &str
528                    ) {
529                        for parameter in <Self as salvo::oapi::ToParameters>::to_parameters(components) {
530                            operation.parameters.insert(parameter);
531                        }
532                    }
533                }
534                impl<'__macro_gen_ex> salvo::Extractible<'__macro_gen_ex> for PetQuery {
535                    fn metadata() -> &'static salvo::extract::Metadata {
536                        static METADATA: ::std::sync::OnceLock<salvo::extract::Metadata> = ::std::sync::OnceLock::new();
537                        METADATA.get_or_init(||
538                            salvo::extract::Metadata::new("PetQuery")
539                                .default_sources(vec![salvo::extract::metadata::Source::new(
540                                    salvo::extract::metadata::SourceFrom::Query,
541                                    salvo::extract::metadata::SourceParser::MultiMap
542                                )])
543                                .fields(vec![
544                                    salvo::extract::metadata::Field::new("name"),
545                                    salvo::extract::metadata::Field::new("age"),
546                                    salvo::extract::metadata::Field::new("kind")
547                                ])
548                        )
549                    }
550                    async fn extract(
551                        req: &'__macro_gen_ex mut salvo::Request,
552                        depot: &'__macro_gen_ex mut salvo::Depot
553                    ) -> ::std::result::Result<Self, impl salvo::Writer + Send + std::fmt::Debug + 'static> {
554                        salvo::serde::from_request(req, depot, Self::metadata()).await
555                    }
556                    async fn extract_with_arg(
557                        req: &'__macro_gen_ex mut salvo::Request,
558                        depot: &'__macro_gen_ex mut salvo::Depot,
559                        _arg: &str
560                    ) -> ::std::result::Result<Self, impl salvo::Writer + Send + std::fmt::Debug + 'static> {
561                        Self::extract(req, depot).await
562                    }
563                }
564            }
565            .to_string()
566        );
567    }
568}