use std::fs;
use std::io::{Read, Write};
use treease_core::core::CodecService;
use treease_core::evaluator::{
CanonicalFraming, CanonicalOutcome, CanonicalOutputFormat, CanonicalPayload, execute_canonical,
};
use treease_core::formats::FormatPreferences;
use crate::args::{CliError, InputPayload, ParsedArgs, StreamingInput, StreamingInputSource};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) struct ExecutionStatus {
printed: bool,
has_truthy_result: bool,
}
struct CanonicalSource<'a> {
input_format: &'a str,
display_name: &'a str,
source: &'a str,
}
impl ExecutionStatus {
#[cfg(test)]
pub(super) fn printed(self) -> bool {
self.printed
}
pub(super) fn exit_code(self, enabled: bool) -> u8 {
if enabled && !self.has_truthy_result {
1
} else {
0
}
}
}
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_output_format(
parsed: &ParsedArgs,
first_input: Option<&InputPayload>,
) -> Result<String, CliError> {
let input = first_input
.map(|payload| super::cli_io::input::resolve_input_format(parsed, payload))
.transpose()?;
configured_output_format_from_input_format(parsed, input.as_deref())
}
pub(super) fn execute_command(
parsed: &ParsedArgs,
inputs: &[InputPayload],
) -> Result<Vec<u8>, CliError> {
let output_format = configured_output_format(parsed, inputs.first())?;
let mut output = Vec::new();
let mut printed = false;
let mut has_truthy_result = false;
if parsed.null_input || inputs.is_empty() {
execute_canonical_source(
parsed,
CanonicalSource {
input_format: "json",
display_name: "<null-input>",
source: "null",
},
&output_format,
&mut output,
&mut printed,
&mut has_truthy_result,
)?;
return Ok(output);
}
for payload in inputs {
let input_format = super::cli_io::input::resolve_input_format(parsed, payload)?;
let source = std::str::from_utf8(&payload.bytes)
.map_err(|_| CliError::Eval("input is not valid UTF-8".to_owned()))?;
execute_canonical_source(
parsed,
CanonicalSource {
input_format: &input_format,
display_name: payload.display_name(),
source,
},
&output_format,
&mut output,
&mut printed,
&mut has_truthy_result,
)?;
}
Ok(output)
}
#[cfg(test)]
pub(super) fn execute_command_to_writer<W: Write>(
parsed: &ParsedArgs,
inputs: &[StreamingInput],
output: &mut W,
) -> Result<bool, CliError> {
Ok(execute_command_to_writer_status(parsed, inputs, output)?.printed())
}
pub(super) fn execute_command_to_writer_status<W: Write>(
parsed: &ParsedArgs,
inputs: &[StreamingInput],
output: &mut W,
) -> Result<ExecutionStatus, CliError> {
let output_format = configured_output_format_from_input_format(
parsed,
inputs.first().map(|input| input.input_format.as_str()),
)?;
let mut printed = false;
let mut has_truthy_result = false;
if parsed.null_input || inputs.is_empty() {
execute_canonical_source(
parsed,
CanonicalSource {
input_format: "json",
display_name: "<null-input>",
source: "null",
},
&output_format,
output,
&mut printed,
&mut has_truthy_result,
)?;
} else {
for input in inputs {
let source_bytes = match &input.source {
StreamingInputSource::Stdin(bytes) => bytes.clone(),
StreamingInputSource::FilePath(path) => {
let mut bytes = Vec::new();
fs::File::open(path)?.read_to_end(&mut bytes)?;
bytes
}
};
let source = std::str::from_utf8(&source_bytes)
.map_err(|_| CliError::Eval("input is not valid UTF-8".to_owned()))?;
execute_canonical_source(
parsed,
CanonicalSource {
input_format: &input.input_format,
display_name: input.display_name(),
source,
},
&output_format,
output,
&mut printed,
&mut has_truthy_result,
)?;
}
}
Ok(ExecutionStatus {
printed,
has_truthy_result,
})
}
fn execute_canonical_source<W: Write>(
parsed: &ParsedArgs,
source: CanonicalSource<'_>,
output_format: &str,
output: &mut W,
printed: &mut bool,
has_truthy_result: &mut bool,
) -> Result<(), CliError> {
let format = canonical_output_format(output_format)?;
let prefs = configured_preferences(parsed, output_format)?;
let codec = CodecService::new();
for (document_index, mut document) in codec
.decode_all(source.input_format, source.source)
.map_err(|error| {
CliError::Canonical(treease_core::evaluator::CanonicalFailure::source_decode(
error,
))
})?
.into_iter()
.enumerate()
{
document.store.set_document_meta_if_absent(
document_index as u32,
source.display_name,
document_index as i32,
);
let outcome = execute_canonical(
&document.store,
document.root,
&parsed.expression,
format,
prefs.indent,
CanonicalFraming::Documents,
);
let success = match outcome {
CanonicalOutcome::Success(success) => success,
CanonicalOutcome::Failure(failure) => return Err(CliError::Canonical(failure)),
};
*has_truthy_result |= success.has_truthy_result();
let CanonicalPayload::Documents(texts) = success.payload() else {
unreachable!("documents framing must return documents");
};
if !texts.is_empty()
&& format == CanonicalOutputFormat::Yaml
&& prefs.print_doc_separators
&& *printed
{
output.write_all(b"---\n")?;
}
for text in texts {
output.write_all(text.as_bytes())?;
*printed = true;
if !text.ends_with('\n') {
output.write_all(b"\n")?;
}
}
}
Ok(())
}
fn canonical_output_format(format: &str) -> Result<CanonicalOutputFormat, CliError> {
match format {
"json" => Ok(CanonicalOutputFormat::Json),
"yaml" => Ok(CanonicalOutputFormat::Yaml),
"toml" => Ok(CanonicalOutputFormat::Toml),
_ => Err(CliError::UnsupportedFormat(format.to_owned())),
}
}
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)
}
fn configured_output_format_from_input_format(
parsed: &ParsedArgs,
first_input_format: Option<&str>,
) -> Result<String, 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(output)
}