use std::io::Write;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use tabnas_alchemy::effects::{self, RendererProfile};
use tabnas_alchemy::shared::{Renderers, Routers};
use tabnas_alchemy::value::Val;
use tabnas_alchemy::{Output, Program};
use tabnas_render::WriteOut;
use tabnas_transduce::{Code, Fail, Limits, Metrics, Sink};
use crate::export::{self, ExportError, Input, Job, Records, Renderer};
use crate::load::{self, LoadError};
pub const EXPR_NAME: &str = "--alchemy-expr";
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ProgramArg {
File(PathBuf),
Expr(String),
}
impl ProgramArg {
pub fn name(&self) -> String {
match self {
ProgramArg::File(path) => path.display().to_string(),
ProgramArg::Expr(_) => EXPR_NAME.to_string(),
}
}
pub fn path(&self) -> Option<&Path> {
match self {
ProgramArg::File(path) => Some(path),
ProgramArg::Expr(_) => None,
}
}
pub fn read(&self, max_size: Option<u64>) -> Result<String, LoadError> {
match self {
ProgramArg::File(path) => load::read_path_within(path, max_size).map_err(|e| {
if e.is_too_large() {
let size = std::fs::metadata(path).ok().map(|m| m.len());
LoadError::program_too_large(size, max_size.unwrap_or(0))
.with_origin(&load::origin_of(path))
} else {
e
}
}),
ProgramArg::Expr(text) => Ok(text.clone()),
}
}
}
pub fn compile(text: &str, file: &str) -> Result<Program, Fail> {
tabnas_alchemy::compile(text, file, routers(), renderers())
}
pub(crate) fn routers() -> Arc<dyn Routers<Val>> {
Arc::new(tabnas_transduce::routers())
}
pub(crate) fn renderers() -> Arc<dyn Renderers> {
Arc::new(tabnas_render::renderers())
}
pub struct Compiled {
pub program: Program,
pub source: String,
}
pub fn is_programs(code: Code) -> bool {
matches!(
code,
Code::DslParseError | Code::DslTypeError | Code::StreamReused | Code::StreamabilityUnknown
)
}
pub fn is_placed(fail: &Fail) -> bool {
is_programs(fail.code) || fail.row.is_some()
}
pub fn check_render(program: &Program, render: Option<Renderer>) -> Result<(), String> {
match (program.output(), render) {
(Output::Text, Some(renderer)) => Err(format!(
"--render {} was given, but the program renders its own text ({}): drop --render, \
or export a table or JSON events for aless to render",
renderer.name(),
program.file()
)),
_ => Ok(()),
}
}
fn renderer(render: Renderer) -> Option<tabnas_alchemy::Renderer> {
match render {
Renderer::Csv => Some(tabnas_alchemy::Renderer::Csv),
Renderer::Json => Some(tabnas_alchemy::Renderer::Json),
Renderer::Part(_) => None,
}
}
pub fn run(
job: &Job,
program: &Program,
render: Option<Renderer>,
input: Input<'_>,
out: Box<dyn Write + Send>,
) -> Result<(), ExportError> {
let limits = Limits {
max_output_bytes: job.max_output,
..Limits::default()
};
let records = match (program.output(), render) {
(Output::Text, _) => match effects::summarize(program).renderer {
RendererProfile::Json { .. } => Records::Json,
RendererProfile::Csv { .. } => Records::Csv,
RendererProfile::Text => Records::Any,
},
(Output::TableRows, Some(Renderer::Json)) => Records::Json,
(Output::TableRows, _) => Records::Csv,
(Output::JsonEvents, Some(Renderer::Csv)) => Records::Csv,
(Output::JsonEvents, _) => Records::Json,
};
export::run_program(job, input, out, records, |pipe, abort| {
let metrics = Metrics::new();
let out = WriteOut::new(pipe)
.with_budget(0)
.with_limits(&limits)
.with_metrics(metrics.clone());
program
.with_abort(abort)
.sink_out(Box::new(out), render.and_then(renderer), &limits, metrics)
.map(|sink| Box::new(sink) as Box<dyn Sink + Send>)
.map_err(|fail| ExportError::Program(Box::new(fail)))
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_program_is_named_by_its_file_or_by_the_option() {
let file = ProgramArg::File(PathBuf::from("p.alc"));
assert_eq!(file.name(), "p.alc");
assert_eq!(file.path(), Some(Path::new("p.alc")));
let expr = ProgramArg::Expr("def export [input] (json input)".into());
assert_eq!(expr.name(), EXPR_NAME);
assert_eq!(expr.path(), None);
assert_eq!(
expr.read(Some(4)).unwrap(),
"def export [input] (json input)",
"a program on the command line is not measured against --max-size"
);
let missing = ProgramArg::File(PathBuf::from("/nonexistent/p.alc"));
assert!(missing.read(None).unwrap_err().is_io());
}
#[test]
fn the_programs_own_codes_are_told_from_the_inputs() {
for code in [
Code::DslParseError,
Code::DslTypeError,
Code::StreamReused,
Code::StreamabilityUnknown,
] {
assert!(is_programs(code), "{code:?}");
}
for code in [
Code::InputInvalid,
Code::InputOrderViolation,
Code::ResourceLimitExceeded,
Code::Aborted,
Code::OutputFailed,
Code::ProtocolOrderError,
] {
assert!(!is_programs(code), "{code:?}");
}
}
#[test]
fn a_sinks_failure_is_placed_in_the_program_by_code_or_position() {
let recursion = Fail::new(Code::StreamabilityUnknown, "recursion: ...");
assert!(is_placed(&recursion), "a language code, no position");
let mut refused = Fail::new(Code::InputInvalid, "no Bob");
assert!(!is_placed(&refused), "the transducer's code, no position");
refused.row = Some(3);
refused.column = Some(14);
assert!(is_placed(&refused), "the transducer's code at a form");
let missing = Fail::new(Code::MissingValue, "row 2 has no value");
assert!(
!is_placed(&missing),
"a renderer's, over the program's rows"
);
let limit = Fail::new(Code::ResourceLimitExceeded, "max_columns");
assert!(!is_placed(&limit));
}
#[test]
fn render_is_refused_for_a_program_that_renders_its_own_text() {
let text = compile("def export [input] (json input)", "t.alc").unwrap();
assert_eq!(text.output(), Output::Text);
assert!(check_render(&text, None).is_ok());
let e = check_render(&text, Some(Renderer::Json)).unwrap_err();
assert!(e.contains("--render json") && e.contains("t.alc"), "{e}");
let events = compile("def export [input] input", "e.alc").unwrap();
assert_eq!(events.output(), Output::JsonEvents);
assert!(check_render(&events, Some(Renderer::Csv)).is_ok());
assert!(check_render(&events, Some(Renderer::Json)).is_ok());
}
#[test]
fn a_program_that_does_not_compile_fails_with_its_own_code() {
let e = compile("def export [input]\n (json input", "bad.alc").unwrap_err();
assert_eq!(e.code, Code::DslParseError);
assert!(e.row.is_some() && e.column.is_some(), "{e}");
let e = compile("def export [input] (nope input)", "bad.alc").unwrap_err();
assert_eq!(e.code, Code::DslTypeError);
assert!(e.message.starts_with("unknown_name"), "{}", e.message);
}
}