main-spex 0.21.0

AI-first development framework that turns specifications into deterministic, verifiable code
use std::fs;
use std::io::{self, BufWriter, Read, Write};
use std::path::{Path, PathBuf};
use std::process::ExitCode;

use clap::Args;
use eros::{Context, IntoAnyUnion};
use jaq_all::jaq_core::{Vars, unwrap_valr};
use jaq_all::load::{FileReports, FileReportsDisp};
use jaq_all::{compile_with, data, defs};
use jaq_json::Val;
use jaq_json::read;
use jaq_json::write::{self, Pp};

use crate::error::{Result, could_not_read};

#[cfg(test)]
#[path = "jq_test.rs"]
mod jq_test;

const PRETTY_INDENT: &str = "  ";

#[derive(Args)]
pub struct JqArgs {
    #[arg(help = "jq filter to run on the input, e.g. `.project_name`")]
    filter: String,
    #[arg(help = "JSON or YAML file to read; reads standard input when omitted")]
    file: Option<PathBuf>,
    #[arg(short, long, help = "Print string results without quotes")]
    raw_output: bool,
    #[arg(short, long, help = "Print each result on a single line")]
    compact_output: bool,
}

#[derive(Clone, Copy)]
struct Output {
    raw: bool,
    compact: bool,
}

pub fn run(args: JqArgs) -> Result<ExitCode> {
    let text = read_input(args.file.as_deref())?;
    let output = Output {
        raw: args.raw_output,
        compact: args.compact_output,
    };
    let mut stdout = BufWriter::new(io::stdout().lock());
    evaluate(&args.filter, &text, output, &mut stdout)?;
    stdout
        .flush()
        .context("could not write to standard output")?;
    Ok(ExitCode::SUCCESS)
}

fn read_input(file: Option<&Path>) -> Result<String> {
    match file {
        Some(path) => fs::read_to_string(path)
            .with_context(|| could_not_read(path))
            .any_union(),
        None => {
            let mut text = String::new();
            io::stdin()
                .read_to_string(&mut text)
                .context("could not read standard input")?;
            Ok(text)
        }
    }
}

fn evaluate(filter: &str, text: &str, output: Output, out: &mut dyn Write) -> Result<()> {
    let compiled = compile_with(filter, defs(), data::base_funs(), &[])
        .map_err(|reports| eros::error!("{}", describe_filter_errors(&reports)))?;
    let input = parse_input(text)?;
    let pp = printer(output);

    data::run(
        &data::Runner::default(),
        &compiled,
        Vars::default(),
        std::iter::once(Ok::<_, String>(input)),
        |message| eros::error!("{message}"),
        |result| {
            let value = unwrap_valr(result).map_err(|err| eros::error!("jq: {err}"))?;
            write_value(out, &pp, output, &value).context("could not write to standard output")?;
            Ok(())
        },
    )
}

fn parse_input(text: &str) -> Result<Val> {
    if let Ok(value) = read::parse_single(text.as_bytes()) {
        return Ok(value);
    }
    serde_yaml_ng::from_str(text)
        .context("could not parse the input as JSON or YAML")
        .any_union()
}

fn printer(output: Output) -> Pp {
    Pp {
        indent: (!output.compact).then(|| PRETTY_INDENT.to_string()),
        sep_space: !output.compact,
        ..Pp::default()
    }
}

fn write_value(out: &mut dyn Write, pp: &Pp, output: Output, value: &Val) -> io::Result<()> {
    match value {
        Val::TStr(text) if output.raw => out.write_all(&text[..])?,
        _ => write::write(out, pp, 0, value)?,
    }
    out.write_all(b"\n")
}

fn describe_filter_errors(reports: &[FileReports]) -> String {
    let rendered = reports
        .first()
        .map(|report| FileReportsDisp::new(report).to_string())
        .unwrap_or_default();
    let message = rendered
        .lines()
        .next()
        .unwrap_or_default()
        .trim_start_matches("Error: ");
    format!("invalid jq filter: {message}")
}