mrml-cli 1.3.2

CLI for the Rust implementation of MJML renderer
use clap::{crate_authors, crate_description, crate_name, crate_version, Clap};
use mrml::mjml::MJML;
use mrml::prelude::print::Print;
use mrml::prelude::render::Options as RenderOptions;
use std::fs::File;
use std::io::prelude::*;

#[derive(Clap)]
#[clap(name = crate_name!(), about = crate_description!(), version = crate_version!(), author = crate_authors!())]
struct Options {
    #[clap(subcommand)]
    pub subcmd: SubCommand,
    #[clap(about = "Path to your mjml file", index = 1)]
    pub input: Option<String>,
}

impl Options {
    fn read_file(&self, filename: &str) -> String {
        log::debug!("reading from file {}", filename);
        let mut file = File::open(filename).expect("couldn't find file");
        let mut content = String::new();
        file.read_to_string(&mut content)
            .expect("couldn't read file");
        content
    }

    fn read_stdin(&self) -> String {
        log::info!("waiting for input...");
        let mut buffer = String::new();
        std::io::stdin()
            .read_to_string(&mut buffer)
            .expect("couldn't read input");
        buffer
    }

    fn parse_json(&self, input: &str) -> Result<MJML, String> {
        log::debug!("parsing json input");
        serde_json::from_str::<MJML>(&input)
            .map_err(|err| format!("unable to parse json: {:?}", err))
    }

    fn parse_mjml(&self, input: &str) -> Result<MJML, String> {
        log::debug!("parsing mjml input");
        MJML::parse(input).map_err(|err| format!("unable to parse mjml: {:?}", err))
    }

    fn parse_input(&self, input: &str) -> MJML {
        if let Some(ref filename) = self.input {
            if filename.ends_with(".json") {
                self.parse_json(input).unwrap()
            } else if filename.ends_with(".mjml") {
                self.parse_mjml(input).unwrap()
            } else {
                panic!("unknown file type");
            }
        } else {
            self.parse_mjml(input)
                .or_else(|_| self.parse_json(input))
                .expect("unable to parse input")
        }
    }

    fn read_input(&self) -> String {
        if let Some(ref filename) = self.input {
            self.read_file(&filename)
        } else {
            self.read_stdin()
        }
    }

    pub fn execute(self) {
        let root = self.read_input();
        let root = self.parse_input(&root);
        self.subcmd.execute(&root);
    }
}

#[derive(Clap)]
enum SubCommand {
    #[clap(about = "Format template to JSON")]
    FormatJSON(Format),
    #[clap(about = "Format template to MJML")]
    FormatMJML(Format),
    #[clap(about = "Render template to HTML")]
    Render(Render),
    #[clap(about = "Read input file and validate its structure")]
    Validate,
}

impl SubCommand {
    pub fn execute(self, root: &MJML) {
        match self {
            Self::FormatJSON(opts) => {
                log::debug!("format to json");
                let output = if opts.pretty {
                    serde_json::to_string_pretty(root).expect("couldn't format to JSON")
                } else {
                    serde_json::to_string(root).expect("couldn't format to JSON")
                };
                println!("{}", output);
            }
            Self::FormatMJML(opts) => {
                log::debug!("format to mjml");
                let output = if opts.pretty {
                    root.pretty_print()
                } else {
                    root.dense_print()
                };
                println!("{}", output);
            }
            Self::Render(render) => {
                log::debug!("render");
                let render_opts = RenderOptions::from(render);
                let output = root.render(&render_opts).expect("couldn't render template");
                println!("{}", output);
            }
            Self::Validate => log::debug!("validate"),
        };
    }
}

#[derive(Clap)]
struct Format {
    #[clap(long, about = "Pretty print")]
    pub pretty: bool,
}

#[derive(Clap)]
struct Render {
    #[clap(short, long, about = "Remove comments from html output")]
    pub disable_comments: bool,
    #[clap(short, long, about = "Base url for social icons")]
    pub social_icon_origin: Option<String>,
}

impl From<Render> for RenderOptions {
    fn from(value: Render) -> Self {
        Self {
            disable_comments: value.disable_comments,
            social_icon_origin: value.social_icon_origin,
        }
    }
}

fn main() {
    env_logger::init();
    Options::parse().execute();
}

#[cfg(test)]
mod tests {
    use super::Options;
    use clap::Clap;

    fn execute(args: Vec<&str>) {
        Options::parse_from(args).execute();
    }

    fn execute_stdin(args: Vec<&str>, input: &str) {
        let opts = Options::parse_from(args);
        let root = opts.parse_input(input);
        opts.subcmd.execute(&root);
    }

    #[test]
    #[should_panic]
    fn missing_file() {
        execute(vec!["mrml-cli", "./cant/be/found.mjml", "validate"]);
    }

    #[test]
    #[should_panic]
    fn unknown_extension() {
        execute(vec!["mrml-cli", "./cant/be/found.txt", "validate"]);
    }

    #[test]
    #[should_panic]
    fn unknown_extension_stdin() {
        execute_stdin(vec!["mrml-cli", "validate"], "###");
    }

    #[test]
    fn format_json_amario_stdio() {
        let input = include_str!("../../../resources/template/amario.mjml");
        execute_stdin(vec!["mrml-cli", "format-json"], input);
    }

    #[test]
    fn format_json_amario() {
        execute(vec![
            "mrml-cli",
            "../../resources/template/amario.mjml",
            "format-json",
        ]);
    }

    #[test]
    fn format_json_pretty_amario() {
        execute(vec![
            "mrml-cli",
            "../../resources/template/amario.mjml",
            "format-json",
            "--pretty",
        ]);
    }

    #[test]
    fn format_mjml_amario() {
        execute(vec![
            "mrml-cli",
            "../../resources/template/amario.json",
            "format-mjml",
        ]);
    }

    #[test]
    fn format_mjml_pretty_amario() {
        execute(vec![
            "mrml-cli",
            "../../resources/template/amario.json",
            "format-mjml",
            "--pretty",
        ]);
    }

    #[test]
    fn render_amario() {
        execute(vec![
            "mrml-cli",
            "../../resources/template/amario.mjml",
            "render",
        ]);
    }

    #[test]
    fn validate_amario_json() {
        execute(vec![
            "mrml-cli",
            "../../resources/template/amario.json",
            "validate",
        ]);
    }

    #[test]
    fn validate_amario_mjml() {
        execute(vec![
            "mrml-cli",
            "../../resources/template/amario.mjml",
            "validate",
        ]);
    }
}