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 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#[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#[proc_macro_derive(ToSchema, attributes(salvo))] pub 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#[proc_macro_derive(ToParameters, attributes(salvo))] pub 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#[proc_macro_derive(ToResponse, attributes(salvo))] pub 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#[proc_macro_derive(ToResponses, attributes(salvo))] pub 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 })
147 .map(|s| s.to_token_stream());
148 match stream {
149 Ok(stream) => stream.into(),
150 Err(diag) => diag.emit_as_item_tokens().into(),
151 }
152}
153
154pub(crate) trait IntoInner<T> {
155 fn into_inner(self) -> T;
156}
157
158#[cfg(test)]
159mod tests {
160 use quote::quote;
161 use syn::parse2;
162
163 use super::*;
164
165 #[test]
166 fn test_endpoint_for_fn() {
167 let input = quote! {
168 #[endpoint]
169 async fn hello() {
170 res.render_plain_text("Hello World");
171 }
172 };
173 let item = parse2(input).unwrap();
174 assert_eq!(
175 endpoint::generate(parse2(quote! {}).unwrap(), item)
176 .unwrap()
177 .to_string(),
178 quote! {
179 #[allow(non_camel_case_types)]
180 #[derive(Debug)]
181 struct hello;
182 impl hello {
183 async fn hello() {
184 {res.render_plain_text("Hello World");}
185 }
186 }
187 #[salvo::async_trait]
188 impl salvo::Handler for hello {
189 async fn handle(
190 &self,
191 __macro_gen_req: &mut salvo::Request,
192 __macro_gen_depot: &mut salvo::Depot,
193 __macro_gen_res: &mut salvo::Response,
194 __macro_gen_ctrl: &mut salvo::FlowCtrl
195 ) {
196 Self::hello().await
197 }
198 }
199 fn __macro_gen_oapi_endpoint_type_id_hello() -> ::std::any::TypeId {
200 ::std::any::TypeId::of::<hello>()
201 }
202 fn __macro_gen_oapi_endpoint_creator_hello() -> salvo::oapi::Endpoint {
203 let mut components = salvo::oapi::Components::new();
204 let status_codes: &[salvo::http::StatusCode] = &[];
205 let mut operation = salvo::oapi::Operation::new();
206 if operation.operation_id.is_none() {
207 operation.operation_id = Some(salvo::oapi::naming::assign_name::<hello>(salvo::oapi::naming::NameRule::Auto));
208 }
209 if !status_codes.is_empty() {
210 let responses = std::ops::DerefMut::deref_mut(&mut operation.responses);
211 responses.retain(|k, _| {
212 if let Ok(code) = <salvo::http::StatusCode as std::str::FromStr>::from_str(k) {
213 status_codes.contains(&code)
214 } else {
215 true
216 }
217 });
218 }
219 salvo::oapi::Endpoint {
220 operation,
221 components,
222 }
223 }
224 salvo::oapi::__private::inventory::submit! {
225 salvo::oapi::EndpointRegistry::save(__macro_gen_oapi_endpoint_type_id_hello, __macro_gen_oapi_endpoint_creator_hello)
226 }
227 }
228 .to_string()
229 );
230 }
231
232 #[test]
233 fn test_to_schema_struct() {
234 let input = quote! {
235 #[derive(ToSchema)]
239 struct User {
240 #[salvo(schema(examples("chris"), min_length = 1, max_length = 100, required))]
241 name: String,
242 #[salvo(schema(example = 16, default = 0, maximum=100, minimum=0,format = "int32"))]
243 age: i32,
244 #[deprecated = "There is deprecated"]
245 high: u32,
246 }
247 };
248 assert_eq!(
249 schema::to_schema(parse2(input).unwrap()).unwrap()
250 .to_string(),
251 quote! {
252 impl salvo::oapi::ToSchema for User {
253 fn to_schema(components: &mut salvo::oapi::Components) -> salvo::oapi::RefOr<salvo::oapi::schema::Schema> {
254 let name = salvo::oapi::naming::assign_name::<User>(salvo::oapi::naming::NameRule::Auto);
255 let ref_or = salvo::oapi::RefOr::Ref(salvo::oapi::Ref::new(format!("#/components/schemas/{}", name)));
256 if !components.schemas.contains_key(&name) {
257 components.schemas.insert(name.clone(), ref_or.clone());
258 let schema = salvo::oapi::Object::new()
259 .property(
260 "name",
261 salvo::oapi::Object::new()
262 .schema_type(salvo::oapi::schema::SchemaType::basic(salvo::oapi::schema::BasicType::String))
263 .examples([salvo::oapi::__private::serde_json::json!("chris"),])
264 .min_length(1usize)
265 .max_length(100usize)
266 )
267 .required("name")
268 .property(
269 "age",
270 salvo::oapi::Object::new()
271 .schema_type(salvo::oapi::schema::SchemaType::basic(salvo::oapi::schema::BasicType::Integer))
272 .format(salvo::oapi::SchemaFormat::KnownFormat(salvo::oapi::KnownFormat::Int32))
273 .example(salvo::oapi::__private::serde_json::json!(16))
274 .default_value(salvo::oapi::__private::serde_json::json!(0))
275 .maximum(100f64)
276 .minimum(0f64)
277 .format(salvo::oapi::SchemaFormat::Custom(String::from("int32")))
278 )
279 .required("age")
280 .property(
281 "high",
282 salvo::oapi::Object::new()
283 .schema_type(salvo::oapi::schema::SchemaType::basic(salvo::oapi::schema::BasicType::Integer))
284 .format(salvo::oapi::SchemaFormat::KnownFormat(salvo::oapi::KnownFormat::UInt32))
285 .deprecated(salvo::oapi::Deprecated::True)
286 .minimum(0f64)
287 )
288 .required("high")
289 .description("This is user.\n\nThis is user description.");
290 components.schemas.insert(name, schema);
291 }
292 ref_or
293 }
294 }
295 } .to_string()
296 );
297 }
298
299 #[test]
300 fn test_to_schema_generics() {
301 let input = quote! {
302 #[derive(Serialize, Deserialize, ToSchema, Debug)]
303 #[salvo(schema(aliases(MyI32 = MyObject<i32>, MyStr = MyObject<String>)))]
304 struct MyObject<T: ToSchema + std::fmt::Debug + 'static> {
305 value: T,
306 }
307 };
308 assert_eq!(
309 schema::to_schema(parse2(input).unwrap()).unwrap()
310 .to_string().replace("< ", "<").replace("> ", ">"),
311 quote! {
312 impl<T: ToSchema + std::fmt::Debug + 'static> salvo::oapi::ToSchema for MyObject<T>
313 where
314 T: salvo::oapi::ToSchema + 'static
315 {
316 fn to_schema(components: &mut salvo::oapi::Components) -> salvo::oapi::RefOr<salvo::oapi::schema::Schema> {
317 let mut name = None;
318 if ::std::any::TypeId::of::<Self>() == ::std::any::TypeId::of::<MyObject<i32>>() {
319 name = Some(salvo::oapi::naming::assign_name::<MyObject<i32>>(
320 salvo::oapi::naming::NameRule::Force("MyI32")
321 ));
322 }
323 if ::std::any::TypeId::of::<Self>() == ::std::any::TypeId::of::<MyObject<String>>() {
324 name = Some(salvo::oapi::naming::assign_name::<MyObject<String>>(
325 salvo::oapi::naming::NameRule::Force("MyStr")
326 ));
327 }
328 let name = name
329 .unwrap_or_else(|| salvo::oapi::naming::assign_name::<MyObject<T>>(salvo::oapi::naming::NameRule::Auto));
330 let ref_or = salvo::oapi::RefOr::Ref(salvo::oapi::Ref::new(format!("#/components/schemas/{}", name)));
331 if !components.schemas.contains_key(&name) {
332 components.schemas.insert(name.clone(), ref_or.clone());
333 let schema = salvo::oapi::Object::new()
334 .property(
335 "value",
336 salvo::oapi::RefOr::from(<T as salvo::oapi::ToSchema>::to_schema(components))
337 )
338 .required("value");
339 components.schemas.insert(name, schema);
340 }
341 ref_or
342 }
343 }
344 } .to_string().replace("< ", "<").replace("> ", ">")
345 );
346 }
347
348 #[test]
349 fn test_to_schema_enum() {
350 let input = quote! {
351 #[derive(Serialize, Deserialize, ToSchema, Debug)]
352 #[salvo(schema(rename_all = "camelCase"))]
353 enum People {
354 Man,
355 Woman,
356 }
357 };
358 assert_eq!(
359 schema::to_schema(parse2(input).unwrap()).unwrap()
360 .to_string(),
361 quote! {
362 impl salvo::oapi::ToSchema for People {
363 fn to_schema(components: &mut salvo::oapi::Components) -> salvo::oapi::RefOr<salvo::oapi::schema::Schema> {
364 let name = salvo::oapi::naming::assign_name::<People>(salvo::oapi::naming::NameRule::Auto);
365 let ref_or = salvo::oapi::RefOr::Ref(salvo::oapi::Ref::new(format!("#/components/schemas/{}", name)));
366 if !components.schemas.contains_key(&name) {
367 components.schemas.insert(name.clone(), ref_or.clone());
368 let schema = salvo::oapi::Object::new()
369 .schema_type(salvo::oapi::schema::SchemaType::basic(salvo::oapi::schema::BasicType::String))
370 .enum_values::<[&str; 2usize], &str>(["man", "woman",]);
371 components.schemas.insert(name, schema);
372 }
373 ref_or
374 }
375 }
376 } .to_string()
377 );
378 }
379
380 #[test]
381 fn test_to_response() {
382 let input = quote! {
383 #[derive(ToResponse)]
384 #[salvo(response(description = "Person response returns single Person entity"))]
385 struct User{
386 name: String,
387 age: i32,
388 }
389 };
390 assert_eq!(
391 response::to_response(parse2(input).unwrap()).unwrap()
392 .to_string(),
393 quote! {
394 impl salvo::oapi::ToResponse for User {
395 fn to_response(
396 components: &mut salvo::oapi::Components
397 ) -> salvo::oapi::RefOr<salvo::oapi::Response> {
398 let response = salvo::oapi::Response::new("Person response returns single Person entity").add_content(
399 "application/json",
400 salvo::oapi::Content::new(
401 salvo::oapi::Object::new()
402 .property(
403 "name",
404 salvo::oapi::Object::new().schema_type(salvo::oapi::schema::SchemaType::basic(salvo::oapi::schema::BasicType::String))
405 )
406 .required("name")
407 .property(
408 "age",
409 salvo::oapi::Object::new()
410 .schema_type(salvo::oapi::schema::SchemaType::basic(salvo::oapi::schema::BasicType::Integer))
411 .format(salvo::oapi::SchemaFormat::KnownFormat(
412 salvo::oapi::KnownFormat::Int32
413 ))
414 )
415 .required("age")
416 )
417 );
418 components.responses.insert("User", response);
419 salvo::oapi::RefOr::Ref(salvo::oapi::Ref::new(format!("#/components/responses/{}", "User")))
420 }
421 }
422 impl salvo::oapi::EndpointOutRegister for User {
423 fn register(components: &mut salvo::oapi::Components, operation: &mut salvo::oapi::Operation) {
424 operation
425 .responses
426 .insert("200", <Self as salvo::oapi::ToResponse>::to_response(components))
427 }
428 }
429 } .to_string()
430 );
431 }
432
433 #[test]
434 fn test_to_responses() {
435 let input = quote! {
436 #[derive(salvo_oapi::ToResponses)]
437 enum UserResponses {
438 #[salvo(response(status_code = 200))]
440 Success { value: String },
441
442 #[salvo(response(status_code = 404))]
443 NotFound,
444
445 #[salvo(response(status_code = 400))]
446 BadRequest(BadRequest),
447
448 #[salvo(response(status_code = 500))]
449 ServerError(Response),
450
451 #[salvo(response(status_code = 418))]
452 TeaPot(Response),
453 }
454 };
455 assert_eq!(
456 response::to_responses(parse2(input).unwrap()).unwrap().to_string(),
457 quote! {
458 impl salvo::oapi::ToResponses for UserResponses {
459 fn to_responses(components: &mut salvo::oapi::Components) -> salvo::oapi::response::Responses {
460 [
461 (
462 "200",
463 salvo::oapi::RefOr::from(
464 salvo::oapi::Response::new("Success response description.").add_content(
465 "application/json",
466 salvo::oapi::Content::new(
467 salvo::oapi::Object::new()
468 .property(
469 "value",
470 salvo::oapi::Object::new().schema_type(salvo::oapi::schema::SchemaType::basic(salvo::oapi::schema::BasicType::String))
471 )
472 .required("value")
473 .description("Success response description.")
474 )
475 )
476 )
477 ),
478 (
479 "404",
480 salvo::oapi::RefOr::from(salvo::oapi::Response::new(""))
481 ),
482 (
483 "400",
484 salvo::oapi::RefOr::from(salvo::oapi::Response::new("").add_content(
485 "application/json",
486 salvo::oapi::Content::new(salvo::oapi::RefOr::from(
487 <BadRequest as salvo::oapi::ToSchema>::to_schema(components)
488 ))
489 ))
490 ),
491 (
492 "500",
493 salvo::oapi::RefOr::from(salvo::oapi::Response::new("").add_content(
494 "application/json",
495 salvo::oapi::Content::new(salvo::oapi::RefOr::from(
496 <Response as salvo::oapi::ToSchema>::to_schema(components)
497 ))
498 ))
499 ),
500 (
501 "418",
502 salvo::oapi::RefOr::from(salvo::oapi::Response::new("").add_content(
503 "application/json",
504 salvo::oapi::Content::new(salvo::oapi::RefOr::from(
505 <Response as salvo::oapi::ToSchema>::to_schema(components)
506 ))
507 ))
508 ),
509 ]
510 .into()
511 }
512 }
513 impl salvo::oapi::EndpointOutRegister for UserResponses {
514 fn register(components: &mut salvo::oapi::Components, operation: &mut salvo::oapi::Operation) {
515 operation
516 .responses
517 .append(&mut <Self as salvo::oapi::ToResponses>::to_responses(components));
518 }
519 }
520 }
521 .to_string()
522 );
523 }
524
525 #[test]
526 fn test_to_parameters() {
527 let input = quote! {
528 #[derive(Deserialize, ToParameters)]
529 struct PetQuery {
530 name: Option<String>,
532 age: Option<i32>,
534 #[salvo(parameter(inline))]
536 kind: PetKind
537 }
538 };
539 assert_eq!(
540 parameter::to_parameters(parse2(input).unwrap()).unwrap().to_string(),
541 quote! {
542 impl<'__macro_gen_ex> salvo::oapi::ToParameters<'__macro_gen_ex> for PetQuery {
543 fn to_parameters(components: &mut salvo::oapi::Components) -> salvo::oapi::Parameters {
544 salvo::oapi::Parameters(
545 [
546 salvo::oapi::parameter::Parameter::new("name")
547 .description("Name of pet")
548 .required(salvo::oapi::Required::False)
549 .schema(
550 salvo::oapi::Object::new()
551 .schema_type(salvo::oapi::schema::SchemaType::from_iter([salvo::oapi::schema::BasicType::String, salvo::oapi::schema::BasicType::Null]))
552 ),
553 salvo::oapi::parameter::Parameter::new("age")
554 .description("Age of pet")
555 .required(salvo::oapi::Required::False)
556 .schema(
557 salvo::oapi::Object::new()
558 .schema_type(salvo::oapi::schema::SchemaType::from_iter([salvo::oapi::schema::BasicType::Integer, salvo::oapi::schema::BasicType::Null]))
559 .format(salvo::oapi::SchemaFormat::KnownFormat(
560 salvo::oapi::KnownFormat::Int32
561 ))
562 ),
563 salvo::oapi::parameter::Parameter::new("kind")
564 .description("Kind of pet")
565 .required(salvo::oapi::Required::True)
566 .schema(<PetKind as salvo::oapi::ToSchema>::to_schema(components)),
567 ]
568 .to_vec()
569 )
570 }
571 }
572 impl salvo::oapi::EndpointArgRegister for PetQuery {
573 fn register(
574 components: &mut salvo::oapi::Components,
575 operation: &mut salvo::oapi::Operation,
576 _arg: &str
577 ) {
578 for parameter in <Self as salvo::oapi::ToParameters>::to_parameters(components) {
579 operation.parameters.insert(parameter);
580 }
581 }
582 }
583 impl<'__macro_gen_ex> salvo::Extractible<'__macro_gen_ex> for PetQuery {
584 fn metadata() -> &'static salvo::extract::Metadata {
585 static METADATA: ::std::sync::OnceLock<salvo::extract::Metadata> = ::std::sync::OnceLock::new();
586 METADATA.get_or_init(||
587 salvo::extract::Metadata::new("PetQuery")
588 .default_sources(vec![salvo::extract::metadata::Source::new(
589 salvo::extract::metadata::SourceFrom::Query,
590 salvo::extract::metadata::SourceParser::MultiMap
591 )])
592 .fields(vec![
593 salvo::extract::metadata::Field::new("name"),
594 salvo::extract::metadata::Field::new("age"),
595 salvo::extract::metadata::Field::new("kind")
596 ])
597 )
598 }
599 async fn extract(
600 req: &'__macro_gen_ex mut salvo::Request,
601 depot: &'__macro_gen_ex mut salvo::Depot
602 ) -> ::std::result::Result<Self, impl salvo::Writer + Send + std::fmt::Debug + 'static> {
603 salvo::serde::from_request(req, depot, Self::metadata()).await
604 }
605 async fn extract_with_arg(
606 req: &'__macro_gen_ex mut salvo::Request,
607 depot: &'__macro_gen_ex mut salvo::Depot,
608 _arg: &str
609 ) -> ::std::result::Result<Self, impl salvo::Writer + Send + std::fmt::Debug + 'static> {
610 Self::extract(req, depot).await
611 }
612 }
613 }
614 .to_string()
615 );
616 }
617}