third-pact 0.1.2

Macro library for generating basic endpoints for models
Documentation
use syn::parse::{Parse, ParseStream};
use syn::{parenthesized, Ident, Path, Result};

struct Intermediate {
    name: Ident,
    body: Vec<Path>,
}

impl Parse for Intermediate {
    fn parse(input: ParseStream) -> Result<Self> {
        let name: Ident = input.parse().unwrap();
        let body;
        parenthesized!(body in input);
        let body = body
            .parse_terminated::<_, syn::token::Comma>(Path::parse)
            .unwrap()
            .into_iter()
            .collect();
        Ok(Intermediate { name, body })
    }
}

pub enum AuthType {
    CallingUser(Ident),
    Flag(Path, Option<Ident>),
}

pub struct MacroInput {
    pub collection: syn::Path,
    pub get: Option<Option<AuthType>>,
    pub post: Option<Option<AuthType>>,
    pub put: Option<Option<AuthType>>,
    pub delete: Option<Option<AuthType>>,
    pub image: Option<Option<AuthType>>,
}

impl Parse for MacroInput {
    fn parse(attr: ParseStream) -> Result<Self> {
        let mut collection = None;
        let mut get = None;
        let mut post = None;
        let mut put = None;
        let mut delete = None;
        let mut image = None;
        let arg_list: syn::punctuated::Punctuated<Intermediate, syn::token::Comma> =
            syn::punctuated::Punctuated::parse_terminated(attr).unwrap();
        for mut arg in arg_list {
            let name = arg.name.to_string().to_lowercase();
            let name = name.as_str();
            match name {
                "collection" => {
                    assert!(arg.body.len() == 1, "Only one argument in collection allowed");
                    collection = arg.body.pop();
                },
                "get" => {
                    // TODO: Use parse auth type like in image
                    get = match arg.body.len() {
                        0 => Some(None),
                        1 => {
                            let flg = arg.body.pop().unwrap();
                            Some(Some(AuthType::Flag(flg, None)))
                        },
                        2 => {
                            let resource_id = arg.body.pop().unwrap().get_ident().cloned().unwrap();
                            let flg = arg.body.pop().unwrap();
                            if flg.get_ident().is_some() && &flg.get_ident().unwrap().to_string() == "SELF" {
                                Some(Some(AuthType::CallingUser(resource_id)))
                            } else {
                                Some(Some(AuthType::Flag(flg, Some(resource_id))))
                            }
                        },
                        _ => {
                            panic!("Wrong number of arguments for get");
                        },
                    };
                },
                "post" => {
                    // TODO: Use parse auth type like in image
                    post = match arg.body.len() {
                        0 => Some(None),
                        1 => {
                            let flg = arg.body.pop().unwrap();
                            Some(Some(AuthType::Flag(flg, None)))
                        },
                        2 => {
                            let resource_id = arg.body.pop().unwrap().get_ident().cloned().unwrap();
                            let flg = arg.body.pop().unwrap();
                            if flg.get_ident().is_some() && &flg.get_ident().unwrap().to_string() == "SELF" {
                                Some(Some(AuthType::CallingUser(resource_id)))
                            } else {
                                Some(Some(AuthType::Flag(flg, Some(resource_id))))
                            }
                        },
                        _ => {
                            panic!("Wrong number of arguments for get");
                        },
                    };
                },
                "put" => {
                    // TODO: Use parse auth type like in image
                    put = match arg.body.len() {
                        0 => Some(None),
                        1 => {
                            let flg = arg.body.pop().unwrap();
                            Some(Some(AuthType::Flag(flg, None)))
                        },
                        2 => {
                            let resource_id = arg.body.pop().unwrap().get_ident().cloned().unwrap();
                            let flg = arg.body.pop().unwrap();
                            if flg.get_ident().is_some() && &flg.get_ident().unwrap().to_string() == "SELF" {
                                Some(Some(AuthType::CallingUser(resource_id)))
                            } else {
                                Some(Some(AuthType::Flag(flg, Some(resource_id))))
                            }
                        },
                        _ => {
                            panic!("Wrong number of arguments for get");
                        },
                    };
                },
                "delete" => {
                    // TODO: Use parse auth type like in image
                    delete = match arg.body.len() {
                        0 => Some(None),
                        1 => {
                            let flg = arg.body.pop().unwrap();
                            Some(Some(AuthType::Flag(flg, None)))
                        },
                        2 => {
                            let resource_id = arg.body.pop().unwrap().get_ident().cloned().unwrap();
                            let flg = arg.body.pop().unwrap();
                            if flg.get_ident().is_some() && &flg.get_ident().unwrap().to_string() == "SELF" {
                                Some(Some(AuthType::CallingUser(resource_id)))
                            } else {
                                Some(Some(AuthType::Flag(flg, Some(resource_id))))
                            }
                        },
                        _ => {
                            panic!("Wrong number of arguments for get");
                        },
                    };
                },
                "image" => {
                    image = Some(parse_auth_type(arg.body));
                },
                invalid => panic!("Macro input contains unknown keyword: {}, valid keywords are: COLLECTION, GET, POST, PUT, DELETE, IMAGE", invalid),
            }
        }

        Ok(MacroInput {
            collection: collection.expect("Collection not found"),
            get,
            post,
            put,
            delete,
            image,
        })
    }
}

pub struct Fields {
    pub prim: syn::Ident,
    pub partition: syn::Ident,
    pub path: Option<Vec<syn::Ident>>,
}

/// Parses the struct for field attributes, removes applicable ones and return them in a `Field`
/// struct. Then returns the new AST.
pub fn get_field_attributes(mut ast: syn::ItemStruct) -> (syn::ItemStruct, Fields) {
    let fields = match &mut ast.fields {
        syn::Fields::Named(n) => &mut n.named,
        _ => panic!("Model can only be applied to named structs"),
    };
    // Parse the attributes for the fields
    let mut prim = None;
    let mut partition = None;
    let mut path: Option<Vec<syn::Ident>> = None;
    for field in fields.iter_mut() {
        let mut remaining_attrs = vec![];
        let attrs = std::mem::take(&mut field.attrs);
        for attr in attrs {
            let mut found = false;
            for p in &attr.path.segments {
                if p.ident == "prim" {
                    if let Some(_) = prim {
                        panic!("Only one prim allowed");
                    }
                    prim = field.ident.clone();
                    found = true;
                }
                if p.ident == "partition" {
                    if let Some(_) = partition {
                        panic!("Only one partition allowed");
                    }
                    partition = field.ident.clone();
                    found = true;
                }
                if p.ident == "path" {
                    if let Some(v) = &mut path {
                        v.push(field.ident.clone().unwrap());
                    } else {
                        path = Some(vec![field.ident.clone().unwrap()]);
                    }
                    found = true;
                }
            }
            if !found {
                remaining_attrs.push(attr);
            }
        }
        field.attrs = remaining_attrs;
    }

    let prim = prim.expect("A model needs to have a prim field, annotate with #[prim]");
    let partition =
        partition.expect("A model needs to have a partition field, annotate with #[partition]");
    let fields = Fields {
        prim,
        partition,
        path,
    };
    (ast, fields)
}

fn parse_auth_type(mut body: Vec<Path>) -> Option<AuthType> {
    match body.len() {
        0 => None,
        1 => {
            let flg = body.pop().unwrap();
            Some(AuthType::Flag(flg, None))
        },
        2 => {
            let resource_id = body.pop().unwrap().get_ident().cloned().unwrap();
            let flg = body.pop().unwrap();
            if flg.get_ident().is_some() && &flg.get_ident().unwrap().to_string() == "SELF" {
                Some(AuthType::CallingUser(resource_id))
            } else {
                Some(AuthType::Flag(flg, Some(resource_id)))
            }
        },
        _ => {
            panic!("Wrong number of arguments for get");
        },
    }
}