config-forge 0.1.1

A CLI tool for converting, inspecting, and validating configuration files.
Documentation
use std::path::PathBuf;

use anyhow::{Result, bail};
use clap::{Parser, Subcommand};
use config_forge::{Format, check_convert_path, convert_path, inspect_path};

#[derive(Debug, Parser)]
#[command(name = "config-forge")]
#[command(version, about = config_forge::describe())]
struct Cli {
    #[command(subcommand)]
    command: Option<Command>,
}

#[derive(Debug, Subcommand)]
enum Command {
    /// Convert between JSON, TOML, and YAML.
    Convert {
        /// Input configuration file.
        input: PathBuf,

        /// Output file. If omitted, converted content is printed to stdout.
        #[arg(short, long)]
        output: Option<PathBuf>,

        /// Input format. Defaults to detection from input extension.
        #[arg(long, value_parser = parse_format)]
        from: Option<Format>,

        /// Output format. Defaults to detection from output extension.
        #[arg(long, value_parser = parse_format)]
        to: Option<Format>,

        /// Validate that conversion would succeed without writing output.
        #[arg(long)]
        check: bool,

        /// Replace the output file if it already exists.
        #[arg(long)]
        overwrite: bool,
    },

    /// Print basic information about a configuration file.
    Inspect {
        /// Input configuration file.
        input: PathBuf,

        /// Input format. Defaults to detection from input extension.
        #[arg(long, value_parser = parse_format)]
        from: Option<Format>,
    },
}

fn main() -> Result<()> {
    let cli = Cli::parse();

    match cli.command {
        Some(Command::Convert {
            input,
            output,
            from,
            to,
            check,
            overwrite,
        }) => convert(input, output, from, to, check, overwrite),
        Some(Command::Inspect { input, from }) => inspect(input, from),
        None => {
            println!("{} {}", config_forge::NAME, config_forge::VERSION);
            println!("{}", config_forge::describe());
            Ok(())
        }
    }
}

fn convert(
    input: PathBuf,
    output: Option<PathBuf>,
    from: Option<Format>,
    to: Option<Format>,
    check: bool,
    overwrite: bool,
) -> Result<()> {
    if check {
        let output_format = check_convert_path(&input, from, to)?;
        println!("ok: conversion to {} is valid", output_format.name());
        return Ok(());
    }

    if output.is_none() && to.is_none() {
        bail!("--to is required when --output is not provided");
    }

    let rendered = convert_path(&input, output.as_ref(), from, to, overwrite)?;

    if output.is_none() {
        print!("{rendered}");
    }

    Ok(())
}

fn inspect(input: PathBuf, from: Option<Format>) -> Result<()> {
    let info = inspect_path(&input, from)?;

    println!("path: {}", input.display());
    println!("format: {}", info.format.name());
    println!("root: {}", info.root_kind);
    println!("size: {} bytes", info.size_bytes);

    Ok(())
}

fn parse_format(value: &str) -> Result<Format> {
    value.parse()
}