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 compose_context: None,
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 let result = schema::to_schema(parse2(input).unwrap())
250 .unwrap()
251 .to_string();
252 assert!(
254 result.contains("impl salvo :: oapi :: ComposeSchema for User"),
255 "Expected ComposeSchema impl in output"
256 );
257 assert!(
258 result.contains("impl salvo :: oapi :: ToSchema for User"),
259 "Expected ToSchema impl in output"
260 );
261 assert!(result.contains("\"name\""), "Expected 'name' property");
263 assert!(result.contains("\"age\""), "Expected 'age' property");
264 assert!(result.contains("\"high\""), "Expected 'high' property");
265 assert!(
266 result.contains("This is user.\\n\\nThis is user description."),
267 "Expected description"
268 );
269 }
270
271 #[test]
272 fn test_to_schema_generics() {
273 let input = quote! {
274 #[derive(Serialize, Deserialize, ToSchema, Debug)]
275 #[salvo(schema(aliases(MyI32 = MyObject<i32>, MyStr = MyObject<String>)))]
276 struct MyObject<T: ToSchema + std::fmt::Debug + 'static> {
277 value: T,
278 }
279 };
280 let result = schema::to_schema(parse2(input).unwrap())
281 .unwrap()
282 .to_string()
283 .replace("< ", "<")
284 .replace("> ", ">");
285 assert!(
287 result.contains("salvo :: oapi :: ComposeSchema for MyObject"),
288 "Expected ComposeSchema impl in output"
289 );
290 assert!(
291 result.contains("salvo :: oapi :: ToSchema for MyObject"),
292 "Expected ToSchema impl in output"
293 );
294 assert!(
296 result.contains("__compose_generics"),
297 "Expected __compose_generics usage in ComposeSchema impl"
298 );
299 assert!(result.contains("MyI32"), "Expected MyI32 alias");
301 assert!(result.contains("MyStr"), "Expected MyStr alias");
302 }
303
304 #[test]
305 fn test_to_schema_enum() {
306 let input = quote! {
307 #[derive(Serialize, Deserialize, ToSchema, Debug)]
308 #[salvo(schema(rename_all = "camelCase"))]
309 enum People {
310 Man,
311 Woman,
312 }
313 };
314 let result = schema::to_schema(parse2(input).unwrap())
315 .unwrap()
316 .to_string();
317 assert!(
319 result.contains("impl salvo :: oapi :: ComposeSchema for People"),
320 "Expected ComposeSchema impl in output"
321 );
322 assert!(
323 result.contains("impl salvo :: oapi :: ToSchema for People"),
324 "Expected ToSchema impl in output"
325 );
326 assert!(result.contains("\"man\""), "Expected 'man' variant");
328 assert!(result.contains("\"woman\""), "Expected 'woman' variant");
329 }
330
331 #[test]
332 fn test_to_response() {
333 let input = quote! {
334 #[derive(ToResponse)]
335 #[salvo(response(description = "Person response returns single Person entity"))]
336 struct User{
337 name: String,
338 age: i32,
339 }
340 };
341 assert_eq!(
342 response::to_response(parse2(input).unwrap()).unwrap()
343 .to_string(),
344 quote! {
345 impl salvo::oapi::ToResponse for User {
346 fn to_response(
347 components: &mut salvo::oapi::Components
348 ) -> salvo::oapi::RefOr<salvo::oapi::Response> {
349 let response = salvo::oapi::Response::new("Person response returns single Person entity").add_content(
350 "application/json",
351 salvo::oapi::Content::new(
352 salvo::oapi::Object::new()
353 .property(
354 "name",
355 salvo::oapi::Object::new().schema_type(salvo::oapi::schema::SchemaType::basic(salvo::oapi::schema::BasicType::String))
356 )
357 .required("name")
358 .property(
359 "age",
360 salvo::oapi::Object::new()
361 .schema_type(salvo::oapi::schema::SchemaType::basic(salvo::oapi::schema::BasicType::Integer))
362 .format(salvo::oapi::SchemaFormat::KnownFormat(
363 salvo::oapi::KnownFormat::Int32
364 ))
365 )
366 .required("age")
367 )
368 );
369 components.responses.insert("User", response);
370 salvo::oapi::RefOr::Ref(salvo::oapi::Ref::new(format!("#/components/responses/{}", "User")))
371 }
372 }
373 impl salvo::oapi::EndpointOutRegister for User {
374 fn register(components: &mut salvo::oapi::Components, operation: &mut salvo::oapi::Operation) {
375 operation
376 .responses
377 .insert("200", <Self as salvo::oapi::ToResponse>::to_response(components))
378 }
379 }
380 } .to_string()
381 );
382 }
383
384 #[test]
385 fn test_to_responses() {
386 let input = quote! {
387 #[derive(salvo_oapi::ToResponses)]
388 enum UserResponses {
389 #[salvo(response(status_code = 200))]
391 Success { value: String },
392
393 #[salvo(response(status_code = 404))]
394 NotFound,
395
396 #[salvo(response(status_code = 400))]
397 BadRequest(BadRequest),
398
399 #[salvo(response(status_code = 500))]
400 ServerError(Response),
401
402 #[salvo(response(status_code = 418))]
403 TeaPot(Response),
404 }
405 };
406 assert_eq!(
407 response::to_responses(parse2(input).unwrap()).unwrap().to_string(),
408 quote! {
409 impl salvo::oapi::ToResponses for UserResponses {
410 fn to_responses(components: &mut salvo::oapi::Components) -> salvo::oapi::response::Responses {
411 [
412 (
413 "200",
414 salvo::oapi::RefOr::from(
415 salvo::oapi::Response::new("Success response description.").add_content(
416 "application/json",
417 salvo::oapi::Content::new(
418 salvo::oapi::Object::new()
419 .property(
420 "value",
421 salvo::oapi::Object::new().schema_type(salvo::oapi::schema::SchemaType::basic(salvo::oapi::schema::BasicType::String))
422 )
423 .required("value")
424 .description("Success response description.")
425 )
426 )
427 )
428 ),
429 (
430 "404",
431 salvo::oapi::RefOr::from(salvo::oapi::Response::new(""))
432 ),
433 (
434 "400",
435 salvo::oapi::RefOr::from(salvo::oapi::Response::new("").add_content(
436 "application/json",
437 salvo::oapi::Content::new(salvo::oapi::RefOr::from(
438 <BadRequest as salvo::oapi::ToSchema>::to_schema(components)
439 ))
440 ))
441 ),
442 (
443 "500",
444 salvo::oapi::RefOr::from(salvo::oapi::Response::new("").add_content(
445 "application/json",
446 salvo::oapi::Content::new(salvo::oapi::RefOr::from(
447 <Response as salvo::oapi::ToSchema>::to_schema(components)
448 ))
449 ))
450 ),
451 (
452 "418",
453 salvo::oapi::RefOr::from(salvo::oapi::Response::new("").add_content(
454 "application/json",
455 salvo::oapi::Content::new(salvo::oapi::RefOr::from(
456 <Response as salvo::oapi::ToSchema>::to_schema(components)
457 ))
458 ))
459 ),
460 ]
461 .into()
462 }
463 }
464 impl salvo::oapi::EndpointOutRegister for UserResponses {
465 fn register(components: &mut salvo::oapi::Components, operation: &mut salvo::oapi::Operation) {
466 operation
467 .responses
468 .append(&mut <Self as salvo::oapi::ToResponses>::to_responses(components));
469 }
470 }
471 }
472 .to_string()
473 );
474 }
475
476 #[test]
477 fn test_to_parameters() {
478 let input = quote! {
479 #[derive(Deserialize, ToParameters)]
480 struct PetQuery {
481 name: Option<String>,
483 age: Option<i32>,
485 #[salvo(parameter(inline))]
487 kind: PetKind
488 }
489 };
490 assert_eq!(
491 parameter::to_parameters(parse2(input).unwrap()).unwrap().to_string(),
492 quote! {
493 impl<'__macro_gen_ex> salvo::oapi::ToParameters<'__macro_gen_ex> for PetQuery {
494 fn to_parameters(components: &mut salvo::oapi::Components) -> salvo::oapi::Parameters {
495 salvo::oapi::Parameters(
496 [
497 salvo::oapi::parameter::Parameter::new("name")
498 .description("Name of pet")
499 .required(salvo::oapi::Required::False)
500 .schema(
501 salvo::oapi::Object::new()
502 .schema_type(salvo::oapi::schema::SchemaType::basic(salvo::oapi::schema::BasicType::String))
503 ),
504 salvo::oapi::parameter::Parameter::new("age")
505 .description("Age of pet")
506 .required(salvo::oapi::Required::False)
507 .schema(
508 salvo::oapi::Object::new()
509 .schema_type(salvo::oapi::schema::SchemaType::basic(salvo::oapi::schema::BasicType::Integer))
510 .format(salvo::oapi::SchemaFormat::KnownFormat(
511 salvo::oapi::KnownFormat::Int32
512 ))
513 ),
514 salvo::oapi::parameter::Parameter::new("kind")
515 .description("Kind of pet")
516 .required(salvo::oapi::Required::True)
517 .schema(salvo::oapi::RefOr::from(<PetKind as salvo::oapi::ToSchema>::to_schema(components))),
518 ]
519 .to_vec()
520 )
521 }
522 }
523 impl salvo::oapi::EndpointArgRegister for PetQuery {
524 fn register(
525 components: &mut salvo::oapi::Components,
526 operation: &mut salvo::oapi::Operation,
527 _arg: &str
528 ) {
529 for parameter in <Self as salvo::oapi::ToParameters>::to_parameters(components) {
530 operation.parameters.insert(parameter);
531 }
532 }
533 }
534 impl<'__macro_gen_ex> salvo::Extractible<'__macro_gen_ex> for PetQuery {
535 fn metadata() -> &'static salvo::extract::Metadata {
536 static METADATA: ::std::sync::OnceLock<salvo::extract::Metadata> = ::std::sync::OnceLock::new();
537 METADATA.get_or_init(||
538 salvo::extract::Metadata::new("PetQuery")
539 .default_sources(vec![salvo::extract::metadata::Source::new(
540 salvo::extract::metadata::SourceFrom::Query,
541 salvo::extract::metadata::SourceParser::MultiMap
542 )])
543 .fields(vec![
544 salvo::extract::metadata::Field::new("name"),
545 salvo::extract::metadata::Field::new("age"),
546 salvo::extract::metadata::Field::new("kind")
547 ])
548 )
549 }
550 async fn extract(
551 req: &'__macro_gen_ex mut salvo::Request,
552 depot: &'__macro_gen_ex mut salvo::Depot
553 ) -> ::std::result::Result<Self, impl salvo::Writer + Send + std::fmt::Debug + 'static> {
554 salvo::serde::from_request(req, depot, Self::metadata()).await
555 }
556 async fn extract_with_arg(
557 req: &'__macro_gen_ex mut salvo::Request,
558 depot: &'__macro_gen_ex mut salvo::Depot,
559 _arg: &str
560 ) -> ::std::result::Result<Self, impl salvo::Writer + Send + std::fmt::Debug + 'static> {
561 Self::extract(req, depot).await
562 }
563 }
564 }
565 .to_string()
566 );
567 }
568}