third-pact 0.1.2

Macro library for generating basic endpoints for models
Documentation
use proc_macro::TokenStream;
use quote::{quote, ToTokens};
use syn;
mod parsing;
use parsing::{MacroInput, get_field_attributes, Fields, AuthType};

#[proc_macro_attribute]
pub fn model(attr: TokenStream, code: TokenStream) -> TokenStream {
    let ast: syn::ItemStruct = syn::parse(code).unwrap();
    let (ast, fields) = get_field_attributes(ast);
    let new = impl_model(&ast, attr, fields);
    let mut code: TokenStream = ast.into_token_stream().into();
    code.extend(new);
    code
}

fn impl_model(ast: &syn::ItemStruct, attr: TokenStream, field_attributes: Fields) -> TokenStream {
    let input = syn::parse_macro_input!(attr as MacroInput);
    let struct_name = &ast.ident;
    let prim = field_attributes.prim;
    let partition = field_attributes.partition;
    let path = field_attributes.path;
    let collection = input.collection;

    let verify_prim = quote! {
        if instance.#prim != #prim {
            return Err(warp::reject::custom(crate::fault::Fault::IllegalArgument(format!(
                            "{} does not match url ({} != {}).",
                            stringify!(#prim), instance.#prim, #prim
            ))));
        }
    };

    let mut verify = if prim != partition {
        quote! {
            if instance.#partition != #partition {
                return Err(warp::reject::custom(crate::fault::Fault::IllegalArgument(format!(
                                "{} does not match url ({} != {}).",
                                stringify!(#partition), instance.#partition, #partition
                ))));
            }
        }
    } else {
        quote!()
    };

    let (sig_no_prim, sig_prim) = if prim == partition {
        if path.is_some() {
            panic!(
                "If the prim and partition are the same field then no path annotation is allowed"
            );
        }
        (quote!(), quote!(#prim: String,))
    } else {
        let mut path_str = quote!();
        if let Some(path) = path {
            for s in path {
                path_str.extend(quote!(#s: String, ));
                verify.extend(quote! {
                    if instance.#s != #s {
                        return Err(warp::reject::custom(crate::fault::Fault::IllegalArgument(format!(
                                        "{} does not match url ({} != {}).",
                                        stringify!(#s), instance.#s, #s
                        ))));
                    }
                });
            }
        }
        (quote!(#partition: String, #path_str), quote!(#prim: String,))
    };

    let mut get = quote!();
    let mut post = quote!();
    let mut put = quote!();
    let mut delete = quote!();
    let mut image = quote!();
    if let Some(tup) = input.get {
        let authentication = quote_authentication(tup);
        get = quote! {
            pub async fn get(#sig_no_prim#sig_prim claims: crate::models::Claims, _v: u8) -> Result<impl warp::Reply, warp::Rejection> {
                let (instance, _etag): (Self, _) = cosmos_utils::get(#collection, [&#partition], &#prim).await?;
                #verify_prim
                #verify
                #authentication

                Ok(warp::reply::json(&crate::util::DataResponse {
                    data: Some(instance),
                    extra: None::<crate::util::Empty>,
                }))
            }
        };
    }

    if let Some(tup) = input.post {
        let authentication = quote_authentication(tup);
        post = quote! {
            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> {
                let mut instance;
                if let Some(q) = r.data {
                    instance = q;
                } else {
                    return Err(warp::reject::custom(crate::fault::Fault::NoData));
                }
                #verify
                #authentication
                instance.#prim = uuid::Uuid::new_v4().to_string();
                instance.modified = chrono::Utc::now();
                cosmos_utils::insert(#collection, [&instance.#partition], &instance, None).await?;
                Ok(warp::reply::json(&crate::util::DataResponse {
                    data: Some(instance),
                    extra: None::<crate::util::Empty>,
                }))
            }
        };
    }

    if let Some(tup) = input.put {
        let authentication = quote_authentication(tup);
        put = quote! {
            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> {
                let mut new_instance;
                if let Some(q) = r.data {
                    new_instance = q;
                } else {
                    return Err(warp::reject::custom(crate::fault::Fault::NoData));
                }
                #authentication
                let instance = cosmos_utils::modify(#collection, [&#partition], &#prim, |old_instance: Self| {
                    let mut instance = new_instance.clone();
                    #verify_prim
                    #verify
                    instance.modified = chrono::Utc::now();
                    Ok(instance)
                })
                .await?;
                Ok(warp::reply::json(&crate::util::DataResponse {
                    data: Some(instance),
                    extra: None::<crate::util::Empty>,
                }))
            }
        };
    }

    if let Some(tup) = input.delete {
        let authentication = quote_authentication(tup);
        delete = quote! {
            pub async fn delete(#sig_no_prim#sig_prim claims: crate::models::Claims, _v: u8) -> Result<impl warp::Reply, warp::Rejection> {
                #authentication
                let instance = cosmos_utils::modify(#collection, [&#partition], &#prim, |mut instance: Self| {
                    #verify_prim
                    #verify
                    instance.deleted = true;
                    instance.modified = chrono::Utc::now();
                    Ok(instance)
                })
                .await?;
                Ok(warp::reply::json(&crate::util::DataResponse {
                    data: Some(instance),
                    extra: None::<crate::util::Empty>,
                }))
            }
        };
    }

    if let Some(tup) = input.image {
        let authentication = quote_authentication(tup);
        image = quote! {
            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> {
                #authentication
                let (mut instance, etag): (Self, _) = cosmos_utils::get(#collection, [&#partition], &#prim).await?;
                #verify_prim
                #verify
                let image_id = cosmos_utils::upload_image(f).await?;
                instance.images.push(image_id);
                instance.modified = Utc::now();

                cosmos_utils::upsert(#collection, [&#partition], &instance, Some(&etag)).await?;

                // TODO: Delete old image, if any.
                Ok(warp::reply::json(&crate::util::DataResponse {
                    data: Some(instance),
                    extra: None::<crate::util::Empty>,
                }))
            }
        };
    }

    let gen = quote! {
        impl #struct_name {
            #get
            #post
            #put
            #delete
            #image
        }
    };
    gen.into()
}

fn quote_authentication(t: Option<AuthType>) -> proc_macro2::TokenStream {
    match t {
        Some(AuthType::Flag(flgs, None)) => {
            quote! {
                if !crate::util::has_role(None, &claims, #flgs) {
                    return Err(warp::reject::custom(crate::fault::Fault::Forbidden(format!(
                                    "Insufficient roles, caller does not have privileges",
                    ))));
                }
            }
        },
        Some(AuthType::Flag(flgs, Some(res_id))) => {
            quote! {
                if !crate::util::has_role(Some(&#res_id), &claims, #flgs) {
                    return Err(warp::reject::custom(crate::fault::Fault::Forbidden(format!(
                                    "Insufficient roles, caller does not have privileges for {}", stringify!(#res_id)
                    ))));
                }
            }
        },
        Some(AuthType::CallingUser(res_id)) => {
            quote! {
                if claims.sub != #res_id {
                    return Err(warp::reject::custom(crate::fault::Fault::Forbidden(format!(
                                    "Calling user does not have the privilege, {} != {}", claims.sub, stringify!(#res_id)
                    ))));
                }
            }
        },
        None => {
            quote! {}
        }
    }
}