1#![doc(html_favicon_url = "https://salvo.rs/favicon-32x32.png")]
11#![doc(html_logo_url = "https://salvo.rs/images/logo.svg")]
12#![cfg_attr(docsrs, feature(doc_cfg))]
13#![cfg_attr(test, allow(clippy::unwrap_used))]
14
15use proc_macro::TokenStream;
16use quote::ToTokens;
17use syn::parse::{Parse, ParseStream};
18use syn::token::Bracket;
19use syn::{Ident, Item, Token, bracketed, parse_macro_input};
20
21mod attribute;
22pub(crate) mod bound;
23mod component;
24mod doc_comment;
25mod endpoint;
26pub(crate) mod feature;
27mod operation;
28mod parameter;
29pub(crate) mod parse_utils;
30mod response;
31mod schema;
32mod schema_type;
33mod security_requirement;
34mod server;
35mod shared;
36mod type_tree;
37
38pub(crate) use proc_macro2_diagnostics::{Diagnostic, Level as DiagLevel};
39pub(crate) use salvo_serde_util::{self as serde_util, RenameRule, SerdeContainer, SerdeValue};
40
41pub(crate) use self::component::{ComponentSchema, ComponentSchemaProps};
42pub(crate) use self::endpoint::EndpointAttr;
43pub(crate) use self::feature::Feature;
44pub(crate) use self::operation::Operation;
45pub(crate) use self::parameter::Parameter;
46pub(crate) use self::response::Response;
47pub(crate) use self::server::Server;
48pub(crate) use self::shared::*;
49pub(crate) use self::type_tree::TypeTree;
50
51#[proc_macro_attribute]
90pub fn endpoint(attr: TokenStream, input: TokenStream) -> TokenStream {
91 let attr = syn::parse_macro_input!(attr as EndpointAttr);
92 let item = parse_macro_input!(input as Item);
93 match endpoint::generate(attr, item) {
94 Ok(stream) => stream.into(),
95 Err(e) => e.to_compile_error().into(),
96 }
97}
98
99#[proc_macro_derive(ToSchema, attributes(salvo))] pub fn derive_to_schema(input: TokenStream) -> TokenStream {
126 match schema::to_schema(syn::parse_macro_input!(input)) {
127 Ok(stream) => stream.into(),
128 Err(e) => e.emit_as_item_tokens().into(),
129 }
130}
131
132#[proc_macro_derive(ToParameters, attributes(salvo))] pub fn derive_to_parameters(input: TokenStream) -> TokenStream {
165 match parameter::to_parameters(syn::parse_macro_input!(input)) {
166 Ok(stream) => stream.into(),
167 Err(e) => e.emit_as_item_tokens().into(),
168 }
169}
170
171#[proc_macro_derive(ToResponse, attributes(salvo))] pub fn derive_to_response(input: TokenStream) -> TokenStream {
193 match response::to_response(syn::parse_macro_input!(input)) {
194 Ok(stream) => stream.into(),
195 Err(e) => e.emit_as_item_tokens().into(),
196 }
197}
198
199#[proc_macro_derive(ToResponses, attributes(salvo))] pub fn to_responses(input: TokenStream) -> TokenStream {
218 match response::to_responses(syn::parse_macro_input!(input)) {
219 Ok(stream) => stream.into(),
220 Err(e) => e.emit_as_item_tokens().into(),
221 }
222}
223
224#[doc(hidden)]
225#[proc_macro]
226pub fn schema(input: TokenStream) -> TokenStream {
227 struct Schema {
228 inline: bool,
229 ty: syn::Type,
230 }
231 impl Parse for Schema {
232 fn parse(input: ParseStream) -> syn::Result<Self> {
233 let inline = if input.peek(Token![#]) && input.peek2(Bracket) {
234 input.parse::<Token![#]>()?;
235
236 let inline;
237 bracketed!(inline in input);
238 let i = inline.parse::<Ident>()?;
239 i == "inline"
240 } else {
241 false
242 };
243
244 let ty = input.parse()?;
245 Ok(Self { inline, ty })
246 }
247 }
248
249 let schema = syn::parse_macro_input!(input as Schema);
250 let type_tree = match TypeTree::from_type(&schema.ty) {
251 Ok(type_tree) => type_tree,
252 Err(diag) => return diag.emit_as_item_tokens().into(),
253 };
254
255 let stream = ComponentSchema::new(ComponentSchemaProps {
256 features: Some(vec![Feature::Inline(schema.inline.into())]),
257 type_tree: &type_tree,
258 deprecated: None,
259 description: None,
260 object_name: "",
261 compose_context: None,
262 })
263 .map(|s| s.to_token_stream());
264 match stream {
265 Ok(stream) => stream.into(),
266 Err(diag) => diag.emit_as_item_tokens().into(),
267 }
268}
269
270pub(crate) trait IntoInner<T> {
271 fn into_inner(self) -> T;
272}
273
274#[cfg(test)]
275mod tests {
276 use quote::quote;
277 use syn::parse2;
278
279 use super::*;
280
281 #[test]
282 fn test_endpoint_for_fn() {
283 let input = quote! {
284 #[endpoint]
285 async fn hello() {
286 res.render_plain_text("Hello World");
287 }
288 };
289 let item = parse2(input).unwrap();
290 assert_eq!(
291 endpoint::generate(parse2(quote! {}).unwrap(), item)
292 .unwrap()
293 .to_string(),
294 quote! {
295 #[allow(non_camel_case_types)]
296 #[derive(Debug)]
297 struct hello;
298 impl hello {
299 async fn hello() {
300 {res.render_plain_text("Hello World");}
301 }
302 }
303 #[salvo::async_trait]
304 impl salvo::Handler for hello {
305 async fn handle(
306 &self,
307 __macro_gen_req: &mut salvo::Request,
308 __macro_gen_depot: &mut salvo::Depot,
309 __macro_gen_res: &mut salvo::Response,
310 __macro_gen_ctrl: &mut salvo::FlowCtrl
311 ) {
312 Self::hello().await
313 }
314 }
315 fn __macro_gen_oapi_endpoint_type_id_hello() -> ::std::any::TypeId {
316 ::std::any::TypeId::of::<hello>()
317 }
318 fn __macro_gen_oapi_endpoint_creator_hello() -> salvo::oapi::Endpoint {
319 let mut components = salvo::oapi::Components::new();
320 let status_codes: &[salvo::http::StatusCode] = &[];
321 let mut operation = salvo::oapi::Operation::new();
322 if operation.operation_id.is_none() {
323 operation.operation_id = Some(salvo::oapi::naming::assign_name::<hello>(salvo::oapi::naming::NameRule::Auto));
324 }
325 if !status_codes.is_empty() {
326 let responses = std::ops::DerefMut::deref_mut(&mut operation.responses);
327 responses.retain(|k, _| {
328 if let Ok(code) = <salvo::http::StatusCode as std::str::FromStr>::from_str(k) {
329 status_codes.contains(&code)
330 } else {
331 true
332 }
333 });
334 }
335 salvo::oapi::Endpoint {
336 operation,
337 components,
338 }
339 }
340 salvo::oapi::__private::inventory::submit! {
341 salvo::oapi::EndpointRegistry::save(__macro_gen_oapi_endpoint_type_id_hello, __macro_gen_oapi_endpoint_creator_hello)
342 }
343 }
344 .to_string()
345 );
346 }
347
348 #[test]
349 fn test_to_schema_struct() {
350 let input = quote! {
351 #[derive(ToSchema)]
355 struct User {
356 #[salvo(schema(examples("chris"), min_length = 1, max_length = 100, required))]
357 name: String,
358 #[salvo(schema(example = 16, default = 0, maximum=100, minimum=0,format = "int32"))]
359 age: i32,
360 #[deprecated = "There is deprecated"]
361 high: u32,
362 }
363 };
364 let result = schema::to_schema(parse2(input).unwrap())
365 .unwrap()
366 .to_string();
367 assert!(
369 result.contains("impl salvo :: oapi :: ComposeSchema for User"),
370 "Expected ComposeSchema impl in output"
371 );
372 assert!(
373 result.contains("impl salvo :: oapi :: ToSchema for User"),
374 "Expected ToSchema impl in output"
375 );
376 assert!(result.contains("\"name\""), "Expected 'name' property");
378 assert!(result.contains("\"age\""), "Expected 'age' property");
379 assert!(result.contains("\"high\""), "Expected 'high' property");
380 assert!(
381 result.contains("This is user.\\n\\nThis is user description."),
382 "Expected description"
383 );
384 }
385
386 #[test]
387 fn test_to_schema_generics() {
388 let input = quote! {
389 #[derive(Serialize, Deserialize, ToSchema, Debug)]
390 #[salvo(schema(aliases(MyI32 = MyObject<i32>, MyStr = MyObject<String>)))]
391 struct MyObject<T: ToSchema + std::fmt::Debug + 'static> {
392 value: T,
393 }
394 };
395 let result = schema::to_schema(parse2(input).unwrap())
396 .unwrap()
397 .to_string()
398 .replace("< ", "<")
399 .replace("> ", ">");
400 assert!(
402 result.contains("salvo :: oapi :: ComposeSchema for MyObject"),
403 "Expected ComposeSchema impl in output"
404 );
405 assert!(
406 result.contains("salvo :: oapi :: ToSchema for MyObject"),
407 "Expected ToSchema impl in output"
408 );
409 assert!(
411 result.contains("__compose_generics"),
412 "Expected __compose_generics usage in ComposeSchema impl"
413 );
414 assert!(result.contains("MyI32"), "Expected MyI32 alias");
416 assert!(result.contains("MyStr"), "Expected MyStr alias");
417 }
418
419 #[test]
420 fn test_to_schema_enum() {
421 let input = quote! {
422 #[derive(Serialize, Deserialize, ToSchema, Debug)]
423 #[salvo(schema(rename_all = "camelCase"))]
424 enum People {
425 Man,
426 Woman,
427 }
428 };
429 let result = schema::to_schema(parse2(input).unwrap())
430 .unwrap()
431 .to_string();
432 assert!(
434 result.contains("impl salvo :: oapi :: ComposeSchema for People"),
435 "Expected ComposeSchema impl in output"
436 );
437 assert!(
438 result.contains("impl salvo :: oapi :: ToSchema for People"),
439 "Expected ToSchema impl in output"
440 );
441 assert!(result.contains("\"man\""), "Expected 'man' variant");
443 assert!(result.contains("\"woman\""), "Expected 'woman' variant");
444 }
445
446 #[test]
447 fn test_to_response() {
448 let input = quote! {
449 #[derive(ToResponse)]
450 #[salvo(response(description = "Person response returns single Person entity"))]
451 struct User{
452 name: String,
453 age: i32,
454 }
455 };
456 assert_eq!(
457 response::to_response(parse2(input).unwrap()).unwrap()
458 .to_string(),
459 quote! {
460 impl salvo::oapi::ToResponse for User {
461 fn to_response(
462 components: &mut salvo::oapi::Components
463 ) -> salvo::oapi::RefOr<salvo::oapi::Response> {
464 let response = salvo::oapi::Response::new("Person response returns single Person entity").add_content(
465 "application/json",
466 salvo::oapi::Content::new(
467 salvo::oapi::Object::new()
468 .property(
469 "name",
470 salvo::oapi::Object::new().schema_type(salvo::oapi::schema::SchemaType::basic(salvo::oapi::schema::BasicType::String))
471 )
472 .required("name")
473 .property(
474 "age",
475 salvo::oapi::Object::new()
476 .schema_type(salvo::oapi::schema::SchemaType::basic(salvo::oapi::schema::BasicType::Integer))
477 .format(salvo::oapi::SchemaFormat::KnownFormat(
478 salvo::oapi::KnownFormat::Int32
479 ))
480 )
481 .required("age")
482 )
483 );
484 components.responses.insert("User", response);
485 salvo::oapi::RefOr::Ref(salvo::oapi::Ref::new(format!("#/components/responses/{}", "User")))
486 }
487 }
488 impl salvo::oapi::EndpointOutRegister for User {
489 fn register(components: &mut salvo::oapi::Components, operation: &mut salvo::oapi::Operation) {
490 operation
491 .responses
492 .insert("200", <Self as salvo::oapi::ToResponse>::to_response(components))
493 }
494 }
495 } .to_string()
496 );
497 }
498
499 #[test]
500 fn test_to_responses() {
501 let input = quote! {
502 #[derive(salvo_oapi::ToResponses)]
503 enum UserResponses {
504 #[salvo(response(status_code = 200))]
506 Success { value: String },
507
508 #[salvo(response(status_code = 404))]
509 NotFound,
510
511 #[salvo(response(status_code = 400))]
512 BadRequest(BadRequest),
513
514 #[salvo(response(status_code = 500))]
515 ServerError(Response),
516
517 #[salvo(response(status_code = 418))]
518 TeaPot(Response),
519 }
520 };
521 assert_eq!(
522 response::to_responses(parse2(input).unwrap()).unwrap().to_string(),
523 quote! {
524 impl salvo::oapi::ToResponses for UserResponses {
525 fn to_responses(components: &mut salvo::oapi::Components) -> salvo::oapi::response::Responses {
526 [
527 (
528 "200",
529 salvo::oapi::RefOr::from(
530 salvo::oapi::Response::new("Success response description.").add_content(
531 "application/json",
532 salvo::oapi::Content::new(
533 salvo::oapi::Object::new()
534 .property(
535 "value",
536 salvo::oapi::Object::new().schema_type(salvo::oapi::schema::SchemaType::basic(salvo::oapi::schema::BasicType::String))
537 )
538 .required("value")
539 .description("Success response description.")
540 )
541 )
542 )
543 ),
544 (
545 "404",
546 salvo::oapi::RefOr::from(salvo::oapi::Response::new(""))
547 ),
548 (
549 "400",
550 salvo::oapi::RefOr::from(salvo::oapi::Response::new("").add_content(
551 "application/json",
552 salvo::oapi::Content::new(salvo::oapi::RefOr::from(
553 <BadRequest as salvo::oapi::ToSchema>::to_schema(components)
554 ))
555 ))
556 ),
557 (
558 "500",
559 salvo::oapi::RefOr::from(salvo::oapi::Response::new("").add_content(
560 "application/json",
561 salvo::oapi::Content::new(salvo::oapi::RefOr::from(
562 <Response as salvo::oapi::ToSchema>::to_schema(components)
563 ))
564 ))
565 ),
566 (
567 "418",
568 salvo::oapi::RefOr::from(salvo::oapi::Response::new("").add_content(
569 "application/json",
570 salvo::oapi::Content::new(salvo::oapi::RefOr::from(
571 <Response as salvo::oapi::ToSchema>::to_schema(components)
572 ))
573 ))
574 ),
575 ]
576 .into()
577 }
578 }
579 impl salvo::oapi::EndpointOutRegister for UserResponses {
580 fn register(components: &mut salvo::oapi::Components, operation: &mut salvo::oapi::Operation) {
581 operation
582 .responses
583 .append(&mut <Self as salvo::oapi::ToResponses>::to_responses(components));
584 }
585 }
586 }
587 .to_string()
588 );
589 }
590
591 #[test]
592 fn test_to_parameters() {
593 let input = quote! {
594 #[derive(Deserialize, ToParameters)]
595 struct PetQuery {
596 name: Option<String>,
598 age: Option<i32>,
600 #[salvo(parameter(inline))]
602 kind: PetKind
603 }
604 };
605 assert_eq!(
606 parameter::to_parameters(parse2(input).unwrap()).unwrap().to_string(),
607 quote! {
608 impl<'__macro_gen_ex> salvo::oapi::ToParameters<'__macro_gen_ex> for PetQuery {
609 fn to_parameters(components: &mut salvo::oapi::Components) -> salvo::oapi::Parameters {
610 salvo::oapi::Parameters(
611 [
612 salvo::oapi::parameter::Parameter::new("name")
613 .location(salvo::oapi::parameter::ParameterIn::Query)
614 .description("Name of pet")
615 .required(salvo::oapi::Required::False)
616 .schema(
617 salvo::oapi::Object::new()
618 .schema_type(salvo::oapi::schema::SchemaType::basic(salvo::oapi::schema::BasicType::String))
619 ),
620 salvo::oapi::parameter::Parameter::new("age")
621 .location(salvo::oapi::parameter::ParameterIn::Query)
622 .description("Age of pet")
623 .required(salvo::oapi::Required::False)
624 .schema(
625 salvo::oapi::Object::new()
626 .schema_type(salvo::oapi::schema::SchemaType::basic(salvo::oapi::schema::BasicType::Integer))
627 .format(salvo::oapi::SchemaFormat::KnownFormat(
628 salvo::oapi::KnownFormat::Int32
629 ))
630 ),
631 salvo::oapi::parameter::Parameter::new("kind")
632 .location(salvo::oapi::parameter::ParameterIn::Query)
633 .description("Kind of pet")
634 .required(salvo::oapi::Required::True)
635 .schema(<PetKind as salvo::oapi::ComposeSchema>::compose(components, ::std::vec::Vec::new())),
636 ]
637 .to_vec()
638 )
639 }
640 }
641 impl salvo::oapi::EndpointArgRegister for PetQuery {
642 fn register(
643 components: &mut salvo::oapi::Components,
644 operation: &mut salvo::oapi::Operation,
645 _arg: &str
646 ) {
647 for parameter in <Self as salvo::oapi::ToParameters>::to_parameters(components) {
648 operation.parameters.insert(parameter);
649 }
650 }
651 }
652 impl<'__macro_gen_ex> salvo::Extractible<'__macro_gen_ex> for PetQuery {
653 fn metadata() -> &'static salvo::extract::Metadata {
654 static METADATA: ::std::sync::OnceLock<salvo::extract::Metadata> = ::std::sync::OnceLock::new();
655 METADATA.get_or_init(||
656 salvo::extract::Metadata::new("PetQuery")
657 .default_sources(vec![salvo::extract::metadata::Source::new(
658 salvo::extract::metadata::SourceFrom::Query,
659 salvo::extract::metadata::SourceParser::MultiMap
660 )])
661 .fields(vec![
662 salvo::extract::metadata::Field::new("name"),
663 salvo::extract::metadata::Field::new("age"),
664 salvo::extract::metadata::Field::new("kind")
665 ])
666 )
667 }
668 async fn extract(
669 req: &'__macro_gen_ex mut salvo::Request,
670 depot: &'__macro_gen_ex mut salvo::Depot
671 ) -> ::std::result::Result<Self, impl salvo::Writer + Send + std::fmt::Debug + 'static> {
672 salvo::serde::from_request(req, depot, Self::metadata()).await
673 }
674 async fn extract_with_arg(
675 req: &'__macro_gen_ex mut salvo::Request,
676 depot: &'__macro_gen_ex mut salvo::Depot,
677 _arg: &str
678 ) -> ::std::result::Result<Self, impl salvo::Writer + Send + std::fmt::Debug + 'static> {
679 Self::extract(req, depot).await
680 }
681 }
682 }
683 .to_string()
684 );
685 }
686}