1#![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#[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#[proc_macro_derive(ToSchema, attributes(salvo))] pub 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#[proc_macro_derive(ToParameters, attributes(salvo))] pub 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#[proc_macro_derive(ToResponse, attributes(salvo))] pub 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#[proc_macro_derive(ToResponses, attributes(salvo))] pub 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 #[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 #[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: Option<String>,
533 age: Option<i32>,
535 #[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}