wmap 1.0.0

Command line tool to generate wardley map images from wmap files.
use std::fmt;
use std::fs::{read_to_string, write};
use std::io::{self, Error as IoError, IsTerminal, Read, Result as IoResult, Write};
use std::path::PathBuf;
use std::str::FromStr;
use wmap_renderer::{Configuration, StageType, render_to_png, render_to_svg};

use thiserror::Error;

#[derive(Debug)]
enum ImageType {
    Png,
    Svg,
}

impl fmt::Display for ImageType {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            ImageType::Png => write!(f, "png"),
            ImageType::Svg => write!(f, "svg"),
        }
    }
}

#[derive(Error, Debug)]
pub enum WmapError {
    #[error("unknown image type.")]
    UnknownImageType,
    #[error("unknown stage type.")]
    UnknownStageType,
}

impl FromStr for ImageType {
    type Err = WmapError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.to_lowercase().as_str() {
            "png" => Ok(ImageType::Png),
            "svg" => Ok(ImageType::Svg),
            _ => Err(Self::Err::UnknownImageType),
        }
    }
}

fn stage_from_str(s: &str) -> Option<StageType> {
    match s.to_lowercase().as_str() {
        "activities" => Some(StageType::Activities),
        "behavior" => Some(StageType::Behavior),
        "certainty" => Some(StageType::Certainty),
        "comparison" => Some(StageType::Comparison),
        "cynefin" => Some(StageType::Cynefin),
        "data" => Some(StageType::Data),
        "decision_drivers" => Some(StageType::DecisionDrivers),
        "efficiency" => Some(StageType::Efficiency),
        "failure" => Some(StageType::Failure),
        "focus_of_value" => Some(StageType::FocusOfValue),
        "knowledge" => Some(StageType::Knowledge),
        "knowledge_management" => Some(StageType::KnowledgeManagement),
        "market" => Some(StageType::Market),
        "market_action" => Some(StageType::MarketAction),
        "market_perception" => Some(StageType::MarketPerception),
        "perception_in_industry" => Some(StageType::PerceptionInIndustry),
        "practice" => Some(StageType::Practice),
        "publication_types" => Some(StageType::PublicationTypes),
        "ubiquity" => Some(StageType::Ubiquity),
        "understanding" => Some(StageType::Understanding),
        "user_perception" => Some(StageType::UserPerception),
        "evolution_stage" => Some(StageType::EvolutionStage),
        _ => None,
    }
}

struct Arguments {
    input: Option<PathBuf>,
    output: PathBuf,
    write_to_stdout: bool,
    stage: StageType,
    image_type: ImageType,
}

fn print_help() {
    println!(
        "Usage: wmap [-t|--type IMAGE_TYPE] [-s|--stage STAGE] [-o|--output OUTPUT_FILE] INPUT_FILE"
    );
}

fn parse_arguments() -> Result<Arguments, lexopt::Error> {
    use lexopt::prelude::*;

    let mut input = None;
    let mut output: String = String::new();
    let mut write_to_stdout: bool = false;
    let mut stage = StageType::Activities;
    let mut image_type = ImageType::Png;

    let mut argument_parser = lexopt::Parser::from_env();
    while let Some(argument) = argument_parser.next()? {
        match argument {
            Short('t') | Long("type") => {
                image_type = argument_parser.value()?.parse()?;
            }
            Short('s') | Long("stage") => {
                let stage_string: String = argument_parser.value()?.parse()?;
                stage = stage_from_str(&stage_string).ok_or("Invalid stage name")?;
            }
            Short('o') | Long("output") => {
                output = argument_parser.value()?.parse()?;
            }
            Value(value) if input.is_none() => {
                input = Some(value.string()?);
            }
            Short('h') | Long("help") => {
                print_help();
                std::process::exit(0);
            }
            Short('v') | Long("version") => {
                let version = env!("CARGO_PKG_VERSION");
                println!("{version}");
                std::process::exit(0);
            }
            _ => return Err(argument.unexpected()),
        }
    }
    let input = input.map(PathBuf::from);
    let output = if output.is_empty() {
        if io::stdout().is_terminal() {
            match &input {
                Some(path) => path.with_extension(image_type.to_string()),
                None => PathBuf::from(format!("./map.{image_type}")),
            }
        } else {
            write_to_stdout = true;
            PathBuf::new()
        }
    } else {
        PathBuf::from(output)
    };

    Ok(Arguments {
        input,
        output,
        write_to_stdout,
        stage,
        image_type,
    })
}

fn run() -> IoResult<()> {
    let arguments = parse_arguments().map_err(|_| IoError::other("Unable to parse arguments"))?;

    let source = if let Some(input_path) = arguments.input {
        input_path.try_exists()?;
        read_to_string(input_path)?
    } else {
        let mut buffer = String::new();
        io::stdin().read_to_string(&mut buffer)?;
        buffer
    };
    let map = wmap_parser::parse(&source);
    let configuration = Configuration::default();

    let image_data = match arguments.image_type {
        ImageType::Png => render_to_png(&map, arguments.stage, &configuration)
            .map_err(|_| IoError::other("Unable to render PNG"))?,
        ImageType::Svg => render_to_svg(&map, arguments.stage, &configuration)
            .map_err(|_| IoError::other("Unable to render SVG"))?
            .into_bytes(),
    };
    if arguments.write_to_stdout {
        io::stdout().write_all(&image_data)?;
    } else {
        write(arguments.output, image_data)?;
    }
    Ok(())
}

fn main() -> IoResult<()> {
    let result = run();

    if cfg!(debug_assertions) {
        result
    } else {
        match result {
            Ok(()) => Ok(()),
            Err(e) => {
                eprintln!("Error: {e}");
                eprintln!();
                print_help();
                std::process::exit(1);
            }
        }
    }
}