treease-cli 1.1.0

Command-line interface for querying, transforming, and previewing structured documents with Treease
Documentation
use std::fs;
use std::io::{self, Write};

use treease_core::core::{
    Context, CoreError, Encoder, IoPrinterWriter, NodeId, Printer, Reader, SystemError,
    VecPrinterWriter,
};
use treease_core::evaluator::StreamEvaluator;
use treease_core::formats::{
    CsvEncoder, Encode, FormatPreferences, JavascriptEncoder, JsonEncoder, PythonEncoder,
    TomlEncoder, YamlEncoder,
};

use crate::args::{CliError, InputPayload, ParsedArgs, StreamingInput, StreamingInputSource};

struct CliPrinterEncoder {
    inner: Box<dyn Encode>,
}

impl Encoder for CliPrinterEncoder {
    fn encode(
        &self,
        ctx: &mut Context,
        node: NodeId,
        writer: &mut dyn std::io::Write,
    ) -> Result<(), CoreError> {
        let store = ctx
            .current_print_store()
            .ok_or(CoreError::System(SystemError::Error))?;
        self.inner.encode(store, node, writer)
    }

    fn encode_evaluated_value(
        &self,
        value: &treease_core::evaluator::Value,
        writer: &mut dyn std::io::Write,
    ) -> Result<bool, CoreError> {
        self.inner.encode_evaluated_value(value, writer)
    }
}

#[derive(Debug, Clone)]
pub(super) struct ConfiguredFormats {
    pub(super) output: String,
}

pub(super) fn canonical_cli_format(value: &str) -> Result<String, CliError> {
    treease_core::core::find_cli_format_spec(value)
        .map(|spec| spec.name.to_string())
        .ok_or_else(|| CliError::UnsupportedFormat(value.to_string()))
}

pub(super) fn configured_formats(
    parsed: &ParsedArgs,
    first_input: Option<&InputPayload>,
) -> Result<ConfiguredFormats, CliError> {
    let input = first_input
        .map(|payload| super::cli_io::input::resolve_input_format(parsed, payload))
        .transpose()?;
    configured_formats_from_input_format(parsed, input.as_deref())
}

pub(super) fn execute_command(
    parsed: &ParsedArgs,
    inputs: &[InputPayload],
) -> Result<Vec<u8>, CliError> {
    let mut registry = treease_core::init()?;
    let result = (|| {
        let mut ctx = Context::empty(registry.handle());
        let configured = configured_formats(parsed, inputs.first())?;
        let prefs = configured_preferences(parsed, &configured.output)?;
        let encoder = CliPrinterEncoder {
            inner: make_value_encoder(&configured.output, prefs.clone())?,
        };
        let writer = VecPrinterWriter::new();
        let mut printer = Printer::new(encoder, writer);

        let mut evaluator = StreamEvaluator::new();
        if parsed.null_input {
            evaluator
                .evaluate_new(&mut ctx, &parsed.expression, &mut printer)
                .map_err(|err| CliError::Eval(format!("{err:?}")))?;
        } else {
            let parsed_expression = parse_expression(&parsed.expression)?;
            let mut total_processed_docs = 0_u32;
            for payload in inputs {
                let input_format = super::cli_io::input::resolve_input_format(parsed, payload)?;
                let mut cursor = io::Cursor::new(payload.bytes.as_slice());
                let mut reader = Reader::new(&mut cursor);
                total_processed_docs += evaluator
                    .evaluate_with_format(
                        &mut ctx,
                        payload.display_name(),
                        &mut reader,
                        parsed_expression.as_deref(),
                        &mut printer,
                        Some(input_format.as_str()),
                    )
                    .map_err(|err| CliError::Eval(format!("{err:?}")))?;
            }
            if total_processed_docs == 0 {
                evaluator
                    .evaluate_new(&mut ctx, &parsed.expression, &mut printer)
                    .map_err(|err| CliError::Eval(format!("{err:?}")))?;
            }
        }

        Ok(printer.into_writer().into_bytes())
    })();
    treease_core::deinit(&mut registry);
    result
}

pub(super) fn execute_command_to_writer<W: Write>(
    parsed: &ParsedArgs,
    inputs: &[StreamingInput],
    output: &mut W,
) -> Result<bool, CliError> {
    let mut registry = treease_core::init()?;
    let result = (|| {
        let mut ctx = Context::empty(registry.handle());
        let configured = configured_formats_from_input_format(
            parsed,
            inputs.first().map(|input| input.input_format.as_str()),
        )?;
        let prefs = configured_preferences(parsed, &configured.output)?;
        let encoder = CliPrinterEncoder {
            inner: make_value_encoder(&configured.output, prefs.clone())?,
        };
        let writer = IoPrinterWriter::new(output);
        let mut printer = Printer::new(encoder, writer);

        let mut evaluator = StreamEvaluator::new();
        if parsed.null_input {
            evaluator
                .evaluate_new(&mut ctx, &parsed.expression, &mut printer)
                .map_err(|err| CliError::Eval(format!("{err:?}")))?;
        } else {
            let parsed_expression = parse_expression(&parsed.expression)?;
            let mut total_processed_docs = 0_u32;
            for input in inputs {
                total_processed_docs += match &input.source {
                    StreamingInputSource::Stdin(bytes) => {
                        let mut cursor = io::Cursor::new(bytes.as_slice());
                        let mut reader = Reader::new(&mut cursor);
                        evaluator
                            .evaluate_with_format(
                                &mut ctx,
                                input.display_name(),
                                &mut reader,
                                parsed_expression.as_deref(),
                                &mut printer,
                                Some(input.input_format.as_str()),
                            )
                            .map_err(|err| CliError::Eval(format!("{err:?}")))?
                    }
                    StreamingInputSource::FilePath(path) => {
                        let mut file = fs::File::open(path)?;
                        let mut reader = Reader::new(&mut file);
                        evaluator
                            .evaluate_with_format(
                                &mut ctx,
                                input.display_name(),
                                &mut reader,
                                parsed_expression.as_deref(),
                                &mut printer,
                                Some(input.input_format.as_str()),
                            )
                            .map_err(|err| CliError::Eval(format!("{err:?}")))?
                    }
                };
            }
            if total_processed_docs == 0 {
                evaluator
                    .evaluate_new(&mut ctx, &parsed.expression, &mut printer)
                    .map_err(|err| CliError::Eval(format!("{err:?}")))?;
            }
        }

        Ok(printer.printed_anything())
    })();
    treease_core::deinit(&mut registry);
    result
}

pub(super) fn configured_preferences(
    parsed: &ParsedArgs,
    output_format: &str,
) -> Result<FormatPreferences, CliError> {
    let spec = treease_core::core::find_cli_format_spec(output_format)
        .ok_or_else(|| CliError::UnsupportedFormat(output_format.to_string()))?;
    let language = spec
        .format_language
        .ok_or_else(|| CliError::UnsupportedFormat(output_format.to_string()))?;
    let mut prefs = treease_core::formats::configured_language_preferences().effective(language);
    let pretty_print = parsed.pretty_print.unwrap_or(spec.default_pretty_print);
    prefs.indent = if pretty_print {
        parsed.indent.unwrap_or(prefs.indent)
    } else {
        0
    };
    prefs.unwrap_scalar = parsed.unwrap_scalar;
    if parsed.no_doc {
        prefs.print_doc_separators = false;
    }
    Ok(prefs)
}

pub(super) fn make_value_encoder(
    format_name: &str,
    prefs: FormatPreferences,
) -> Result<Box<dyn Encode>, CliError> {
    match format_name {
        "json" => Ok(Box::new(JsonEncoder::new(prefs))),
        "yaml" => Ok(Box::new(YamlEncoder::new(prefs))),
        "toml" => Ok(Box::new(TomlEncoder::new(prefs))),
        "csv" => Ok(Box::new(CsvEncoder::new(prefs))),
        "python" => Ok(Box::new(PythonEncoder::new(prefs))),
        "javascript" => Ok(Box::new(JavascriptEncoder::new(prefs))),
        _ => Err(CliError::UnsupportedFormat(format_name.to_string())),
    }
}

pub(super) fn parse_expression(
    expression: &str,
) -> Result<Option<Box<treease_core::core::ExpressionNode>>, CliError> {
    treease_core::parser::parse_expression(expression)
        .map_err(|error| CliError::Eval(format!("{error:?}")))
}

fn configured_formats_from_input_format(
    parsed: &ParsedArgs,
    first_input_format: Option<&str>,
) -> Result<ConfiguredFormats, CliError> {
    let input = first_input_format.unwrap_or("json").to_string();
    let output = match parsed.output_format.as_deref() {
        Some(value) => canonical_cli_format(value)?,
        None if first_input_format.is_some() => input.clone(),
        None => "yaml".to_string(),
    };
    Ok(ConfiguredFormats { output })
}

#[cfg_attr(not(test), allow(dead_code))]
pub(super) fn compute_exit_status(enabled: bool, output: &[u8]) -> u8 {
    let trimmed = std::str::from_utf8(output)
        .unwrap_or("")
        .trim_matches(|ch| matches!(ch, ' ' | '\t' | '\r' | '\n' | '\0'));
    let printed = !trimmed.is_empty() && trimmed != "null" && trimmed != "false";
    compute_exit_status_from_printed(enabled, printed)
}

pub(super) fn compute_exit_status_from_printed(enabled: bool, printed: bool) -> u8 {
    if enabled && !printed { 1 } else { 0 }
}