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