rocketjson_macro/
lib.rs

1#![recursion_limit="256"]
2
3extern crate proc_macro;
4
5use quote::quote;
6use darling::FromDeriveInput;
7
8#[derive(Debug, FromDeriveInput)]
9#[darling(attributes(validate))]
10struct JsonBodyOpts {
11    #[darling(default)]
12    context: Option<syn::Path>,
13}
14
15///# Validated Json Input
16///Structs that derive [`JsonBody`] can be used as Endpoint Input.
17///The data is read from the body as Json and validated via [`Validator`].
18///If arguments are passed to cusom Validators
19///`#[validate(custom(function="validate_password", arg="&'v_a Config"))]`
20///they are read from [state](https://docs.rs/rocket/0.5.0-rc.1/rocket/struct.Rocket.html#method.state)
21///# Requirements
22///The struct has to implement [`serde::Deserialize`] and [`validator::Validate`]
23///# Example
24///```
25///#[derive(serde::Deserialize, validator::Validate, rocketjson::JsonBody)]
26///pub struct TestRequest {
27///   #[validate(length(min = 1))]
28///   username: String 
29///}
30///
31///#[post("/register", data="<data>")]
32///pub fn register(data: RegisterRequest) {
33/// //data is validated from json body
34///}
35///```
36///[`Validator`]: https://github.com/Keats/validator
37///[`validator::Validate`]: https://docs.rs/validator/0.14.0/validator/trait.Validate.html
38///[`serde::Deserialize`]: https://docs.serde.rs/serde/trait.Deserialize.html
39#[proc_macro_derive(JsonBody, attributes(validate))]
40pub fn derive_jsonbody(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
41    let ast = syn::parse_macro_input!(input as syn::DeriveInput);
42    let name = &ast.ident;
43
44    let opts = JsonBodyOpts::from_derive_input(&ast).unwrap();
45
46    let (ctx_line, validate_call) = if let Some(ctx) = opts.context {
47        (
48            // Generate fetching of context from Rocket state
49            quote! {
50                let ctx: & #ctx = req.rocket().state::<#ctx>().expect("context state not found");
51            },
52            // Call validate_with_args if context exists
53            quote! { obj.validate_with_args(ctx) }
54        )
55    } else {
56        (
57            // No context line
58            quote! {},
59            // Call normal validate
60            quote! { obj.validate() }
61        )
62    };
63
64    let gen = quote! {
65        #[rocket::async_trait]
66        impl<'r> rocket::data::FromData<'r> for #name {
67            type Error = ();
68
69            async fn from_data(
70                req: &'r rocket::Request<'_>,
71                data: rocket::data::Data<'r>
72            ) -> rocket::data::Outcome<'r, Self> {
73                //NOTE: forward if not JSON
74                if req.content_type() != Some(&rocket::http::ContentType::new("application", "json")) {
75                    return rocket::data::Outcome::Forward((data, rocket::http::Status::Continue));
76                }
77
78                //NOTE: parse JSON
79                let json_outcome = rocket::serde::json::Json::<Self>::from_data(req, data).await;
80
81                let obj = match json_outcome {
82                    rocket::data::Outcome::Success(json) => json.into_inner(),
83                    rocket::data::Outcome::Error(_) => {
84                        req.local_cache(|| rocketjson::error::JsonBodyError::JsonValidationError);
85                        return rocket::data::Outcome::Error((rocket::http::Status::BadRequest, ()));
86                    },
87                    rocket::data::Outcome::Forward(f) => return rocket::data::Outcome::Forward(f),
88                };
89
90                //NOTE: validate
91                #ctx_line
92
93                if let Err(errors) = #validate_call {
94                    req.local_cache(|| rocketjson::error::JsonBodyError::ValidationError(errors));
95                    return rocket::data::Outcome::Error((rocket::http::Status::BadRequest, ()));
96                }
97
98                rocket::data::Outcome::Success(obj)
99            }
100        }
101    };
102
103    gen.into()
104}