instruct 0.1.0

A language to write general purpose 'makefile like' tasks which are powerful and reuseable
Documentation
use nom::{
    branch::alt,
    bytes::complete::tag,
    character::complete::char,
    character::complete::{alpha1, alphanumeric1, space0},
    combinator::{not, recognize},
    error::ParseError,
    multi::{many0_count, separated_list1},
    sequence::{pair, preceded},
    IResult,
};

use crate::parse::ast::{VariableBinding, VariableBindings};

pub fn output_variable_bindings<'a, E: ParseError<&'a str>>(
    i: &'a str,
) -> IResult<&'a str, VariableBindings, E> {
    let (i, _) = preceded(space0, tag("let"))(i)?;
    let (i, bindings) = variable_bindings(i)?;
    let (i, _) = preceded(space0, tag("from"))(i)?;
    Ok((i, bindings))
}

pub fn option_variable_bindings<'a, E: ParseError<&'a str>>(
    i: &'a str,
) -> IResult<&'a str, VariableBindings, E> {
    let (i, _) = preceded(space0, tag("with"))(i)?;
    let (i, bindings) = variable_bindings(i)?;
    Ok((i, bindings))
}

fn variable_bindings<'a, E: ParseError<&'a str>>(
    i: &'a str,
) -> IResult<&'a str, VariableBindings, E> {
    let (i, _) = preceded(space0, char('('))(i)?;
    let (i, bindings) = separated_list1(
        char(','),
        alt((single_variable_binding, dual_variable_binding)),
    )(i)?;
    let (i, _) = preceded(space0, char(')'))(i)?;
    Ok((i, VariableBindings { bindings }))
}

fn dual_variable_binding<'a, E: ParseError<&'a str>>(
    i: &'a str,
) -> IResult<&'a str, VariableBinding, E> {
    let (i, output) = preceded(space0, variable)(i)?;
    let (i, _) = preceded(space0, char(':'))(i)?;
    let (i, input) = preceded(space0, variable)(i)?;
    Ok((i, VariableBinding::Dual(output, input)))
}

fn single_variable_binding<'a, E: ParseError<&'a str>>(
    i: &'a str,
) -> IResult<&'a str, VariableBinding, E> {
    let (i, input) = preceded(space0, variable)(i)?;
    not(preceded(space0, char(':')))(i)?;
    Ok((i, VariableBinding::Single(input)))
}

pub fn variable<'a, E: ParseError<&'a str>>(i: &'a str) -> IResult<&'a str, String, E> {
    let (i, name) = recognize(pair(
        alt((alpha1, tag("_"), tag("-"))),
        many0_count(alt((alphanumeric1, tag("_"), tag("-")))),
    ))(i)?;

    Ok((i, name.into()))
}

#[cfg(test)]
mod tests {
    use super::*;
    use nom::error::Error;

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

        #[test]
        fn ok_simple() {
            assert_eq!(variable::<Error<&str>>("x"), Ok(("", "x".into())));
        }

        #[test]
        fn ok_complex() {
            assert_eq!(
                variable::<Error<&str>>("var_with_complex_name1234"),
                Ok(("", "var_with_complex_name1234".into()))
            );
        }

        #[test]
        fn ok_complex_preceded_underscore() {
            assert_eq!(
                variable::<Error<&str>>("_var_with_complex_name"),
                Ok(("", "_var_with_complex_name".into()))
            );
        }

        #[test]
        fn nok_preceded_number() {
            assert!(variable::<Error<&str>>("1x").is_err());
        }

        #[test]
        fn nok_special_char1() {
            assert_eq!(
                variable::<Error<&str>>("var-name"),
                Ok(("", "var-name".into()))
            );
        }

        #[test]
        fn nok_special_char2() {
            assert_ne!(
                variable::<Error<&str>>("var()name"),
                Ok(("", "var()name".into()))
            );
        }
    }

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

        #[test]
        fn ok_simple() {
            assert_eq!(
                single_variable_binding::<Error<&str>>("x"),
                Ok(("", "x".into()))
            );
        }

        #[test]
        fn ok_preceded_spaces() {
            assert_eq!(
                single_variable_binding::<Error<&str>>("  x"),
                Ok(("", "x".into()))
            );
        }

        #[test]
        fn nok_dual_binding() {
            assert_ne!(
                single_variable_binding::<Error<&str>>("x: y"),
                Ok(("", ("x", "y").into()))
            );
        }

        #[test]
        fn nok_double_dot() {
            assert_ne!(
                single_variable_binding::<Error<&str>>("x:"),
                Ok(("", "x".into()))
            );
        }
    }

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

        #[test]
        fn ok_simple() {
            assert_eq!(
                dual_variable_binding::<Error<&str>>("x: y"),
                Ok(("", ("x", "y").into()))
            );
        }

        #[test]
        fn ok_preceded_spaces() {
            assert_eq!(
                dual_variable_binding::<Error<&str>>("  x :  y"),
                Ok(("", ("x", "y").into()))
            );
        }

        #[test]
        fn nok_single_binding() {
            assert!(dual_variable_binding::<Error<&str>>("x").is_err());
        }

        #[test]
        fn nok_missing_right_side() {
            assert!(dual_variable_binding::<Error<&str>>("x:").is_err());
        }

        #[test]
        fn nok_missing_double_dot() {
            assert!(dual_variable_binding::<Error<&str>>("x y").is_err());
        }
    }

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

        #[test]
        fn ok_simple() {
            assert_eq!(
                output_variable_bindings::<Error<&str>>("let (x: y) from"),
                Ok((
                    "",
                    VariableBindings {
                        bindings: vec!(("x", "y").into())
                    }
                ))
            );
        }

        #[test]
        fn ok_spaced() {
            assert_eq!(
                output_variable_bindings::<Error<&str>>("let   (  x  :  y)     from"),
                Ok((
                    "",
                    VariableBindings {
                        bindings: vec!(("x", "y").into())
                    }
                ))
            );
        }

        #[test]
        fn ok_no_spaces() {
            assert_eq!(
                output_variable_bindings::<Error<&str>>("let(x:y)from"),
                Ok((
                    "",
                    VariableBindings {
                        bindings: vec!(("x", "y").into())
                    }
                ))
            );
        }

        #[test]
        fn ok_multiple() {
            assert_eq!(
                output_variable_bindings::<Error<&str>>(
                    "let (x: y, stdout, stderr: input_var) from"
                ),
                Ok((
                    "",
                    VariableBindings {
                        bindings: vec!(
                            ("x", "y").into(),
                            "stdout".into(),
                            ("stderr", "input_var").into()
                        )
                    }
                ))
            );
        }

        #[test]
        fn nok_missing_double_dot() {
            assert_ne!(
                output_variable_bindings::<Error<&str>>("let (x y) from"),
                Ok((
                    "",
                    VariableBindings {
                        bindings: vec!(("x", "y").into())
                    }
                ))
            );
        }

        #[test]
        fn nok_missing_brackets() {
            assert_ne!(
                output_variable_bindings::<Error<&str>>("let (x: y from"),
                Ok((
                    "",
                    VariableBindings {
                        bindings: vec!(("x", "y").into())
                    }
                ))
            );
        }

        #[test]
        fn nok_missing_from() {
            assert_ne!(
                output_variable_bindings::<Error<&str>>("let (x: y)"),
                Ok((
                    "",
                    VariableBindings {
                        bindings: vec!(("x", "y").into())
                    }
                ))
            );
        }
    }

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

        #[test]
        fn ok_simple() {
            assert_eq!(
                option_variable_bindings::<Error<&str>>("with (x: y)"),
                Ok((
                    "",
                    VariableBindings {
                        bindings: vec!(("x", "y").into())
                    }
                ))
            );
        }

        #[test]
        fn ok_spaced() {
            assert_eq!(
                option_variable_bindings::<Error<&str>>("with   (  x  :  y)"),
                Ok((
                    "",
                    VariableBindings {
                        bindings: vec!(("x", "y").into())
                    }
                ))
            );
        }

        #[test]
        fn ok_multiple() {
            assert_eq!(
                option_variable_bindings::<Error<&str>>("with (x: y, stdout, stderr: input_var)"),
                Ok((
                    "",
                    VariableBindings {
                        bindings: vec!(
                            ("x", "y").into(),
                            "stdout".into(),
                            ("stderr", "input_var").into()
                        )
                    }
                ))
            );
        }

        #[test]
        fn nok_missing_double_dot() {
            assert_ne!(
                option_variable_bindings::<Error<&str>>("with (x y)"),
                Ok((
                    "",
                    VariableBindings {
                        bindings: vec!(("x", "y").into())
                    }
                ))
            );
        }

        #[test]
        fn nok_missing_brackets() {
            assert_ne!(
                option_variable_bindings::<Error<&str>>("with (x: y"),
                Ok((
                    "",
                    VariableBindings {
                        bindings: vec!(("x", "y").into())
                    }
                ))
            );
        }

        #[test]
        fn nok_missing_with() {
            assert_ne!(
                option_variable_bindings::<Error<&str>>("(x: y)"),
                Ok((
                    "",
                    VariableBindings {
                        bindings: vec!(("x", "y").into())
                    }
                ))
            );
        }
    }
}