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#![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#[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 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 #[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 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 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 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 assert!(
297 result.contains("__compose_generics"),
298 "Expected __compose_generics usage in ComposeSchema impl"
299 );
300 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 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 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 #[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: Option<String>,
484 age: Option<i32>,
486 #[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}