third_pact/
lib.rs

1use proc_macro::TokenStream;
2use quote::{quote, ToTokens};
3use syn;
4mod parsing;
5use parsing::{MacroInput, get_field_attributes, Fields, AuthType};
6
7#[proc_macro_attribute]
8pub fn model(attr: TokenStream, code: TokenStream) -> TokenStream {
9    let ast: syn::ItemStruct = syn::parse(code).unwrap();
10    let (ast, fields) = get_field_attributes(ast);
11    let new = impl_model(&ast, attr, fields);
12    let mut code: TokenStream = ast.into_token_stream().into();
13    code.extend(new);
14    code
15}
16
17fn impl_model(ast: &syn::ItemStruct, attr: TokenStream, field_attributes: Fields) -> TokenStream {
18    let input = syn::parse_macro_input!(attr as MacroInput);
19    let struct_name = &ast.ident;
20    let prim = field_attributes.prim;
21    let partition = field_attributes.partition;
22    let path = field_attributes.path;
23    let collection = input.collection;
24
25    let verify_prim = quote! {
26        if instance.#prim != #prim {
27            return Err(warp::reject::custom(crate::fault::Fault::IllegalArgument(format!(
28                            "{} does not match url ({} != {}).",
29                            stringify!(#prim), instance.#prim, #prim
30            ))));
31        }
32    };
33
34    let mut verify = if prim != partition {
35        quote! {
36            if instance.#partition != #partition {
37                return Err(warp::reject::custom(crate::fault::Fault::IllegalArgument(format!(
38                                "{} does not match url ({} != {}).",
39                                stringify!(#partition), instance.#partition, #partition
40                ))));
41            }
42        }
43    } else {
44        quote!()
45    };
46
47    let (sig_no_prim, sig_prim) = if prim == partition {
48        if path.is_some() {
49            panic!(
50                "If the prim and partition are the same field then no path annotation is allowed"
51            );
52        }
53        (quote!(), quote!(#prim: String,))
54    } else {
55        let mut path_str = quote!();
56        if let Some(path) = path {
57            for s in path {
58                path_str.extend(quote!(#s: String, ));
59                verify.extend(quote! {
60                    if instance.#s != #s {
61                        return Err(warp::reject::custom(crate::fault::Fault::IllegalArgument(format!(
62                                        "{} does not match url ({} != {}).",
63                                        stringify!(#s), instance.#s, #s
64                        ))));
65                    }
66                });
67            }
68        }
69        (quote!(#partition: String, #path_str), quote!(#prim: String,))
70    };
71
72    let mut get = quote!();
73    let mut post = quote!();
74    let mut put = quote!();
75    let mut delete = quote!();
76    let mut image = quote!();
77    if let Some(tup) = input.get {
78        let authentication = quote_authentication(tup);
79        get = quote! {
80            pub async fn get(#sig_no_prim#sig_prim claims: crate::models::Claims, _v: u8) -> Result<impl warp::Reply, warp::Rejection> {
81                let (instance, _etag): (Self, _) = cosmos_utils::get(#collection, [&#partition], &#prim).await?;
82                #verify_prim
83                #verify
84                #authentication
85
86                Ok(warp::reply::json(&crate::util::DataResponse {
87                    data: Some(instance),
88                    extra: None::<crate::util::Empty>,
89                }))
90            }
91        };
92    }
93
94    if let Some(tup) = input.post {
95        let authentication = quote_authentication(tup);
96        post = quote! {
97            pub async fn post(#sig_no_prim r: crate::util::DataRequest<Self, crate::util::Empty>, claims: crate::models::Claims, _v: u8) -> Result<impl warp::Reply, warp::Rejection> {
98                let mut instance;
99                if let Some(q) = r.data {
100                    instance = q;
101                } else {
102                    return Err(warp::reject::custom(crate::fault::Fault::NoData));
103                }
104                #verify
105                #authentication
106                instance.#prim = uuid::Uuid::new_v4().to_string();
107                instance.modified = chrono::Utc::now();
108                cosmos_utils::insert(#collection, [&instance.#partition], &instance, None).await?;
109                Ok(warp::reply::json(&crate::util::DataResponse {
110                    data: Some(instance),
111                    extra: None::<crate::util::Empty>,
112                }))
113            }
114        };
115    }
116
117    if let Some(tup) = input.put {
118        let authentication = quote_authentication(tup);
119        put = quote! {
120            pub async fn put(#sig_no_prim#sig_prim r: crate::util::DataRequest<Self, crate::util::Empty>, claims: crate::models::Claims, _v: u8) -> Result<impl warp::Reply, warp::Rejection> {
121                let mut new_instance;
122                if let Some(q) = r.data {
123                    new_instance = q;
124                } else {
125                    return Err(warp::reject::custom(crate::fault::Fault::NoData));
126                }
127                #authentication
128                let instance = cosmos_utils::modify(#collection, [&#partition], &#prim, |old_instance: Self| {
129                    let mut instance = new_instance.clone();
130                    #verify_prim
131                    #verify
132                    instance.modified = chrono::Utc::now();
133                    Ok(instance)
134                })
135                .await?;
136                Ok(warp::reply::json(&crate::util::DataResponse {
137                    data: Some(instance),
138                    extra: None::<crate::util::Empty>,
139                }))
140            }
141        };
142    }
143
144    if let Some(tup) = input.delete {
145        let authentication = quote_authentication(tup);
146        delete = quote! {
147            pub async fn delete(#sig_no_prim#sig_prim claims: crate::models::Claims, _v: u8) -> Result<impl warp::Reply, warp::Rejection> {
148                #authentication
149                let instance = cosmos_utils::modify(#collection, [&#partition], &#prim, |mut instance: Self| {
150                    #verify_prim
151                    #verify
152                    instance.deleted = true;
153                    instance.modified = chrono::Utc::now();
154                    Ok(instance)
155                })
156                .await?;
157                Ok(warp::reply::json(&crate::util::DataResponse {
158                    data: Some(instance),
159                    extra: None::<crate::util::Empty>,
160                }))
161            }
162        };
163    }
164
165    if let Some(tup) = input.image {
166        let authentication = quote_authentication(tup);
167        image = quote! {
168            pub async fn image(#sig_no_prim#sig_prim claims: crate::models::Claims, _v: u8, f: warp::filters::multipart::FormData) -> Result<impl warp::Reply, warp::Rejection> {
169                #authentication
170                let (mut instance, etag): (Self, _) = cosmos_utils::get(#collection, [&#partition], &#prim).await?;
171                #verify_prim
172                #verify
173                let image_id = cosmos_utils::upload_image(f).await?;
174                instance.images.push(image_id);
175                instance.modified = Utc::now();
176
177                cosmos_utils::upsert(#collection, [&#partition], &instance, Some(&etag)).await?;
178
179                // TODO: Delete old image, if any.
180                Ok(warp::reply::json(&crate::util::DataResponse {
181                    data: Some(instance),
182                    extra: None::<crate::util::Empty>,
183                }))
184            }
185        };
186    }
187
188    let gen = quote! {
189        impl #struct_name {
190            #get
191            #post
192            #put
193            #delete
194            #image
195        }
196    };
197    gen.into()
198}
199
200fn quote_authentication(t: Option<AuthType>) -> proc_macro2::TokenStream {
201    match t {
202        Some(AuthType::Flag(flgs, None)) => {
203            quote! {
204                if !crate::util::has_role(None, &claims, #flgs) {
205                    return Err(warp::reject::custom(crate::fault::Fault::Forbidden(format!(
206                                    "Insufficient roles, caller does not have privileges",
207                    ))));
208                }
209            }
210        },
211        Some(AuthType::Flag(flgs, Some(res_id))) => {
212            quote! {
213                if !crate::util::has_role(Some(&#res_id), &claims, #flgs) {
214                    return Err(warp::reject::custom(crate::fault::Fault::Forbidden(format!(
215                                    "Insufficient roles, caller does not have privileges for {}", stringify!(#res_id)
216                    ))));
217                }
218            }
219        },
220        Some(AuthType::CallingUser(res_id)) => {
221            quote! {
222                if claims.sub != #res_id {
223                    return Err(warp::reject::custom(crate::fault::Fault::Forbidden(format!(
224                                    "Calling user does not have the privilege, {} != {}", claims.sub, stringify!(#res_id)
225                    ))));
226                }
227            }
228        },
229        None => {
230            quote! {}
231        }
232    }
233}