trinitry 0.2.2

A very simple programming language, used to map functions to commands
Documentation
/*
* Copyright (C) 2024 - 2024:
* The Trinitrix Project <bpeetz@b-peetz.de, antifallobst@systemausfall.org>
* SPDX-License-Identifier: LGPL-3.0-or-later
*
* This file is part of the Trinitry crate for Trinitrix.
*
* Trinitry is free software: you can redistribute it and/or modify
* it under the terms of the Lesser GNU General Public License as
* published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* and the Lesser GNU General Public License along with this program.
* If not, see <https://www.gnu.org/licenses/>.
*/

//! This crate is a parser for the 'Trinitry' (not 'Trinity') language, used to map all sort of
//! Functions to a memorable command.
//!
//! This parser is more of a validator, as Trinitry does not support any language features besides
//! the aforementioned commands and arguments. That includes some simple constructs like: '||' (OR)
//! or '&&' (AND). If you need these features, simple write them in the language, you've written your
//! Function in.
//!
//! # General specification
//! ## Command
//! Basically every command can be a series of alphanumeric ASCII values.
//!
//! Correctly spoken, the Language, containing all valid command names, is just the Kleene closure
//! over an Alphabet $\Sigma$, which contains all alphanumeric characters:
//! \\[ \Sigma_{cmd} = \\{x | 0 \leqslant x \leqslant 9\\} \cup \\{x | "\text{a}" \leqslant x \leqslant "\text{z}"\\} \cup \\{x | "\text{A}" \leqslant x \leqslant "\text{Z}"\\} \cup \\{"\text{\\_}", "\text{-}", "."\\} \\]
//!
//! ## Argument
//! Arguments constructed from the same alphabet as the commands, but can contain additional chars
#![doc = concat!("listed in the [trinitry.pest](https://docs.rs/crate/trinitry/", env!("CARGO_PKG_VERSION"), "/source/src/trinitry.pest) file.")]
//! \\[ \Sigma_{args} = \Sigma_{cmd} \cup{} \\{\\dots{}\\} \\]
//!
//! Besides the extra chars outlined above the arguments can also contain
//! spaces and quotes, if they are quoted. Quoted args are either double quoted, and can thus
//! contain single quotes, or single quoted, and can contain double quotes.
//! \\[
//! \Sigma_{args-double-quoted} = \Sigma_{args} \cup \\{"\texttt{ꞌ}", "\\ "\\} \\\\
//! \Sigma_{args-single-quoted} = \Sigma_{args} \cup \\{"\texttt{"}", "\\ "\\}
//! \\]
//!
//! # Examples
//! ## Command
//! A valid command would be something like that:
//! ```text
//! quit
//! ```
//! something like that would not be valid however, as Trinitry does not support these 'complex'
//! language features:
//! ```text
//! write && quit
//! ```
//! ## Arguments
//! A valid argumented command would be:
//! ```text
//! lua "function() print('Hi!') end"
//! ```
//! Whilst this would not be valid (it is actually valid, but results in something, you likely did not expect):
//! ```text
//! lua "function() print("Hi!") end"
//! ```
//! as the double quotes in the print statement actually unquote the argument, leaving you with
//! three arguments:
//! 1. `function() print(`
//! 1. `Hi!`
//! 1. `) end`
use std::{fmt::Display, str::FromStr};

use parsing::{Rule, TrinitryParser};
use pest::error::Error;

mod parsing;

pub struct Trinitry {
    command: String,
    arguments: Vec<String>,
}

impl Trinitry {
    pub fn new(input: &str) -> Result<Self, <Self as FromStr>::Err> {
        input.parse()
    }

    pub fn command(&self) -> &str {
        &self.command
    }

    pub fn arguments(&self) -> &[String] {
        &self.arguments
    }
}

impl FromStr for Trinitry {
    type Err = Error<Rule>;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let parsed = TrinitryParser::new(s)?;
        Ok(Self::from(parsed))
    }
}

impl From<TrinitryParser<'_>> for Trinitry {
    fn from(parsed: TrinitryParser) -> Self {
        let command = {
            let command: Vec<_> = parsed.0.clone().find_tagged("command").collect();

            // Ensure that we have only one command
            // This should be ensured by the grammar, thus the 'debug_assert'
            debug_assert_eq!(command.len(), 1);

            // PERFORMANCE(@soispha): Replace this with `mem::take` (when pairs implements Default)
            // <2023-11-01>
            command
                .first()
                .expect("This should contain exactly one element")
                .to_owned()
        };
        let arguments: Vec<_> = parsed.0.clone().find_tagged("argument").collect();

        Self {
            command: command.as_str().to_owned(),
            arguments: arguments
                .iter()
                .map(|arg| {
                    let mut arg = arg.as_str().trim();
                    arg = if let Some(new_arg) = arg.strip_prefix("\"") {
                        new_arg
                    } else {
                        arg
                    };
                    arg = if let Some(new_arg) = arg.strip_suffix("\"") {
                        new_arg
                    } else {
                        arg
                    };

                    arg = if let Some(new_arg) = arg.strip_prefix("'") {
                        new_arg
                    } else {
                        arg
                    };
                    arg = if let Some(new_arg) = arg.strip_suffix("'") {
                        new_arg
                    } else {
                        arg
                    };
                    arg.to_owned()
                })
                .collect(),
        }
    }
}

impl Display for Trinitry {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        if self.arguments.is_empty() {
            f.write_str(&self.command)
        } else {
            f.write_fmt(format_args!(
                "{} {}",
                &self.command,
                &self.arguments.join(" ")
            ))
        }
    }
}

mod tests;

#[cfg(test)]
mod test {
    use crate::Trinitry;

    #[test]
    fn parse_cmd() {
        let string = "quit";
        let p = Trinitry::new(string).unwrap_or_else(|e| {
            panic!("{}", e);
        });
        assert_eq!(&p.command, "quit");
        assert!(&p.arguments.is_empty());
    }

    #[test]
    fn parse_arg_clean() {
        let string = r##"lua print("Hi")"##;
        let p = Trinitry::new(string).unwrap_or_else(|e| {
            panic!("{}", e);
        });
        assert_eq!(&p.command, "lua");
        assert_eq!(&p.arguments[0], r#"print("Hi")"#);
    }

    #[test]
    fn parse_arg_quote() {
        let string = r##"write "some 'file' name""##;
        let p = Trinitry::new(string).unwrap_or_else(|e| {
            panic!("{}", e);
        });
        assert_eq!(&p.command, "write");
        assert_eq!(&p.arguments[0], "some 'file' name");
    }

    #[test]
    fn parse_arg_single_quote() {
        let string = r##"write 'some "file" name'"##;
        let p = Trinitry::new(string).unwrap_or_else(|e| {
            panic!("{}", e);
        });
        assert_eq!(&p.command, "write");
        assert_eq!(&p.arguments[0], "some \"file\" name");
    }

    #[test]
    fn parse_arg_multi() {
        let string = r##"write 'some "file" name' "other name" last"##;
        let p = Trinitry::new(string).unwrap_or_else(|e| {
            panic!("{}", e);
        });

        let expected_args = vec!["some \"file\" name", "other name", "last"]
            .iter()
            .map(|str| (*str).to_owned())
            .collect::<Vec<String>>();

        assert_eq!(&p.command, "write");
        assert_eq!(&p.arguments, &expected_args);
    }
}