impl_table 0.1.3

Generate table binding and utils for rust-postgres and rusqlite.
Documentation
use crate::Argument;
use proc_macro2::{Punct, Spacing, Span, TokenStream, TokenTree};

#[derive(Debug)]
enum State {
    Start,
    Ident(proc_macro2::Ident),
    Assign(proc_macro2::Ident, proc_macro2::Punct),
    Done,
    Next(proc_macro2::Punct),
    End,

    Error(ErrorStruct),
}

#[derive(Debug)]
pub(crate) struct ErrorStruct(proc_macro2::Span, &'static str);

impl From<ErrorStruct> for syn::Error {
    fn from(e: ErrorStruct) -> syn::Error {
        let ErrorStruct(span, message) = e;
        syn::Error::new(span, message.to_string())
    }
}

// The rust compile does not accept ';' as a procedural macro argument.
// Assuming it will not appear in the input TokenStream.
const EOF: char = ';';

macro_rules! build_error {
    ($p:ident, $($body:tt)*) => {
        State::Error(ErrorStruct($p.span(), $($body)*))
    }
}

fn state_machine_transfer(tt: TokenTree, state: State, ret: &mut Vec<Argument>) -> State {
    match state {
        State::Start => match tt {
            TokenTree::Ident(i) => State::Ident(i),
            TokenTree::Punct(p) => {
                if p.as_char() == EOF {
                    State::End
                } else {
                    build_error!(p, "Expecting an identifier.")
                }
            }
            _ => build_error!(tt, "Expecting an identifier or empty list."),
        },
        State::Ident(i) => match tt {
            TokenTree::Punct(p) => match p.as_char() {
                '=' => State::Assign(i, p),
                ',' => {
                    ret.push(Argument::Switch {
                        name: i.to_string(),
                    });
                    State::Next(p)
                }
                EOF => {
                    ret.push(Argument::Switch {
                        name: i.to_string(),
                    });
                    State::End
                }
                _ => build_error!(p, "Expecting '=', ',', or end of argument list."),
            },
            TokenTree::Group(g) => match parse_arguments(g.stream(), g.span()) {
                Ok(args) => {
                    ret.push(Argument::Function {
                        name: i.to_string(),
                        args: args,
                    });
                    State::Done
                }
                Err(e) => State::Error(e),
            },
            _ => build_error!(tt, "Expecting '=', ',', or end of argument list."),
        },
        State::Assign(i, _p) => match tt {
            TokenTree::Ident(to_i) => {
                ret.push(Argument::Flag {
                    key: i.to_string(),
                    value: to_i.to_string(),
                });
                State::Done
            }
            TokenTree::Literal(to_i) => {
                let string_value = to_i.to_string();
                ret.push(Argument::Flag {
                    key: i.to_string(),
                    // Remove the quotes from string literal.
                    value: string_value[1..string_value.len() - 1].into(),
                });
                State::Done
            }
            _ => build_error!(tt, "Expecting an identifier or a string literal."),
        },
        State::Done => {
            if let TokenTree::Punct(p) = tt {
                match p.as_char() {
                    ',' => State::Next(p),
                    EOF => State::End,
                    _ => build_error!(p, "Expecting ',' or end of argument list."),
                }
            } else {
                build_error!(tt, "Expecting ',' or end of argument list.")
            }
        }
        State::Next(_p) => {
            if let TokenTree::Ident(i) = tt {
                State::Ident(i)
            } else {
                build_error!(tt, "Expecting an identifier.")
            }
        }
        State::Error(_) => state,
        State::End => panic!("Reached end state but still receives more token: {}", tt),
    }
}

// Parse the following argument into a structured form.
// `key = string_value, key = ident, invoke_func(flag1, flag2)`
pub(crate) fn parse_arguments(
    attr: TokenStream,
    outer_span: Span,
) -> Result<Vec<Argument>, ErrorStruct> {
    let mut ret = vec![];
    let mut state = State::Start;
    for tt in attr.into_iter() {
        state = state_machine_transfer(tt, state, &mut ret);
    }

    // Push end of line into the machine to get the error message.
    let mut eof = TokenTree::Punct(Punct::new(EOF, Spacing::Alone));
    // TODO: using the outer span yields less than ideal error message. The last element ')' of the
    // outer span is the right thing to point to.
    eof.set_span(outer_span);
    state = state_machine_transfer(eof, state, &mut ret);

    match state {
        State::Error(e) => return Err(e),
        State::End => (),
        _ => panic!("Reached non-end state {:?}", state),
    }

    return Ok(ret);
}

#[cfg(test)]
mod tests {
    use super::*;
    use quote::quote;

    macro_rules! parse_quote {
        ($($body:tt)*) => ({
            let expr = quote! {
                $($body)*
            };
            parse_arguments(expr.into(), Span::call_site())
                .expect(concat!("Unexpected error when parsing {", stringify!($($body)*), "}"))

        })
    }

    macro_rules! parse_quote_error {
        ($($body:tt)*) => ({
            let expr = quote! {
                $($body)*
            };
            parse_arguments(expr.into(), Span::call_site()).err()
                .expect(concat!("Expecting error when parsing {", stringify!($($body)*), "}"))
        })
    }

    macro_rules! assert_parse_quote_error_msg_eq {
        ($msg:expr, $($body:tt)*) => ({
            let ErrorStruct(_,message) = parse_quote_error!($($body)*);
            assert_eq!($msg, message);
        })
    }

    #[test]
    fn test_empty() {
        let args = parse_quote! {};

        assert_eq!(0, args.len());
    }

    #[test]
    fn test_switch() {
        let args = parse_quote! { TurnThisOn };

        assert_eq!(1, args.len());

        assert_eq!(
            Argument::Switch {
                name: "TurnThisOn".into()
            },
            args[0]
        );
    }

    #[test]
    fn test_flag() {
        let args = parse_quote! {
            SetValue = tts, SetStr = "123"
        };

        assert_eq!(2, args.len());

        assert_eq!(
            Argument::Flag {
                key: "SetValue".into(),
                value: "tts".into()
            },
            args[0]
        );

        assert_eq!(
            Argument::Flag {
                key: "SetStr".into(),
                value: "123".into()
            },
            args[1]
        );
    }

    #[test]
    fn test_function() {
        let args = parse_quote! { CallThisFunc() };

        assert_eq!(1, args.len());
        assert_eq!(
            Argument::Function {
                name: "CallThisFunc".into(),
                args: vec![]
            },
            args[0]
        );
    }

    #[test]
    fn test_function_args() {
        let args = parse_quote! { CallThisFunc(DoX, Doy) };

        assert_eq!(1, args.len());
        assert_eq!(
            Argument::Function {
                name: "CallThisFunc".into(),
                args: vec![
                    Argument::Switch { name: "DoX".into() },
                    Argument::Switch { name: "Doy".into() }
                ]
            },
            args[0]
        );
    }

    #[test]
    fn test_nested_functions() {
        let args = parse_quote! { CallThisFunc(DoX, CallAnotherFunc(Doy), DoZ) };

        assert_eq!(1, args.len());
        assert_eq!(
            Argument::Function {
                name: "CallThisFunc".into(),
                args: vec![
                    Argument::Switch { name: "DoX".into() },
                    Argument::Function {
                        name: "CallAnotherFunc".into(),
                        args: vec![Argument::Switch { name: "Doy".into() }]
                    },
                    Argument::Switch { name: "DoZ".into() }
                ]
            },
            args[0]
        );
    }

    #[test]
    fn test_error_in_the_middle() {
        let ErrorStruct(_, message) = parse_quote_error! { A, , A };
        assert_eq!("Expecting an identifier.", message);
    }

    #[test]
    fn test_error_at_the_end() {
        let ErrorStruct(_, message) = parse_quote_error! { A, };
        assert_eq!("Expecting an identifier.", message);
    }

    #[test]
    fn test_all() {
        let args = parse_quote! {
            Switch0,
            Switch1,
            CallThisFunc(Switch8, Flag = "f", NestedFunc(PP)),
            AnotherFlag = VV,
            OneMore = WW,
            Switch7,
            PlusOne = "ZZZ",
            CallAnotherFunc(DD=AAA),
            Join(),
            Switch9
        };

        assert_eq!(
            vec![
                Argument::Switch {
                    name: "Switch0".into()
                },
                Argument::Switch {
                    name: "Switch1".into()
                },
                Argument::Function {
                    name: "CallThisFunc".into(),
                    args: vec![
                        Argument::Switch {
                            name: "Switch8".into()
                        },
                        Argument::Flag {
                            key: "Flag".into(),
                            value: "f".into()
                        },
                        Argument::Function {
                            name: "NestedFunc".into(),
                            args: vec![Argument::Switch { name: "PP".into() }]
                        }
                    ]
                },
                Argument::Flag {
                    key: "AnotherFlag".into(),
                    value: "VV".into()
                },
                Argument::Flag {
                    key: "OneMore".into(),
                    value: "WW".into()
                },
                Argument::Switch {
                    name: "Switch7".into()
                },
                Argument::Flag {
                    key: "PlusOne".into(),
                    value: "ZZZ".into()
                },
                Argument::Function {
                    name: "CallAnotherFunc".into(),
                    args: vec![Argument::Flag {
                        key: "DD".into(),
                        value: "AAA".into()
                    }]
                },
                Argument::Function {
                    name: "Join".into(),
                    args: vec![]
                },
                Argument::Switch {
                    name: "Switch9".into()
                }
            ],
            args
        );
    }

    #[test]
    fn test_state_machine() {
        // start: ident, EOF, other punct, other
        parse_quote! { A };
        parse_quote! {};
        assert_parse_quote_error_msg_eq! {"Expecting an identifier.", : };
        assert_parse_quote_error_msg_eq! {"Expecting an identifier or empty list.", () };

        // ident: '=', ',', EOF, other punct, group, group error, other
        parse_quote! { A = a };
        parse_quote! { A, A };
        parse_quote! { A };
        assert_parse_quote_error_msg_eq! {"Expecting '=', ',', or end of argument list.", A : };
        parse_quote! { A () };
        assert_parse_quote_error_msg_eq! {"Expecting an identifier.", A (:) };
        assert_parse_quote_error_msg_eq! {"Expecting an identifier.", A ( B ( C, :) ) };
        assert_parse_quote_error_msg_eq! {"Expecting '=', ',', or end of argument list.", A  A };

        // assign: ident, literal, other
        parse_quote! { A = a };
        parse_quote! { A = "a" };
        assert_parse_quote_error_msg_eq! {"Expecting an identifier or a string literal.", A = () };

        // done: ',', EOF, other punct, other
        parse_quote! { A = A, A };
        parse_quote! { A = A };
        assert_parse_quote_error_msg_eq! {"Expecting ',' or end of argument list.", A = A: };
        assert_parse_quote_error_msg_eq! {"Expecting ',' or end of argument list.", A = a () };

        // next: ident, other
        parse_quote! { A = A, A };
        assert_parse_quote_error_msg_eq! {"Expecting an identifier.", A = A, () };

        // error: ignores
        assert_parse_quote_error_msg_eq! {"Expecting an identifier.", A, (), A, A, A};
    }

    #[test]
    #[should_panic]
    fn test_end_end() {
        // end: fails,
        parse_quote_error! { ; A };
    }
}