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#![cfg_attr(test, allow(clippy::unwrap_used))]
11
12use proc_macro::TokenStream;
13use quote::ToTokens;
14use syn::parse::{Parse, ParseStream};
15use syn::token::Bracket;
16use syn::{Ident, Item, Token, bracketed, parse_macro_input};
17
18#[macro_use]
19mod cfg;
20mod attribute;
21pub(crate) mod bound;
22mod component;
23mod doc_comment;
24mod endpoint;
25pub(crate) mod feature;
26mod operation;
27mod parameter;
28pub(crate) mod parse_utils;
29mod response;
30mod schema;
31mod schema_type;
32mod security_requirement;
33mod server;
34mod shared;
35mod type_tree;
36
37pub(crate) use proc_macro2_diagnostics::{Diagnostic, Level as DiagLevel};
38pub(crate) use salvo_serde_util::{self as serde_util, RenameRule, SerdeContainer, SerdeValue};
39
40pub(crate) use self::component::{ComponentSchema, ComponentSchemaProps};
41pub(crate) use self::endpoint::EndpointAttr;
42pub(crate) use self::feature::Feature;
43pub(crate) use self::operation::Operation;
44pub(crate) use self::parameter::Parameter;
45pub(crate) use self::response::Response;
46pub(crate) use self::server::Server;
47pub(crate) use self::shared::*;
48pub(crate) use self::type_tree::TypeTree;
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        compose_context: None,
148    })
149    .map(|s| s.to_token_stream());
150    match stream {
151        Ok(stream) => stream.into(),
152        Err(diag) => diag.emit_as_item_tokens().into(),
153    }
154}
155
156pub(crate) trait IntoInner<T> {
157    fn into_inner(self) -> T;
158}
159
160#[cfg(test)]
161mod tests {
162    use quote::quote;
163    use syn::parse2;
164
165    use super::*;
166
167    #[test]
168    fn test_endpoint_for_fn() {
169        let input = quote! {
170            #[endpoint]
171            async fn hello() {
172                res.render_plain_text("Hello World");
173            }
174        };
175        let item = parse2(input).unwrap();
176        assert_eq!(
177            endpoint::generate(parse2(quote! {}).unwrap(), item)
178                .unwrap()
179                .to_string(),
180            quote! {
181                #[allow(non_camel_case_types)]
182                #[derive(Debug)]
183                struct hello;
184                impl hello {
185                    async fn hello() {
186                        {res.render_plain_text("Hello World");}
187                    }
188                }
189                #[salvo::async_trait]
190                impl salvo::Handler for hello {
191                    async fn handle(
192                        &self,
193                        __macro_gen_req: &mut salvo::Request,
194                        __macro_gen_depot: &mut salvo::Depot,
195                        __macro_gen_res: &mut salvo::Response,
196                        __macro_gen_ctrl: &mut salvo::FlowCtrl
197                    ) {
198                        Self::hello().await
199                    }
200                }
201                fn __macro_gen_oapi_endpoint_type_id_hello() -> ::std::any::TypeId {
202                    ::std::any::TypeId::of::<hello>()
203                }
204                fn __macro_gen_oapi_endpoint_creator_hello() -> salvo::oapi::Endpoint {
205                    let mut components = salvo::oapi::Components::new();
206                    let status_codes: &[salvo::http::StatusCode] = &[];
207                    let mut operation = salvo::oapi::Operation::new();
208                    if operation.operation_id.is_none() {
209                        operation.operation_id = Some(salvo::oapi::naming::assign_name::<hello>(salvo::oapi::naming::NameRule::Auto));
210                    }
211                    if !status_codes.is_empty() {
212                        let responses = std::ops::DerefMut::deref_mut(&mut operation.responses);
213                        responses.retain(|k, _| {
214                            if let Ok(code) = <salvo::http::StatusCode as std::str::FromStr>::from_str(k) {
215                                status_codes.contains(&code)
216                            } else {
217                                true
218                            }
219                        });
220                    }
221                    salvo::oapi::Endpoint {
222                        operation,
223                        components,
224                    }
225                }
226                salvo::oapi::__private::inventory::submit! {
227                    salvo::oapi::EndpointRegistry::save(__macro_gen_oapi_endpoint_type_id_hello, __macro_gen_oapi_endpoint_creator_hello)
228                }
229            }
230            .to_string()
231        );
232    }
233
234    #[test]
235    fn test_to_schema_struct() {
236        let input = quote! {
237            /// This is user.
238            ///
239            /// This is user description.
240            #[derive(ToSchema)]
241            struct User {
242                #[salvo(schema(examples("chris"), min_length = 1, max_length = 100, required))]
243                name: String,
244                #[salvo(schema(example = 16, default = 0, maximum=100, minimum=0,format = "int32"))]
245                age: i32,
246                #[deprecated = "There is deprecated"]
247                high: u32,
248            }
249        };
250        let result = schema::to_schema(parse2(input).unwrap())
251            .unwrap()
252            .to_string();
253        // Should contain both ComposeSchema and ToSchema impls
254        assert!(
255            result.contains("impl salvo :: oapi :: ComposeSchema for User"),
256            "Expected ComposeSchema impl in output"
257        );
258        assert!(
259            result.contains("impl salvo :: oapi :: ToSchema for User"),
260            "Expected ToSchema impl in output"
261        );
262        // Verify schema body content
263        assert!(result.contains("\"name\""), "Expected 'name' property");
264        assert!(result.contains("\"age\""), "Expected 'age' property");
265        assert!(result.contains("\"high\""), "Expected 'high' property");
266        assert!(
267            result.contains("This is user.\\n\\nThis is user description."),
268            "Expected description"
269        );
270    }
271
272    #[test]
273    fn test_to_schema_generics() {
274        let input = quote! {
275            #[derive(Serialize, Deserialize, ToSchema, Debug)]
276            #[salvo(schema(aliases(MyI32 = MyObject<i32>, MyStr = MyObject<String>)))]
277            struct MyObject<T: ToSchema + std::fmt::Debug + 'static> {
278                value: T,
279            }
280        };
281        let result = schema::to_schema(parse2(input).unwrap())
282            .unwrap()
283            .to_string()
284            .replace("< ", "<")
285            .replace("> ", ">");
286        // Should contain both ComposeSchema and ToSchema impls
287        assert!(
288            result.contains("salvo :: oapi :: ComposeSchema for MyObject"),
289            "Expected ComposeSchema impl in output"
290        );
291        assert!(
292            result.contains("salvo :: oapi :: ToSchema for MyObject"),
293            "Expected ToSchema impl in output"
294        );
295        // ComposeSchema should use __compose_generics for generic param T
296        assert!(
297            result.contains("__compose_generics"),
298            "Expected __compose_generics usage in ComposeSchema impl"
299        );
300        // ToSchema should still use ToSchema::to_schema for type aliases
301        assert!(result.contains("MyI32"), "Expected MyI32 alias");
302        assert!(result.contains("MyStr"), "Expected MyStr alias");
303    }
304
305    #[test]
306    fn test_to_schema_enum() {
307        let input = quote! {
308            #[derive(Serialize, Deserialize, ToSchema, Debug)]
309            #[salvo(schema(rename_all = "camelCase"))]
310            enum People {
311                Man,
312                Woman,
313            }
314        };
315        let result = schema::to_schema(parse2(input).unwrap())
316            .unwrap()
317            .to_string();
318        // Should contain both ComposeSchema and ToSchema impls
319        assert!(
320            result.contains("impl salvo :: oapi :: ComposeSchema for People"),
321            "Expected ComposeSchema impl in output"
322        );
323        assert!(
324            result.contains("impl salvo :: oapi :: ToSchema for People"),
325            "Expected ToSchema impl in output"
326        );
327        // Verify enum values
328        assert!(result.contains("\"man\""), "Expected 'man' variant");
329        assert!(result.contains("\"woman\""), "Expected 'woman' variant");
330    }
331
332    #[test]
333    fn test_to_response() {
334        let input = quote! {
335            #[derive(ToResponse)]
336            #[salvo(response(description = "Person response returns single Person entity"))]
337            struct User{
338                name: String,
339                age: i32,
340            }
341        };
342        assert_eq!(
343            response::to_response(parse2(input).unwrap()).unwrap()
344                .to_string(),
345            quote! {
346                impl salvo::oapi::ToResponse for User {
347                    fn to_response(
348                        components: &mut salvo::oapi::Components
349                    ) -> salvo::oapi::RefOr<salvo::oapi::Response> {
350                        let response = salvo::oapi::Response::new("Person response returns single Person entity").add_content(
351                            "application/json",
352                            salvo::oapi::Content::new(
353                                salvo::oapi::Object::new()
354                                    .property(
355                                        "name",
356                                        salvo::oapi::Object::new().schema_type(salvo::oapi::schema::SchemaType::basic(salvo::oapi::schema::BasicType::String))
357                                    )
358                                    .required("name")
359                                    .property(
360                                        "age",
361                                        salvo::oapi::Object::new()
362                                            .schema_type(salvo::oapi::schema::SchemaType::basic(salvo::oapi::schema::BasicType::Integer))
363                                            .format(salvo::oapi::SchemaFormat::KnownFormat(
364                                                salvo::oapi::KnownFormat::Int32
365                                            ))
366                                    )
367                                    .required("age")
368                            )
369                        );
370                        components.responses.insert("User", response);
371                        salvo::oapi::RefOr::Ref(salvo::oapi::Ref::new(format!("#/components/responses/{}", "User")))
372                    }
373                }
374                impl salvo::oapi::EndpointOutRegister for User {
375                    fn register(components: &mut salvo::oapi::Components, operation: &mut salvo::oapi::Operation) {
376                        operation
377                            .responses
378                            .insert("200", <Self as salvo::oapi::ToResponse>::to_response(components))
379                    }
380                }
381            } .to_string()
382        );
383    }
384
385    #[test]
386    fn test_to_responses() {
387        let input = quote! {
388            #[derive(salvo_oapi::ToResponses)]
389            enum UserResponses {
390                /// Success response description.
391                #[salvo(response(status_code = 200))]
392                Success { value: String },
393
394                #[salvo(response(status_code = 404))]
395                NotFound,
396
397                #[salvo(response(status_code = 400))]
398                BadRequest(BadRequest),
399
400                #[salvo(response(status_code = 500))]
401                ServerError(Response),
402
403                #[salvo(response(status_code = 418))]
404                TeaPot(Response),
405            }
406        };
407        assert_eq!(
408            response::to_responses(parse2(input).unwrap()).unwrap().to_string(),
409            quote! {
410                impl salvo::oapi::ToResponses for UserResponses {
411                    fn to_responses(components: &mut salvo::oapi::Components) -> salvo::oapi::response::Responses {
412                        [
413                            (
414                                "200",
415                                salvo::oapi::RefOr::from(
416                                    salvo::oapi::Response::new("Success response description.").add_content(
417                                        "application/json",
418                                        salvo::oapi::Content::new(
419                                            salvo::oapi::Object::new()
420                                                .property(
421                                                    "value",
422                                                    salvo::oapi::Object::new().schema_type(salvo::oapi::schema::SchemaType::basic(salvo::oapi::schema::BasicType::String))
423                                                )
424                                                .required("value")
425                                                .description("Success response description.")
426                                        )
427                                    )
428                                )
429                            ),
430                            (
431                                "404",
432                                salvo::oapi::RefOr::from(salvo::oapi::Response::new(""))
433                            ),
434                            (
435                                "400",
436                                salvo::oapi::RefOr::from(salvo::oapi::Response::new("").add_content(
437                                    "application/json",
438                                    salvo::oapi::Content::new(salvo::oapi::RefOr::from(
439                                        <BadRequest as salvo::oapi::ToSchema>::to_schema(components)
440                                    ))
441                                ))
442                            ),
443                            (
444                                "500",
445                                salvo::oapi::RefOr::from(salvo::oapi::Response::new("").add_content(
446                                    "application/json",
447                                    salvo::oapi::Content::new(salvo::oapi::RefOr::from(
448                                        <Response as salvo::oapi::ToSchema>::to_schema(components)
449                                    ))
450                                ))
451                            ),
452                            (
453                                "418",
454                                salvo::oapi::RefOr::from(salvo::oapi::Response::new("").add_content(
455                                    "application/json",
456                                    salvo::oapi::Content::new(salvo::oapi::RefOr::from(
457                                        <Response as salvo::oapi::ToSchema>::to_schema(components)
458                                    ))
459                                ))
460                            ),
461                        ]
462                        .into()
463                    }
464                }
465                impl salvo::oapi::EndpointOutRegister for UserResponses {
466                    fn register(components: &mut salvo::oapi::Components, operation: &mut salvo::oapi::Operation) {
467                        operation
468                            .responses
469                            .append(&mut <Self as salvo::oapi::ToResponses>::to_responses(components));
470                    }
471                }
472            }
473            .to_string()
474        );
475    }
476
477    #[test]
478    fn test_to_parameters() {
479        let input = quote! {
480            #[derive(Deserialize, ToParameters)]
481            struct PetQuery {
482                /// Name of pet
483                name: Option<String>,
484                /// Age of pet
485                age: Option<i32>,
486                /// Kind of pet
487                #[salvo(parameter(inline))]
488                kind: PetKind
489            }
490        };
491        assert_eq!(
492            parameter::to_parameters(parse2(input).unwrap()).unwrap().to_string(),
493            quote! {
494                impl<'__macro_gen_ex> salvo::oapi::ToParameters<'__macro_gen_ex> for PetQuery {
495                    fn to_parameters(components: &mut salvo::oapi::Components) -> salvo::oapi::Parameters {
496                        salvo::oapi::Parameters(
497                            [
498                                salvo::oapi::parameter::Parameter::new("name")
499                                    .description("Name of pet")
500                                    .required(salvo::oapi::Required::False)
501                                    .schema(
502                                        salvo::oapi::Object::new()
503                                            .schema_type(salvo::oapi::schema::SchemaType::basic(salvo::oapi::schema::BasicType::String))
504                                    ),
505                                salvo::oapi::parameter::Parameter::new("age")
506                                    .description("Age of pet")
507                                    .required(salvo::oapi::Required::False)
508                                    .schema(
509                                        salvo::oapi::Object::new()
510                                            .schema_type(salvo::oapi::schema::SchemaType::basic(salvo::oapi::schema::BasicType::Integer))
511                                            .format(salvo::oapi::SchemaFormat::KnownFormat(
512                                                salvo::oapi::KnownFormat::Int32
513                                            ))
514                                    ),
515                                salvo::oapi::parameter::Parameter::new("kind")
516                                    .description("Kind of pet")
517                                    .required(salvo::oapi::Required::True)
518                                    .schema(salvo::oapi::RefOr::from(<PetKind as salvo::oapi::ToSchema>::to_schema(components))),
519                            ]
520                            .to_vec()
521                        )
522                    }
523                }
524                impl salvo::oapi::EndpointArgRegister for PetQuery {
525                    fn register(
526                        components: &mut salvo::oapi::Components,
527                        operation: &mut salvo::oapi::Operation,
528                        _arg: &str
529                    ) {
530                        for parameter in <Self as salvo::oapi::ToParameters>::to_parameters(components) {
531                            operation.parameters.insert(parameter);
532                        }
533                    }
534                }
535                impl<'__macro_gen_ex> salvo::Extractible<'__macro_gen_ex> for PetQuery {
536                    fn metadata() -> &'static salvo::extract::Metadata {
537                        static METADATA: ::std::sync::OnceLock<salvo::extract::Metadata> = ::std::sync::OnceLock::new();
538                        METADATA.get_or_init(||
539                            salvo::extract::Metadata::new("PetQuery")
540                                .default_sources(vec![salvo::extract::metadata::Source::new(
541                                    salvo::extract::metadata::SourceFrom::Query,
542                                    salvo::extract::metadata::SourceParser::MultiMap
543                                )])
544                                .fields(vec![
545                                    salvo::extract::metadata::Field::new("name"),
546                                    salvo::extract::metadata::Field::new("age"),
547                                    salvo::extract::metadata::Field::new("kind")
548                                ])
549                        )
550                    }
551                    async fn extract(
552                        req: &'__macro_gen_ex mut salvo::Request,
553                        depot: &'__macro_gen_ex mut salvo::Depot
554                    ) -> ::std::result::Result<Self, impl salvo::Writer + Send + std::fmt::Debug + 'static> {
555                        salvo::serde::from_request(req, depot, Self::metadata()).await
556                    }
557                    async fn extract_with_arg(
558                        req: &'__macro_gen_ex mut salvo::Request,
559                        depot: &'__macro_gen_ex mut salvo::Depot,
560                        _arg: &str
561                    ) -> ::std::result::Result<Self, impl salvo::Writer + Send + std::fmt::Debug + 'static> {
562                        Self::extract(req, depot).await
563                    }
564                }
565            }
566            .to_string()
567        );
568    }
569}