univert 0.4.0

Universal file converter (library and CLI)
Documentation
pub mod cli;
pub mod conversion_map;
pub mod converters;
pub mod pipeline;

use std::path::{Path, PathBuf};

#[derive(Debug, thiserror::Error)]
pub enum Error {
  #[error("Input file {0} does not exist")]
  InputMissing(PathBuf),

  #[error("Output file {0} already exists")]
  OutputExists(PathBuf),

  #[error("Input file {0} has no extension")]
  InputNoExtension(PathBuf),

  #[error("Output file {0} has no extension")]
  OutputNoExtension(PathBuf),

  #[error(
    "Conversion from {from} to {to} is not yet supported.\nRequest it at https://github.com/ad-si/univert/issues"
  )]
  Unsupported { from: String, to: String },

  #[error(
    "Conversion from {from} to {to} requires `{tool}`, which was not found on PATH."
  )]
  MissingTool {
    from: String,
    to: String,
    tool: &'static str,
  },

  #[error("External tool `{tool}` failed: {message}")]
  ToolFailed { tool: &'static str, message: String },

  #[error(transparent)]
  Io(#[from] std::io::Error),

  #[error(transparent)]
  Csv(#[from] csv::Error),

  #[error("{0}")]
  Other(String),
}

impl Error {
  pub fn other(msg: impl Into<String>) -> Self {
    Error::Other(msg.into())
  }
}

pub type Result<T> = std::result::Result<T, Error>;

#[derive(Debug, Clone, Default)]
pub struct Options {
  pub force: bool,
  pub split: bool,
}

pub struct Job<'a> {
  pub input: &'a Path,
  pub output: &'a Path,
  pub options: &'a Options,
}

pub fn normalize_extension(ext: &str) -> String {
  let lower = ext.to_ascii_lowercase();
  match lower.as_str() {
    "jpg" => "jpeg".to_string(),
    "mid" => "midi".to_string(),
    _ => lower,
  }
}

pub fn extension_of(path: &Path) -> Option<String> {
  path
    .extension()
    .and_then(|e| e.to_str())
    .map(normalize_extension)
}

pub fn convert(input: &Path, output: &Path, options: &Options) -> Result<()> {
  if !input.exists() {
    return Err(Error::InputMissing(input.to_path_buf()));
  }

  if output.exists() && !options.force {
    return Err(Error::OutputExists(output.to_path_buf()));
  }

  let from = extension_of(input)
    .ok_or_else(|| Error::InputNoExtension(input.to_path_buf()))?;

  let to = extension_of(output)
    .ok_or_else(|| Error::OutputNoExtension(output.to_path_buf()))?;

  let entry = conversion_map::lookup(&from, &to).ok_or(Error::Unsupported {
    from: from.clone(),
    to: to.clone(),
  })?;

  let job = Job {
    input,
    output,
    options,
  };

  pipeline::run(&from, &to, entry, &job)
}