tomq 0.1.2

jq, but from TOML
mod bin_finder;
mod dociter;
mod error;
mod jq_pipe;
mod opts;
mod output;
mod pager;
mod reader;
mod short_display;
mod toml_pp;
mod tomly;

use crate::dociter::DocumentIterator;
use crate::jq_pipe::{toml_to_jq, toml_to_json_to_stdout};
use crate::output::output_to_stdout;
use crate::reader::MultiFileRead;
use crate::short_display::ShortDisplayExt;
use crate::tomly::{create_toml_doc_from_json, parse_toml_document, print_toml};
use atty::Stream;
use clap::Parser;
use opts::TomlOpts;
use std::error::Error;
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::path::PathBuf;
use std::process::ExitCode;

const LICENSE_FOOTER: &'static str = r#"
                      Licensed under MIT.
        Copyright (c) 2022-present, the authors of tomq.
"#;

/// Transcodes toml to json and feeds to jq (supports TOML v1.0.0 spec) (use `--help` for long help).
///
/// # Dependencies
///
/// tomq depends on having jq installed on the machine and available on the `PATH`,
/// if jq is not found, it will just print the JSON output instead of piping it to
/// jq, and the filter argument will have no effects, so it's still useful without
/// jq to transcode from TOML to JSON.
///
/// If you have jq installed but just want TOML to JSON conversion, you can omit the jq filter:
///
/// >    cat Cargo.toml | tomq
///
/// And the output will be the same with or without jq installed, however, this doesn't work
/// when reading directly from a file, in this case you can use an empty filter or the `--input-file` flag:
///
/// >    tomq '' Cargo.toml
///
/// >    tomq --input-file Cargo.toml
///
///
/// # Examples
///
/// Example:
///
/// >    cat Cargo.toml | tomq '.dependencies'
///
/// OR
///
/// >    tomq '.dependencies' Cargo.toml
///
/// The output will be a JSON map of dependencies, but you can use the re-encode option
/// to get a TOML out of it:
///
/// >    tomq -R '.dependencies' Cargo.toml
///
/// If you are seeing inline tables instead of a pretty output, is because you don't have
/// a PTY attached (or it is being hidden some way or another), in this case, you can use
/// the pretty option (`-B`, for beautiful as you):
///
/// >    tomq -B -R '.dependencies' Cargo.toml
///
/// If you try to pipe it to `cat`, `bat` or `less`, you will probably get the non-pretty (compact)
/// output, so be aware to always use `-B` whenever you need a more readable TOML output.
///
/// And talking about `bat`, if you got it installed and available in your PATH, you can use
/// `--bat` to get a pretty and highlighted output (whenever `tomq` is your final command in the
/// pipeline, otherwise `bat` will notice the absence of a pty and will just act like the regular
/// `cat`).
///
/// # Multi-document support
///
/// This tool also features a custom non-compliant TOML format for multi-documents,
/// which is used by default whenever a multi-document is re-encoded back to TOML,
/// unless a root key is provided.
#[derive(Parser)]
#[clap(
    author,
    version,
    about,
    after_help = LICENSE_FOOTER,
    after_long_help = LICENSE_FOOTER
)]
struct Opts {
    #[clap(flatten)]
    opts: TomlOpts,
    /// File to read TOML (or JSON) from (reads from stdin if absent and stdin is not a tty).
    #[clap(index = 2)]
    file: Option<PathBuf>,
    /// File to read TOML (or JSON) from, cannot be provided along to the positional `file` argument.
    #[clap(long = "input-file", conflicts_with = "file")]
    in_file: Option<PathBuf>,
    /// File to read TOML (or JSON) from, cannot be provided along to the positional `file`
    /// or the `--input-file` flag argument.
    ///
    /// It will always use multi-document mode and ignore `single-doc`, even if a single
    /// file is provided. For the separator, it uses the one provided with `-S` flag, or the default
    /// one.
    ///
    /// Very useful to be used with `xargs`, example:
    ///
    /// ```
    /// fd -0 Cargo.toml | xargs -r0 -l128 tomq '.package.name' --input-files
    /// ```
    ///
    /// Be aware that, currently, this opens all files at once, which means that you will like to
    /// use *xargs* `-l` flag to set a limit in the amount of files that are passed to `tomq`,
    /// otherwise you could hit the default kernel limit.
    ///
    /// **This option always implies multi-document mode** and there is no way to disable this,
    /// the entire purpose of `--input-files` is to improve the performance when dealing with
    /// hundreds of Toml files by launching and opening a single pipe to `jq` instead of one for every file.
    #[clap(long = "input-files", multiple = true, conflicts_with_all(&["in-file", "file"]))]
    in_files: Vec<PathBuf>,
}

fn main() -> Result<ExitCode, Box<dyn Error>> {
    let opts: Opts = Opts::parse();

    let sops = TomlOpts {
        pretty_print: opts.opts.pretty_print || atty::is(Stream::Stdout) || opts.opts.bat,
        ..opts.opts.clone()
    };

    let status = if !opts.in_files.is_empty() {
        let separator = &format!("\n\n{}\n\n", opts.opts.multi_doc_separator);
        let sops = TomlOpts {
            single_doc: false,
            ..sops.clone()
        };

        let mut reader = MultiFileRead::new(opts.in_files.clone(), separator.clone())?;
        let names = opts
            .in_files
            .iter()
            .map(|f| f.to_string_lossy().to_string())
            .collect::<Vec<String>>();

        let names = names.short_display(5);
        read_and_output(BufReader::new(&mut reader), sops)
            .map_err(|e| format!("Failed while handling files `{names:?}`: {}.", e))?
    } else {
        match opts.file.or(opts.in_file) {
            Some(file) => read_and_output(BufReader::new(File::open(&file)?), sops)
                .map_err(|e| format!("Failed while handling files `{}`: {}", file.display(), e))?,
            None => {
                if !atty::is(Stream::Stdin) {
                    read_and_output(std::io::stdin().lock(), sops)?
                } else {
                    use clap::CommandFactory;
                    Opts::command().print_long_help()?;
                    -1
                }
            }
        }
    };

    Ok(ExitCode::from(status as u8))
}

/// Reads the input `R` and handles all the options, such as JSON to TOML and TOML to JSON conversion
/// (with and without filter), piping to `bat`, pretty print, etc.
///
/// All the logic is there.
fn read_and_output<R: BufRead>(reader: R, opts: TomlOpts) -> Result<i32, Box<dyn Error>> {
    if opts.toml_it {
        if opts.jq_filter.as_ref().filter(|q| !q.is_empty()).is_none() {
            let toml = create_toml_doc_from_json(
                reader,
                opts.skip_invalid,
                opts.null_to_empty_doc,
                opts.fallback_key,
            );
            print_toml(
                toml,
                opts.pretty_print,
                &opts.root_key,
                &opts.multi_doc_separator,
                opts.bat,
            )?;
            Ok(0)
        } else {
            let toml = create_toml_doc_from_json(
                reader,
                opts.skip_invalid,
                opts.null_to_empty_doc,
                opts.fallback_key.clone(),
            );
            Ok(toml_to_jq(toml, opts)?)
        }
    } else {
        let toml = parse_toml_document(reader, &opts);
        if opts.jq_filter.as_ref().filter(|q| !q.is_empty()).is_none() {
            Ok(toml_to_json_to_stdout(toml, opts)?)
        } else {
            Ok(toml_to_jq(toml, opts)?)
        }
    }
}

#[cfg(test)]
mod tests {}