docopticon 0.1.2

An argument-parser based on the obligatory help-text
Documentation
//!

use crate::{
    args::ArgTree,
    error::Error,
    parser::{self, Parser},
    Result,
};

/// Substates of Usage parsing.
#[derive(PartialEq, PartialOrd, Eq, Ord)]
pub enum UsageState {
    Start,
    InUsage,
    Command,
    Optional,
    Repeatable,
    Required,
    Argument,
    End,
}

// flags! {
//     #[repr(u32)]
//     enum ArgFlags: u32 {
//         /// Argument is optional. If it is not, an error will be returned when
//         /// parsing the argument list.
//         Optional,
//         /// Argument is globally specifiable. Meaning its order in the argument list does not matter.
//         Global,
//         /// Argument is repeatable. If it is not, an error will be returned if multiple of the same
//         /// argument is in the argument list.
//         Repeatable,
//         /// Argument is an alias for another argument.
//         Alias,
//         /// Argument is hidden from help.
//         Hidden,
//     }
// }

/// Parser of docopt.
pub struct Docopt<'d> {
    /// Internal parser instance.
    parser: Parser<'d>,
    /// Parser settings on how consistency issues in the help text should be handled if at all.
    ///
    /// If `None` parser will try to do its best to guess `[Settings]`.
    pub settings: Option<Settings>,
}

/// A collection of parser settings that will adjust how the parser should parse certain elements.
pub struct Settings {
    /// Leading number of spaces before an option; no more, no less.
    leading_num_spaces: usize,
    /// Whether or not arguments should be separated with an equals sign '=' or a space ' '.
    arguments_uses_equals_sign: bool,
    /// Whether or not argument names should only be upper-case or be surrounded by angle
    /// brackets ('<', '>').
    arguments_are_upper_case: bool,
    /// Whether or not aliases need to have a comma ',' before the next alias.
    comma_separated_aliases: bool,
    /// Whether or not options with aliases need to have the single-dashed '-' options before the
    /// double-dashed options '--'.
    short_opt_first: bool,
}

impl Default for Settings {
    fn default() -> Self {
        Self {
            leading_num_spaces: 2,
            arguments_uses_equals_sign: true,
            arguments_are_upper_case: false,
            comma_separated_aliases: true,
            short_opt_first: true,
        }
    }
}

impl<'d> Docopt<'d> {
    pub const fn new(data: &'d str) -> Self {
        Self {
            parser: Parser::new(data, 0),
            settings: None,
        }
    }

    /// Parses the given help text, ignoring any irrelevant text as long as the `'usage:'` and `options:`
    /// sections appear after each other, and then generates an appropriate argument parser.
    // pub const fn parse_help(mut self) -> ArgTree<'d> {
    //     // let Ok((mut tree, mut parser)) = self.parse_usage(env!("CARGO_PKG_NAME")) else {
    //     //     todo!()
    //     // };
    //     let Ok(tree) = self.parse_options() else {
    //         todo!()
    //     };
    //     tree
    // }

    /// Parses the usage part of the help text.
    const fn parse_usage(mut self, program_name: &'d str) -> Result<'d, (ArgTree<'d>, Self)> {
        // ensure usage is usage is correctly formatted:
        // * each line starts with program's name
        // extract the information about elements:
        // * optional
        // * required
        // * mutually exclusive
        // * repeatable
        // * is `--` or `-` present?

        let Some((_, parser)) = self.parser.split_once_delimiter("usage:", true) else {
            return Err(Error::MissingUsage);
        };

        // Read the rest of the line
        let Some((line, parser)) = parser.readline() else {
            return Err(Error::MissingUsage);
        };

        // If the line does not contain any further ASCII the following line is a usage line
        let line = parser::trim(line);
        if line.len() == 0 {
            while let Some((line, _parser)) = parser.readline() {
                let line = Parser::new(line, 0);
                if !line.starts_with_num(" ", 2) {
                    return Err(Error::TooFewSpaces);
                }
                let Some(line) = line.skip(2) else {
                    // Something has gone horribly wrong if this fails.
                    return Err(Error::InvalidParserState);
                };

                let Some(line) = line.read_until(" ", false) else {
                    return Err(Error::InvalidParserState);
                };
                // if !line.starts_with(program_name) {
                //     return Err(Error::MissingProgramName);
                // }
                // let Some(line) = line.skip(program_name.len()) else {
                //     return Err(Error::EarlyEnd);
                // };
            }
        }

        todo!()

        // // After finding 'usage:' we continue until an empty line is encountered
        // // then we return the current tree
        //
        // let mut tree = ArgTree::new();
        //
    }

    /// Parses the options part of the help text.
    const fn parse_options(mut self) -> Result<'d, ArgTree<'d>> {
        // ensure options are correctly formatted:
        // * no line starts with anything but space or `-`
        // extract info:
        // * synonyms for options
        // * whether an option takes arguments or not
        // * default values for options
        //
        // to ensure consistency we need to keep track of:
        // * how many spaces are used for the first option
        // * whether the first argument is preceeded by an equals sign or not
        // * whether the first argument is surrounded by angular brackets or is in upper-case
        //
        // * each option line starts with at least 2 spaces (to ensure some form of consistency)
        // * followed by either - | -- to indicate short or long option (order matters, short
        // before long)
        // * then some valid utf8 followed by either --, ',' or a ' '
        // * description starts after 2 or more spaces from the last option
        // * arguments to options are either all upper-case or surrounded by angular brackets '<>'
        // -oFILE --output=FILE       # without comma, with "=" sign
        // -i<file>, --input <file>   # with comma, without "=" sign

        let mut leading_space_count = 0;
        let mut leading_dash_count = 0;

        while let Some((line, parser)) = self.parser.readline() {
            let line = Parser::new(line, 0);
            if !line.starts_with_num(" ", 2) {
                continue;
            }
            let Some(line) = line.skip(2) else {
                // Something has gone horribly wrong if this fails.
                return Err(Error::InvalidParserState);
            };

            while !line.starts_with_num(" ", 2) {
                if !line.starts_with("-") {
                    continue;
                }
                // Option name
                let Some((name, line)) = line.read_until(" ", false) else {
                    // Could either be a '-' or '--' without any description
                    // in that case we should flag a warning about missing description
                    return Err(Error::InvalidParserState);
                };

                // Description delimiter encountered due to 2 spaces in a row
                if line.starts_with(" ") {
                    break;
                }

                if line.starts_with("<") {
                    // Make sure no whitespace
                    line.read_until(">", false);
                }
                // Argument(s)
            }

            let Some((description, _)) = parser.readline() else {
                continue;
            };
        }

        todo!()
    }

    /// Parses an option, whether it is a shortopt or a longopt.
    fn parse_option(parser: Parser<'d>) -> Result<((&'d str, Option<&'d str>), Parser<'d>)> {
        // Longopt
        if parser.starts_with("--") {
            let Some((name, parser)) = parser.read_until_either(&[" ", "="], false) else {
                return Err(Error::InvalidParserState);
            };

            // TODO: '--' exception
            let Some(parser) = parser.skip(1) else {
                return Err(Error::InvalidParserState);
            };

            let (param, parser) = match Self::parse_parameter(parser) {
                Ok((param, parser)) => (param, parser),
                Err(_) => (None, parser),
            };

            Ok(((name, param), parser))
        } else {
            if !parser.starts_with("-") {
                return Err(Error::NotAnOption);
            }

            // TODO: '-' exception
            // shortopt names are just 2 characters including '-'
            let Some((name, parser)) = parser.read(2) else {
                return Err(Error::InvalidParserState);
            };

            // shortopt does not need a separator but it is allowed
            let parser = if parser.starts_with(" ") || parser.starts_with("=") {
                match parser.skip(1) {
                    Some(p) => p,
                    None => return Err(Error::InvalidParserState),
                }
            } else {
                parser
            };

            let (param, parser) = match Self::parse_parameter(parser) {
                Ok((param, parser)) => (param, parser),
                Err(_) => (None, parser),
            };

            Ok(((name, param), parser))
        }
    }

    /// Parses a parameter.
    const fn parse_parameter(parser: Parser<'d>) -> Result<(Option<&'d str>, Parser<'d>)> {
        match parser.read_while_alphanumeric() {
            Some((param, parser)) => Ok((Some(param), parser)),
            None => Err(Error::MalformedParameter),
        }
    }

    /// Parses the description part of an option, trims it, then returns the description text
    /// and optionally returns the default value(s) for specified parameter(s), if they exist.
    ///
    /// This does not return the line parser since it assumes that the description is the end
    /// of the line.
    const fn parse_description(&self, parser: Parser<'d>) -> Result<(&'d str, Option<&'d str>)> {
        // Count and skip number of preceeding spaces
        // 1 or less: Error::TooFewSpaces
        // 2 or more: description
        let Some((spaces, parser)) = parser.read_while_whitespace() else {
            return Err(Error::InvalidParserState);
        };

        if let Some(settings) = &self.settings {
            if settings.leading_num_spaces != spaces.len() {
                return Err(Error::InvalidNumberOfSpaces);
            }
        } else {
            if spaces.len() != 2 {
                return Err(Error::InvalidNumberOfSpaces);
            }
        }

        let Some((desc, parser)) = parser.split_once_delimiter("[default: ", true) else {
            match parser.readline() {
                // No default value, just return the the rest of the line as the description
                // TODO: Should probably trim it
                Some((desc, _)) => return Ok((parser::trim_end(desc), None)),
                None => return Err(Error::NotAnOption),
            }
        };

        let Some((parameter_value, parser)) = parser.read_while_alphanumeric() else {
            return Err(Error::MalformedParameter);
        };

        if !parser.starts_with("]") {
            return Err(Error::MismatchingBrackets);
        }

        Ok((parser::trim(desc), Some(parameter_value)))
    }
}

/// States of Options: parsing.
#[derive(PartialEq, PartialOrd, Eq, Ord)]
pub enum OptionsState {
    Start,
    Option,
    Argument,
    Description,
}

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

    const USAGE: &'static str = r#"
        Usage:
            example_program --test=<I-Am-Argument>

        Options:
            -t, --test=<ARGUMENTO>  I am a cute description
    "#;

    #[test]
    fn parse_option() {
        let parser = Parser::new("-t ABC --test=ABC  I am a cute description", 0);
        let ((name, param), mut parser) = Docopt::parse_option(parser).unwrap();
        assert_eq!("-t", name);
        assert_eq!(Some("ABC"), param);
        parser = parser.skip(1).unwrap();
        let ((name, param), _) = Docopt::parse_option(parser).unwrap();
        assert_eq!("--test", name);
        assert_eq!(Some("ABC"), param);
    }

    // #[test]
    // fn parse_argument() {
    //     let parser = Parser::new("-t=<arg>, --test=ARG  I am a cute description", 0);
    //     let docopt = Docopt {
    //         parser: parser.clone(),
    //         settings: None,
    //     };
    //     let (opt, parser) = docopt.parse_option(parser).unwrap();
    //     dbg!(parser);
    //     assert_eq!("-t", opt);
    //     let (arg, parser) = docopt.parse_argument(true, parser).unwrap();
    //     dbg!(parser);
    //     assert_eq!(Some("<arg>"), arg);
    //     let (opt, parser) = docopt.parse_option(parser).unwrap();
    //     dbg!(parser);
    //     assert_eq!("--test", opt);
    //     let (arg, _) = docopt.parse_argument(false, parser).unwrap();
    //     dbg!(parser);
    //     assert_eq!(Some("ARG"), arg);
    // }

    #[test]
    fn parse_description() {
        let parser = Parser::new("  I am a cute description [default: hello]", 0);
        let docopt = Docopt {
            parser: parser.clone(),
            settings: None,
        };
        let (desc, value) = docopt.parse_description(parser).unwrap();
        assert_eq!("I am a cute description", desc);
        assert_eq!(Some("hello"), value);
    }
}