use std::path::{Path, PathBuf};
use std::process::ExitCode;
use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
use serde_json::Value;
use crate::{THEMES, compile_theme, find_theme, validate_value};
#[derive(Debug, Parser)]
#[command(name = "ferrocv", version, about, long_about = None)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Debug, Subcommand)]
enum Commands {
Validate {
path: Option<PathBuf>,
},
Render {
path: Option<PathBuf>,
#[arg(long)]
theme: String,
#[arg(long, default_value = "pdf")]
format: Format,
#[arg(short = 'o', long)]
output: Option<PathBuf>,
},
}
#[derive(Debug, Clone, Copy, clap::ValueEnum)]
enum Format {
Pdf,
}
pub fn run() -> Result<ExitCode> {
let cli = Cli::parse();
match cli.command {
Commands::Validate { path } => run_validate(path.as_deref()),
Commands::Render {
path,
theme,
format,
output,
} => run_render(path.as_deref(), &theme, format, output.as_deref()),
}
}
fn run_validate(path: Option<&Path>) -> Result<ExitCode> {
let input = read_input(path)?;
let value: Value = match serde_json::from_str(&input) {
Ok(v) => v,
Err(err) => {
eprintln!("error: {err}");
return Ok(ExitCode::from(2));
}
};
match validate_value(&value) {
Ok(()) => Ok(ExitCode::SUCCESS),
Err(errors) => {
for err in errors {
eprintln!("{err}");
}
Ok(ExitCode::from(1))
}
}
}
fn run_render(
path: Option<&Path>,
theme_name: &str,
format: Format,
output: Option<&Path>,
) -> Result<ExitCode> {
let input = read_input(path)?;
let value: Value = match serde_json::from_str(&input) {
Ok(v) => v,
Err(err) => {
eprintln!("error: {err}");
return Ok(ExitCode::from(2));
}
};
if let Err(errors) = validate_value(&value) {
for err in errors {
eprintln!("{err}");
}
return Ok(ExitCode::from(1));
}
let theme = match find_theme(theme_name) {
Some(t) => t,
None => {
eprintln!("error: unknown theme `{theme_name}`");
let names: Vec<&'static str> = THEMES.iter().map(|t| t.name).collect();
eprintln!("available themes: {}", names.join(", "));
return Ok(ExitCode::from(2));
}
};
let bytes = match format {
Format::Pdf => match compile_theme(theme, &value) {
Ok(bytes) => bytes,
Err(err) => {
eprintln!("{err}");
return Ok(ExitCode::from(2));
}
},
};
let out_path: PathBuf = output
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from("dist/resume.pdf"));
if let Some(parent) = out_path.parent()
&& !parent.as_os_str().is_empty()
&& let Err(err) = std::fs::create_dir_all(parent)
{
eprintln!(
"error: failed to create output directory {}: {err}",
parent.display()
);
return Ok(ExitCode::from(2));
}
if let Err(err) = std::fs::write(&out_path, &bytes) {
eprintln!(
"error: failed to write output file {}: {err}",
out_path.display()
);
return Ok(ExitCode::from(2));
}
Ok(ExitCode::SUCCESS)
}
fn read_input(path: Option<&Path>) -> Result<String> {
match path {
Some(p) => {
std::fs::read_to_string(p).with_context(|| format!("failed to read {}", p.display()))
}
None => std::io::read_to_string(std::io::stdin()).context("failed to read JSON from stdin"),
}
}