use std::io::{self, BufRead, Write};
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use serde_json::Value;
use tabnas::Tabnas;
use tabnas_render::{CsvOptions, CsvRenderer, JsonOptions, JsonRenderer, MissingText, WriteOut};
use tabnas_transduce::source::{LineFormat, LinesSource};
use tabnas_transduce::{
capability, AbortFlag, Code, Duplicates, Fail, Flow, Guarded, JsonEvent, Limits, Metrics,
ParserSource, Prune, Schema, Selector, Sink, Source, SourceMode, TableBinding, TableEvent,
TableFromJson, TableSink, ValueSource,
};
use crate::fmt;
use crate::headless::{kind_of_event, Seg, KEYS_LISTED};
use crate::load::{
self, Deadline, Deep, Format, LoadError, Stop, STOP_DEPTH, STOP_TIME, STOP_VALUE_DEPTH,
};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Renderer {
Csv,
Json,
Part(&'static str),
}
impl Renderer {
pub fn from_name(name: &str) -> Option<Renderer> {
let name = name.trim().to_ascii_lowercase();
if let Some(id) = crate::translate::id_of(&name) {
return Some(Renderer::Part(id));
}
match name.as_str() {
"csv" => Some(Renderer::Csv),
"json" => Some(Renderer::Json),
_ => None,
}
}
pub fn name(self) -> &'static str {
match self {
Renderer::Csv => "csv",
Renderer::Json => "json",
Renderer::Part(id) => id,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Plan {
Lines,
Incremental { prune: bool },
Materialize,
}
pub fn plan(format: Format, at_root: bool) -> Option<Plan> {
Some(match format {
Format::Text => return None,
Format::Jsonl | Format::Csv | Format::Tsv if at_root => Plan::Lines,
Format::Custom(_) => Plan::Materialize,
f if capability::incremental(f.name()) => Plan::Incremental {
prune: !matches!(f, Format::Yaml | Format::Jsonic | Format::Markdown),
},
_ => Plan::Materialize,
})
}
#[derive(Clone, Debug)]
pub enum What {
Render(Renderer),
Program { rows: Option<Selector> },
Part,
}
#[derive(Clone, Debug)]
pub struct Job {
pub name: String,
pub origin: String,
pub format: Format,
pub what: What,
pub path: Vec<Seg>,
pub compact: bool,
pub indent: usize,
pub timeout: Option<Duration>,
pub started: Option<Instant>,
pub max_output: Option<u64>,
}
impl Job {
fn past_deadline(&self) -> bool {
self.started
.zip(self.timeout)
.and_then(|(started, limit)| started.checked_add(limit))
.is_some_and(|at| Instant::now() >= at)
}
}
pub enum Input<'a> {
Text(&'a str),
Lines(Box<dyn BufRead + Send + 'a>),
}
#[derive(Clone, Debug, PartialEq)]
pub struct Nearest {
pub path: String,
pub kind: &'static str,
pub length: Option<usize>,
pub value: Option<Value>,
pub keys: Option<Vec<String>>,
depth: usize,
}
#[derive(Debug)]
pub enum ExportError {
Usage(String),
Load {
error: Box<LoadError>,
partial: bool,
},
Transduce(Box<Fail>),
Program(Box<Fail>),
NotFound {
message: String,
nearest: Box<Nearest>,
},
ReaderGone,
}
fn limits() -> Limits {
Limits::default()
}
pub fn export(job: &Job, input: Input<'_>, out: Box<dyn Write + Send>) -> Result<(), ExportError> {
export_staged(job, input, out, stage_max(job.format))
}
fn export_staged(
job: &Job,
input: Input<'_>,
out: Box<dyn Write + Send>,
stage_max: usize,
) -> Result<(), ExportError> {
let renderer = match &job.what {
What::Render(Renderer::Part(_)) | What::Part | What::Program { .. } => {
return Err(ExportError::Usage(
"a program's run, a part's among them, goes through run_program".to_string(),
))
}
What::Render(renderer) => renderer,
};
let written = Arc::new(AtomicU64::new(0));
let broken = Arc::new(AtomicBool::new(false));
let stage = Stage::new(out, &written, &broken, stage_max);
let records = match renderer {
Renderer::Csv => Records::Csv,
_ => Records::Json,
};
let pipe = || WriteOut::new(Pipe::new(stage.clone(), records)).with_budget(0);
match renderer {
Renderer::Part(_) => unreachable!("refused above"),
Renderer::Json => {
let options = JsonOptions {
indent: (!job.compact).then_some(job.indent),
trailing_newline: true,
};
attempt(job, input, &written, &broken, &stage, |_| {
Ok(Scope::new(
job.path.clone(),
JsonRenderer::new(pipe(), options.clone()),
))
})
}
Renderer::Csv => {
let options = CsvOptions {
missing: MissingText::Text("".into()),
..CsvOptions::default()
};
attempt(job, input, &written, &broken, &stage, |_| {
let csv = CsvRenderer::new(pipe(), options.clone())
.map_err(|f| ExportError::Transduce(Box::new(f)))?;
let binding = TableBinding {
schema: Schema::Infer,
rows: Selector::root().each_index(),
};
let table = TableFromJson::new(
binding,
&limits(),
Duplicates::LastWins,
Metrics::new(),
EmptyOk::new(csv),
)
.map_err(|f| ExportError::Transduce(Box::new(f)))?;
Ok(Scope::new(job.path.clone(), Rows::new(table)))
})
}
}
}
pub fn run_program(
job: &Job,
input: Input<'_>,
out: Box<dyn Write + Send>,
records: Records,
chain: impl Fn(Box<dyn Write + Send>, AbortFlag) -> Result<Box<dyn Sink + Send>, ExportError>,
) -> Result<(), ExportError> {
let written = Arc::new(AtomicU64::new(0));
let broken = Arc::new(AtomicBool::new(false));
let stage = Stage::new(out, &written, &broken, stage_max(job.format));
attempt(job, input, &written, &broken, &stage, |abort| {
let pipe: Box<dyn Write + Send> = Box::new(Pipe::new(stage.clone(), records));
Ok(Scope::new(job.path.clone(), chain(pipe, abort)?))
})
}
fn attempt<S: Sink + Send + 'static>(
job: &Job,
input: Input<'_>,
written: &AtomicU64,
broken: &AtomicBool,
stage: &Stage,
chain: impl Fn(AbortFlag) -> Result<Scope<S>, ExportError>,
) -> Result<(), ExportError> {
let mode = mode_for(job);
let text = match &input {
Input::Text(text) => Some(*text),
Input::Lines(_) => None,
};
let abort = AbortFlag::new();
let program = AbortFlag::new();
let failed = match run(
job,
input,
chain(program.clone())?,
mode.clone(),
abort,
program,
) {
Ok(()) => return Ok(()),
Err(failed) => failed,
};
let programs = failed.from_sink && matches!(job.what, What::Program { .. } | What::Part);
let refused = failed.verdict.is_none()
&& match &failed.why {
Outcome::Failed(f) => {
(f.code == Code::StreamabilityUnknown && !programs)
|| f.code == Code::DuplicateMember
|| (f.code == Code::InputInvalid
&& f.path.as_deref() == Some(".")
&& matches!(job.format, Format::Jsonic | Format::Yaml))
}
_ => false,
};
match text {
Some(text)
if refused
&& mode != SourceMode::Materialize
&& written.load(Ordering::Relaxed) == 0 =>
{
stage.discard();
let abort = AbortFlag::new();
let program = AbortFlag::new();
run(
job,
Input::Text(text),
chain(program.clone())?,
SourceMode::Materialize,
abort,
program,
)
.map_err(|f| {
stage.commit();
classify(job, *f, written, broken)
})
}
_ => {
stage.commit();
Err(classify(job, *failed, written, broken))
}
}
}
#[derive(Debug)]
enum Outcome {
Done,
Failed(Fail),
Panicked(String),
}
impl Outcome {
fn of(result: Result<Flow, Fail>) -> Outcome {
match result {
Ok(_) => Outcome::Done,
Err(fail) => Outcome::Failed(fail),
}
}
}
struct Ran {
outcome: Outcome,
stop: Option<Arc<Stop>>,
verdict: Option<Verdict>,
from_sink: bool,
}
#[derive(Debug)]
struct Failed {
why: Outcome,
stop: Option<Arc<Stop>>,
verdict: Option<Verdict>,
from_sink: bool,
lines: bool,
}
struct Alarm {
lines: bool,
parsed: Arc<AtomicBool>,
abort: AbortFlag,
program: AbortFlag,
}
impl Alarm {
fn fire(&self) {
self.program.abort();
if self.lines || self.parsed.load(Ordering::Relaxed) {
self.abort.abort();
}
}
}
fn run<S: Sink + Send + 'static>(
job: &Job,
input: Input<'_>,
scope: Scope<S>,
mode: SourceMode,
abort: AbortFlag,
program: AbortFlag,
) -> Result<(), Box<Failed>> {
let format = job.format;
let lines = matches!(input, Input::Lines(_));
let alarm = Alarm {
lines,
parsed: Arc::new(AtomicBool::new(false)),
abort: abort.clone(),
program,
};
let parsed = alarm.parsed.clone();
let ran = load::on_parse_thread(
job.timeout,
job.started,
move || alarm.fire(),
move |deadline: Option<Deadline>| match input {
Input::Lines(reader) => {
let source = LinesSource::new(reader, line_format(format))
.limits(limits())
.abort(abort);
match load::catch_grammar(|| source.run_owned(scope)) {
Ok((outcome, scope)) => Ran {
outcome: Outcome::of(outcome),
stop: None,
verdict: scope.verdict(),
from_sink: scope.sink_failed,
},
Err(what) => Ran {
outcome: Outcome::Panicked(what),
stop: None,
verdict: None,
from_sink: false,
},
}
}
Input::Text(text) => {
let mut parser = match load::make_parser(format) {
Ok(parser) => parser.expect("plain text is refused before a run"),
Err(e) => {
return Ran {
outcome: Outcome::Panicked(e.message),
stop: None,
verdict: None,
from_sink: false,
}
}
};
let notify = abort.clone();
let stop = load::guard(
&mut parser,
deadline.clone(),
load::MAX_RULE_DEPTH,
move || notify.abort(),
);
let text = load::parser_text(text);
match mode {
SourceMode::Materialize => {
materialize(parser, format, text, deadline, &abort, &parsed, scope, stop)
}
mode => {
let source = ParserSource::new(parser, text)
.mode(mode)
.grammar(format.name())
.limits(limits())
.abort(abort);
match load::catch_grammar(|| source.run_owned(scope)) {
Ok((outcome, scope)) => Ran {
outcome: Outcome::of(outcome),
stop: Some(stop),
verdict: scope.verdict(),
from_sink: scope.sink_failed,
},
Err(what) => Ran {
outcome: Outcome::Panicked(what),
stop: Some(stop),
verdict: None,
from_sink: false,
},
}
}
}
}
},
);
match ran.outcome {
Outcome::Done => Ok(()),
why => Err(Box::new(Failed {
why,
stop: ran.stop,
verdict: ran.verdict,
from_sink: ran.from_sink,
lines,
})),
}
}
#[allow(clippy::too_many_arguments)]
fn materialize<S: Sink>(
parser: Tabnas,
format: Format,
text: &str,
deadline: Option<Deadline>,
abort: &AbortFlag,
parsed: &AtomicBool,
scope: Scope<S>,
stop: Arc<Stop>,
) -> Ran {
let value = load::catch_grammar(|| parser.parse(text).map_err(Box::new));
parsed.store(true, Ordering::Relaxed);
let late = deadline.as_ref().is_some_and(Deadline::passed);
if late {
abort.abort();
}
let value = match value {
Err(what) => {
return Ran {
outcome: Outcome::Panicked(what),
stop: Some(stop),
verdict: None,
from_sink: false,
}
}
Ok(Err(e)) => {
let fail = if late || (e.code == "cancel" && abort.is_aborted()) {
Fail::aborted()
} else {
Fail::from_tabnas(&e)
};
return Ran {
outcome: Outcome::Failed(fail),
stop: Some(stop),
verdict: None,
from_sink: false,
};
}
Ok(Ok(value)) => value,
};
drop(parser);
if late {
load::drop_deep(value);
return Ran {
outcome: Outcome::Failed(Fail::aborted()),
stop: Some(stop),
verdict: None,
from_sink: false,
};
}
if format.is_custom() && load::nests_past(&value, load::MAX_VALUE_DEPTH) {
stop.mark(STOP_VALUE_DEPTH);
load::drop_deep(value);
return Ran {
outcome: Outcome::Failed(Fail::aborted()),
stop: Some(stop),
verdict: None,
from_sink: false,
};
}
let mut guarded = Guarded::new(scope, &limits(), abort.clone(), Metrics::new());
let outcome = ValueSource(&value).run(&mut guarded);
let scope = guarded.into_inner();
Ran {
outcome: Outcome::of(outcome),
stop: Some(stop),
verdict: scope.verdict(),
from_sink: scope.sink_failed,
}
}
fn mode_for(job: &Job) -> SourceMode {
if job.format.is_custom() {
return SourceMode::Materialize;
}
let prune = match plan(job.format, job.path.is_empty()) {
Some(Plan::Incremental { prune }) => prune,
Some(Plan::Lines) if capability::incremental(job.format.name()) => true,
_ => return SourceMode::Materialize,
};
let prune = if prune {
match &job.what {
What::Render(Renderer::Csv) => Prune::Under(selector_of(&job.path).each_index()),
What::Render(Renderer::Json) | What::Render(Renderer::Part(_)) | What::Part => {
Prune::Under(selector_of(&job.path))
}
What::Program { rows: Some(rows) } => Prune::Under(rows.clone()),
What::Program { rows: None } => Prune::Never,
}
} else {
Prune::Never
};
SourceMode::Incremental { prune }
}
fn line_format(format: Format) -> LineFormat {
match format {
Format::Tsv => {
let mut options = tabnas_csv::CsvOptions::default();
options.field.separation = Some("\t".to_string());
LineFormat::csv_with(true, options)
}
Format::Csv => LineFormat::csv(),
_ => LineFormat::Jsonl,
}
}
fn selector_of(path: &[Seg]) -> Selector {
let mut s = Selector::root();
for seg in path {
s = match seg {
Seg::Name(n) => s.property(n.as_str()),
Seg::Index(i) => s.index(usize::try_from(*i).unwrap_or(usize::MAX)),
};
}
s
}
pub fn path_text(path: &[Seg]) -> String {
let mut out = String::new();
for seg in path {
match seg {
Seg::Name(n) => fmt::push_jq_name(&mut out, n),
Seg::Index(i) => fmt::push_jq_index(&mut out, *i),
}
}
if out.is_empty() {
out.push('.');
}
out
}
fn classify(job: &Job, failed: Failed, written: &AtomicU64, broken: &AtomicBool) -> ExportError {
let Failed {
why,
stop,
verdict,
from_sink,
lines,
} = failed;
let partial = written.load(Ordering::Relaxed) > 0;
let load = |error: LoadError| ExportError::Load {
error: Box::new(error),
partial,
};
let fail = match why {
Outcome::Done => unreachable!("a run that ended well is no failure"),
Outcome::Panicked(what) => {
return load(load::grammar_failed(job.format, &what).with_origin(&job.origin))
}
Outcome::Failed(fail) => fail,
};
match verdict {
Some(Verdict::Missing(nearest)) => {
return ExportError::NotFound {
message: not_found_message(job, &nearest),
nearest: Box::new(nearest),
}
}
Some(Verdict::FromEnd { index, at }) => {
return ExportError::Usage(format!(
"--render reads the input once, front to back, so it cannot count from the end \
of an array: [{index}] in {} names an item of the array at {at}; use --json \
--path, which reads the whole document",
path_text(&job.path)
))
}
Some(Verdict::Duplicate(mut fail)) => {
fail.committed_output = partial;
return ExportError::Transduce(Box::new(fail));
}
None => {}
}
let position = |fail: &Fail| -> (u32, u32) {
let at = |n: Option<u64>| n.and_then(|n| u32::try_from(n).ok()).unwrap_or(0);
(at(fail.row), at(fail.column))
};
let load = |error: LoadError| ExportError::Load {
error: Box::new(error),
partial,
};
match stop.as_ref().map(|s| s.why()) {
Some(STOP_DEPTH) => {
let (line, col) = stop.as_ref().map_or((0, 0), |s| s.position());
return load(too_deep(job, line, col, Deep::Rules(load::MAX_RULE_DEPTH)));
}
Some(STOP_VALUE_DEPTH) => return load(too_deep(job, 0, 0, Deep::Value)),
Some(STOP_TIME) => {
let (line, col) = stop.as_ref().map_or((0, 0), |s| s.position());
return load(timed_out(job, line, col));
}
_ => {}
}
if lines && !from_sink && fail.code != Code::Aborted && job.past_deadline() {
return load(input_timed_out(job));
}
match fail.code {
Code::Aborted => {
let (line, col) = match job.what {
What::Program { .. } | What::Part if from_sink => (0, 0),
_ => position(&fail),
};
load(timed_out(job, line, col))
}
Code::InputInvalid
if fail.message.starts_with("cancel")
|| fail.message.starts_with("the grammar stopped the parse") =>
{
let (line, col) = position(&fail);
load(too_deep(job, line, col, Deep::Grammar))
}
Code::OutputFailed if broken.load(Ordering::Relaxed) => ExportError::ReaderGone,
_ => {
let mut fail = absolute(fail, &job.path);
fail.committed_output = partial;
match job.what {
What::Program { .. } | What::Part if from_sink => {
ExportError::Program(Box::new(fail))
}
_ => ExportError::Transduce(Box::new(fail)),
}
}
}
}
fn absolute(mut fail: Fail, base: &[Seg]) -> Fail {
if let Some(p) = &fail.path {
let p = p.as_str();
fail.path = Some(match (base.is_empty(), p) {
(true, p) if p.starts_with('[') => format!(".{p}"),
(true, _) => return fail,
(false, ".") => path_text(base),
(false, p) if p.starts_with('.') || p.starts_with('[') => {
format!("{}{p}", path_text(base))
}
(false, _) => return fail,
});
}
fail
}
fn positioned(
job: &Job,
code: &str,
message: String,
hint: &str,
line: u32,
col: u32,
) -> LoadError {
let mut e = LoadError::tagged(code, message).with_hint(hint);
let origin = match (line, col) {
(0, _) => job.origin.clone(),
(line, 0) => format!("{}:{line}", job.origin),
(line, col) => format!("{}:{line}:{col}", job.origin),
};
e = e.with_origin(&origin);
e.line = line;
e.col = col;
e
}
fn input_timed_out(job: &Job) -> LoadError {
let (message, hint) = load::input_timed_out_words(job.timeout.unwrap_or_default());
positioned(job, "timeout", message, &hint, 0, 0)
}
fn too_deep(job: &Job, line: u32, col: u32, why: Deep) -> LoadError {
let (message, hint) = load::too_deep_words(job.format, why);
positioned(job, "too_deep", message, &hint, line, col)
}
fn timed_out(job: &Job, line: u32, col: u32) -> LoadError {
let limit = job.timeout.map_or(0.0, |t| t.as_secs_f64()).to_string();
let (message, hint) = match job.what {
What::Render(_) => (
format!("timeout: the parse ran longer than {limit} s"),
format!(
"The parse had got this far when --timeout {limit} stopped it; what was written \
before that stays written.\nPass a larger --timeout to let it finish, or \
--timeout 0 for no limit."
),
),
What::Program { .. } | What::Part => (
format!("timeout: the run (the parse and the program) ran longer than {limit} s"),
format!(
"--timeout {limit} stopped the run, in the parse or in the program's work on an \
item; what was written before that stays written.\nPass a larger --timeout to \
let it finish, or --timeout 0 for no limit."
),
),
};
positioned(job, "timeout", message, &hint, line, col)
}
fn not_found_message(job: &Job, nearest: &Nearest) -> String {
let seg = &job.path[nearest.depth];
let why = match nearest.kind {
"object" => match seg {
Seg::Name(k) => format!("has no key {}", fmt::quote(k)),
Seg::Index(i) => format!("has no key \"{i}\""),
},
"array" => match nearest.length.unwrap_or(0) {
0 => "is an empty array".to_string(),
1 => "has 1 item, [0]".to_string(),
n => format!("has {n} items, [0] to [{}]", n - 1),
},
kind => format!("is {}, which has no keys", with_article(kind)),
};
let near = if nearest.depth == 0 {
"the root (.)".to_string()
} else {
nearest.path.clone()
};
format!("no {} in {}: {near} {why}", path_text(&job.path), job.name)
}
fn with_article(word: &str) -> String {
match word.chars().next() {
Some('a' | 'e' | 'i' | 'o' | 'u') => format!("an {word}"),
_ => format!("a {word}"),
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Records {
Any,
Csv,
Json,
Lines,
}
const HOLD_MAX: usize = 16 << 20;
const STAGE_MAX: usize = 32 << 10;
fn stage_max(format: Format) -> usize {
match format {
Format::Jsonic | Format::Yaml => HOLD_MAX,
_ => STAGE_MAX,
}
}
struct Stage {
out: Arc<Mutex<Box<dyn Write + Send>>>,
written: Arc<AtomicU64>,
broken: Arc<AtomicBool>,
ready: Mutex<Vec<u8>>,
stage_max: usize,
}
impl Stage {
fn new(
out: Box<dyn Write + Send>,
written: &Arc<AtomicU64>,
broken: &Arc<AtomicBool>,
stage_max: usize,
) -> Arc<Stage> {
Arc::new(Stage {
out: Arc::new(Mutex::new(out)),
written: written.clone(),
broken: broken.clone(),
ready: Mutex::new(Vec::new()),
stage_max,
})
}
fn note(&self, r: io::Result<()>) -> io::Result<()> {
if let Err(e) = &r {
if e.kind() == io::ErrorKind::BrokenPipe {
self.broken.store(true, Ordering::Relaxed);
}
}
r
}
fn send(&self, bytes: &[u8]) -> io::Result<()> {
if bytes.is_empty() {
return Ok(());
}
let r = self
.out
.lock()
.unwrap_or_else(|e| e.into_inner())
.write_all(bytes);
self.note(r)?;
self.written
.fetch_add(bytes.len() as u64, Ordering::Relaxed);
Ok(())
}
fn stage(&self, bytes: &[u8]) -> io::Result<()> {
let mut ready = self.ready.lock().unwrap_or_else(|e| e.into_inner());
ready.extend_from_slice(bytes);
if ready.len() < self.stage_max {
return Ok(());
}
let bytes = std::mem::take(&mut *ready);
drop(ready);
self.send(&bytes)
}
fn commit(&self) {
let _ = self.flush();
}
fn discard(&self) {
self.ready.lock().unwrap_or_else(|e| e.into_inner()).clear();
}
fn flush(&self) -> io::Result<()> {
let bytes = std::mem::take(&mut *self.ready.lock().unwrap_or_else(|e| e.into_inner()));
self.send(&bytes)?;
let r = self.out.lock().unwrap_or_else(|e| e.into_inner()).flush();
self.note(r)
}
}
struct Pipe {
stage: Arc<Stage>,
records: Records,
held: Vec<u8>,
seen: usize,
quoted: bool,
escaped: bool,
depth: usize,
root_object: bool,
after_colon: bool,
word: Vec<u8>,
inner_end: usize,
hold_max: usize,
}
impl Pipe {
fn new(stage: Arc<Stage>, records: Records) -> Pipe {
Pipe {
stage,
records,
held: Vec::new(),
seen: 0,
quoted: false,
escaped: false,
depth: 0,
root_object: false,
after_colon: false,
word: Vec::new(),
inner_end: 0,
hold_max: HOLD_MAX,
}
}
fn send(&self, bytes: &[u8]) -> io::Result<()> {
self.stage.stage(bytes)
}
fn last_end(&mut self) -> usize {
let mut end = 0;
for (i, &b) in self.held.iter().enumerate().skip(self.seen) {
match self.records {
Records::Any => end = i + 1,
Records::Lines => {
if b == b'\n' {
end = i + 1;
}
}
Records::Csv => match b {
b'"' => self.quoted = !self.quoted,
b'\n' if !self.quoted => end = i + 1,
_ => {}
},
Records::Json if self.quoted => match b {
_ if self.escaped => self.escaped = false,
b'\\' => self.escaped = true,
b'"' => {
self.quoted = false;
if self.depth == 1 && self.value_here() {
end = i + 1;
self.after_colon = false;
}
}
_ => {}
},
Records::Json => match b {
b'"' => self.quoted = true,
b'[' | b'{' => {
if self.depth == 0 {
self.root_object = b == b'{';
}
self.depth += 1;
}
b']' | b'}' => {
self.depth = self.depth.saturating_sub(1);
if self.depth <= 1 {
end = i + 1;
self.after_colon = false;
}
}
b':' if self.depth == 1 => self.after_colon = true,
b',' if self.depth == 1 => {
end = i;
self.after_colon = false;
}
b',' if self.depth == 2 => self.inner_end = i,
b'a'..=b'z' if self.depth == 1 => {
self.word.push(b);
if matches!(self.word.as_slice(), b"true" | b"false" | b"null") {
end = i + 1;
self.after_colon = false;
}
}
_ => {}
},
}
if !b.is_ascii_lowercase() {
self.word.clear();
}
}
self.seen = self.held.len();
end
}
fn value_here(&self) -> bool {
!self.root_object || self.after_colon
}
}
impl Write for Pipe {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
if self.records == Records::Any {
self.send(buf)?;
return Ok(buf.len());
}
self.held.extend_from_slice(buf);
let end = match self.last_end() {
0 if self.held.len() > self.hold_max && self.inner_end > 0 => self.inner_end,
0 if self.held.len() > self.hold_max => self.held.len(),
end => end,
};
if end > 0 {
let rest = self.held.split_off(end);
let whole = std::mem::replace(&mut self.held, rest);
self.seen -= end;
self.inner_end = self.inner_end.saturating_sub(end);
self.send(&whole)?;
}
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
let held = std::mem::take(&mut self.held);
self.seen = 0;
self.inner_end = 0;
self.send(&held)?;
self.stage.flush()
}
}
struct Scope<S> {
inner: S,
target: Vec<Seg>,
frames: Vec<Frame>,
state: State,
nearest: Option<Nearest>,
verdict: Option<Verdict>,
sink_failed: bool,
}
#[derive(Clone, Debug, PartialEq)]
enum Verdict {
Missing(Nearest),
FromEnd { index: i64, at: String },
Duplicate(Fail),
}
struct Frame {
kind: Container,
count: usize,
on_path: bool,
matched: bool,
}
enum Container {
Object { key: Option<Box<str>> },
Array,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum State {
Before,
Inside {
depth: usize,
},
After,
}
impl<S: Sink> Scope<S> {
fn new(target: Vec<Seg>, inner: S) -> Scope<S> {
Scope {
inner,
target,
frames: Vec::new(),
state: State::Before,
nearest: None,
verdict: None,
sink_failed: false,
}
}
fn verdict(&self) -> Option<Verdict> {
self.verdict.clone()
}
fn stop(&mut self, verdict: Verdict, fail: Fail) -> Result<Flow, Fail> {
self.verdict = Some(verdict);
Err(fail)
}
fn locate(&self) -> (bool, bool) {
let depth = self.frames.len();
let Some(parent) = self.frames.last() else {
return (true, self.target.is_empty());
};
if !parent.on_path || depth > self.target.len() {
return (false, false);
}
let seg = &self.target[depth - 1];
let matches = match &parent.kind {
Container::Object { key: Some(k) } => seg_names_key(seg, k),
Container::Object { key: None } => false,
Container::Array => seg_names_index(seg, parent.count),
};
(matches, matches && depth == self.target.len())
}
fn count_child(&mut self) {
let depth = self.frames.len();
let Some(parent) = self.frames.last_mut() else {
return;
};
let key = match &parent.kind {
Container::Object { key } => key.clone(),
Container::Array => None,
};
parent.count += 1;
if let Some(near) = &mut self.nearest {
if near.depth + 1 == depth && near.length.is_some() {
near.length = Some(parent.count);
if let (Some(keys), Some(k)) = (&mut near.keys, key) {
if keys.len() < KEYS_LISTED {
keys.push(k.to_string());
}
}
}
}
}
fn forward(&mut self, ev: JsonEvent<'_>) -> Result<Flow, Fail> {
let r = self.inner.event(ev);
if r.is_err() {
self.sink_failed = true;
}
r
}
}
impl<S: Sink> Sink for Scope<S> {
fn event(&mut self, ev: JsonEvent<'_>) -> Result<Flow, Fail> {
match ev {
JsonEvent::Key(k) => {
if let Some(Frame {
kind: Container::Object { key },
..
}) = self.frames.last_mut()
{
*key = Some(k.into());
}
match self.state {
State::Inside { .. } => self.forward(ev),
_ => Ok(Flow::Continue),
}
}
JsonEvent::ObjectEnd | JsonEvent::ArrayEnd => {
self.frames.pop();
match self.state {
State::Inside { depth } => {
let flow = self.forward(ev)?;
if self.frames.len() == depth {
self.state = State::After;
}
Ok(flow)
}
_ => Ok(Flow::Continue),
}
}
JsonEvent::End => match self.state {
State::After => self.forward(ev),
State::Before => {
let fail =
Fail::input(format!("the document has no {}", path_text(&self.target)));
match self.nearest.take() {
Some(near) => self.stop(Verdict::Missing(near), fail),
None => Err(fail),
}
}
State::Inside { .. } => Err(Fail::protocol(
"the document ended inside the value being exported",
)),
},
_ => {
let (on_path, is_target) = self.locate();
let depth = self.frames.len();
let path = if on_path {
path_text(&self.target[..depth])
} else {
String::new()
};
if on_path {
if let Some(parent) = self.frames.last_mut() {
if parent.matched {
let key = match &parent.kind {
Container::Object { key: Some(k) } => k.to_string(),
_ => String::new(),
};
let at = path_text(&self.target[..depth - 1]);
let fail = Fail::new(
Code::DuplicateMember,
format!(
"the member {} of {at} is repeated, and --render has taken \
the first: a stream cannot keep the last, as --json does",
fmt::quote(&key)
),
)
.at_path(at);
return self.stop(Verdict::Duplicate(fail.clone()), fail);
}
parent.matched = true;
}
}
self.count_child();
if self.state == State::Before && is_target {
self.state = State::Inside { depth };
}
let inside = matches!(self.state, State::Inside { .. });
if ev.is_start() {
let kind = if ev == JsonEvent::ObjectStart {
Container::Object { key: None }
} else {
Container::Array
};
if on_path && !is_target {
let object = matches!(kind, Container::Object { .. });
self.nearest = Some(Nearest {
path: path.clone(),
kind: kind_of_event(&ev),
length: Some(0),
value: None,
keys: object.then(Vec::new),
depth,
});
if let (false, Seg::Index(i)) = (object, &self.target[depth]) {
if *i < 0 {
let fail = Fail::input(format!(
"[{i}] counts from the end of the array at {path}"
));
return self.stop(
Verdict::FromEnd {
index: *i,
at: path,
},
fail,
);
}
}
}
self.frames.push(Frame {
kind,
count: 0,
on_path,
matched: false,
});
} else if on_path && !is_target {
self.nearest = Some(Nearest {
path,
kind: kind_of_event(&ev),
length: None,
value: Some(scalar_value(&ev)),
keys: None,
depth,
});
}
if !inside {
return Ok(Flow::Continue);
}
let flow = self.forward(ev)?;
if let State::Inside { depth: entered } = self.state {
if ev.is_scalar() && self.frames.len() == entered {
self.state = State::After;
}
}
Ok(flow)
}
}
}
}
fn seg_names_key(seg: &Seg, key: &str) -> bool {
match seg {
Seg::Name(s) => s == key,
Seg::Index(i) => i.to_string() == key,
}
}
fn seg_names_index(seg: &Seg, index: usize) -> bool {
match seg {
Seg::Index(i) => usize::try_from(*i).is_ok_and(|i| i == index),
Seg::Name(s) => {
let canonical = s == "0" || (!s.starts_with('0') && !s.is_empty());
canonical && s.bytes().all(|b| b.is_ascii_digit()) && s.parse() == Ok(index)
}
}
}
fn scalar_value(ev: &JsonEvent<'_>) -> Value {
match ev {
JsonEvent::Null => Value::Null,
JsonEvent::Bool(b) => Value::Bool(*b),
JsonEvent::Number(n) => {
if !n.value.is_finite() {
Value::String(fmt::number(n.value))
} else if n.value.fract() == 0.0 && n.value.abs() < 1e16 {
Value::from(n.value as i64)
} else {
serde_json::Number::from_f64(n.value).map_or(Value::Null, Value::Number)
}
}
JsonEvent::String(s) => Value::String(s.to_string()),
_ => Value::Null,
}
}
pub(crate) struct Rows<S> {
inner: S,
depth: usize,
started: bool,
kind: Option<RowKind>,
rows: usize,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum RowKind {
Objects,
Scalars,
}
impl<S: Sink> Rows<S> {
pub(crate) fn new(inner: S) -> Rows<S> {
Rows {
inner,
depth: 0,
started: false,
kind: None,
rows: 0,
}
}
fn row(&mut self, ev: JsonEvent<'_>) -> Result<Flow, Fail> {
let index = self.rows;
self.rows += 1;
let at = format!("[{index}]");
let kind = kind_of_event(&ev);
let wanted = if ev == JsonEvent::ObjectStart {
RowKind::Objects
} else if ev == JsonEvent::ArrayStart {
return Err(Fail::input(format!(
"row {at} is an array; a row is an object (its members are the columns) or a \
scalar (one column, \"value\")"
))
.at_path(at));
} else {
RowKind::Scalars
};
match self.kind {
None => self.kind = Some(wanted),
Some(first) if first != wanted => {
let (this, that) = match wanted {
RowKind::Objects => ("an object".to_string(), "a scalar"),
RowKind::Scalars => (with_article(kind), "an object"),
};
return Err(Fail::input(format!(
"row {at} is {this}, but the first row was {that}; every row is an object \
with the columns as its members, or every row is a scalar"
))
.at_path(at));
}
Some(_) => {}
}
match wanted {
RowKind::Objects => {
self.depth += 1;
self.inner.event(ev)
}
RowKind::Scalars => {
for wrapped in [JsonEvent::ObjectStart, JsonEvent::Key("value"), ev] {
if self.inner.event(wrapped)? == Flow::Stop {
return Ok(Flow::Stop);
}
}
self.inner.event(JsonEvent::ObjectEnd)
}
}
}
}
impl<S: Sink> Sink for Rows<S> {
fn event(&mut self, ev: JsonEvent<'_>) -> Result<Flow, Fail> {
if !self.started {
self.started = true;
if ev == JsonEvent::ArrayStart {
self.depth = 1;
return self.inner.event(ev);
}
return Err(Fail::input(format!(
"the value to export is {}, not an array of records; give --path to an array \
whose elements are the rows (--paths --depth 2 shows the arrays)",
with_article(kind_of_event(&ev))
))
.at_path("."));
}
match self.depth {
0 => self.inner.event(ev),
1 => match ev {
JsonEvent::ArrayEnd => {
self.depth = 0;
self.inner.event(ev)
}
JsonEvent::End | JsonEvent::Key(_) | JsonEvent::ObjectEnd => {
Err(Fail::protocol("an event out of place between rows"))
}
_ => self.row(ev),
},
_ => {
if ev.is_start() {
self.depth += 1;
} else if ev.is_end() {
self.depth -= 1;
}
self.inner.event(ev)
}
}
}
}
struct EmptyOk<S> {
inner: S,
held: bool,
}
impl<S: TableSink> EmptyOk<S> {
fn new(inner: S) -> EmptyOk<S> {
EmptyOk { inner, held: false }
}
}
impl<S: TableSink> TableSink for EmptyOk<S> {
fn table_event(&mut self, ev: TableEvent<'_>) -> Result<Flow, Fail> {
match ev {
TableEvent::Schema([]) => {
self.held = true;
Ok(Flow::Continue)
}
TableEvent::Row(_) if self.held => {
self.held = false;
self.inner.table_event(TableEvent::Schema(&[]))?;
self.inner.table_event(ev)
}
TableEvent::End if self.held => Ok(Flow::Continue),
_ => self.inner.table_event(ev),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
use tabnas_transduce::sink::replay;
use tabnas_transduce::{OwnedJsonEvent, Source};
#[derive(Clone, Default)]
struct Shared(Arc<Mutex<Vec<u8>>>);
impl Write for Shared {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.0.lock().unwrap().extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
impl Shared {
fn text(&self) -> String {
String::from_utf8(self.0.lock().unwrap().clone()).unwrap()
}
}
fn unstaged(out: &Shared, written: &Arc<AtomicU64>, broken: &Arc<AtomicBool>) -> Arc<Stage> {
Arc::new(Stage {
out: Arc::new(Mutex::new(Box::new(out.clone()))),
written: written.clone(),
broken: broken.clone(),
ready: Mutex::new(Vec::new()),
stage_max: 0,
})
}
fn job(format: Format, renderer: Renderer, path: &str) -> Job {
Job {
name: "-".into(),
origin: "(stdin)".into(),
format,
what: What::Render(renderer),
path: crate::headless::parse_path(path).unwrap(),
compact: true,
indent: 2,
timeout: None,
started: None,
max_output: None,
}
}
fn run_text(job: &Job, text: &str) -> (Result<(), ExportError>, String) {
let out = Shared::default();
let r = export(job, Input::Text(text), Box::new(out.clone()));
(r, out.text())
}
fn run_lines(job: &Job, text: &str) -> (Result<(), ExportError>, String) {
let out = Shared::default();
let reader = Box::new(io::BufReader::new(text.as_bytes()));
let r = export(job, Input::Lines(reader), Box::new(out.clone()));
(r, out.text())
}
#[test]
fn the_pipe_writes_whole_records() {
let csv = "a,\"b\nc\",\"d\"\"e\"\r\n1,2,3\r\n4,\"5,6\",7\r\n8,9";
let json = "[\n {\"a\": \"x,]}\\\"\"},\n [1, 2],\n 3,\n \"s,]\",\n true,\n null,\n {\"b\": {}}\n]\n";
let object = "{\"a\": [1, 2], \"b\": \"c,d\", \"c\": 5, \"d\": false}";
let lines = "{\"a\":1}\n{\"a\":2}\n{\"a\"";
let after = |text: &str, part: &str| text.find(part).unwrap() + part.len();
let at = |end: usize| (end, end);
let comma = |end: usize| (end, end + 1);
let cases = [
(
Records::Csv,
csv,
vec![
at(after(csv, "\"e\"\r\n")),
at(after(csv, "3\r\n")),
at(after(csv, "7\r\n")),
],
),
(
Records::Json,
json,
vec![
at(after(json, "\"\"}")),
at(after(json, "[1, 2]")),
comma(after(json, "3")),
at(after(json, "\"s,]\"")),
at(after(json, "true")),
at(after(json, "null")),
at(after(json, "{}}")),
at(after(json, "{}}\n]")),
],
),
(
Records::Json,
object,
vec![
at(after(object, "[1, 2]")),
at(after(object, "\"c,d\"")),
comma(after(object, "5")),
at(after(object, "false")),
at(object.len()),
],
),
(
Records::Lines,
lines,
vec![at(after(lines, ":1}\n")), at(after(lines, ":2}\n"))],
),
];
for (records, text, ends) in cases {
for piece in [1, 2, 3, 7, 64] {
for upto in 0..=text.len() {
let out = Shared::default();
let written = Arc::new(AtomicU64::new(0));
let broken = Arc::new(AtomicBool::new(false));
let mut pipe = Pipe::new(unstaged(&out, &written, &broken), records);
for chunk in text.as_bytes()[..upto].chunks(piece) {
assert_eq!(pipe.write(chunk).unwrap(), chunk.len());
}
let whole = ends
.iter()
.filter(|&&(_, seen)| seen <= upto)
.map(|&(end, _)| end)
.max()
.unwrap_or(0);
assert_eq!(
out.text(),
text[..whole],
"{records:?} {text:?} to {upto} by {piece}"
);
assert_eq!(written.load(Ordering::Relaxed), whole as u64);
pipe.flush().unwrap();
assert_eq!(out.text(), text[..upto], "{records:?} flushed at {upto}");
}
}
}
}
#[test]
fn a_record_too_long_to_hold_is_written_as_it_comes() {
let json = "{\"a\": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], \"b\": 1";
let csv = "x,\"0123456789abcdefghijklmnop";
for (records, text) in [(Records::Json, json), (Records::Csv, csv)] {
let out = Shared::default();
let w = Arc::new(AtomicU64::new(0));
let b = Arc::new(AtomicBool::new(false));
let mut pipe = Pipe::new(unstaged(&out, &w, &b), records);
pipe.hold_max = 8;
for chunk in text.as_bytes().chunks(3) {
pipe.write_all(chunk).unwrap();
}
let written = out.text();
assert!(text.starts_with(&written), "{records:?}: {written:?}");
assert!(written.len() > 8, "{records:?}: something was let go");
assert!(
text.len() - written.len() <= 8 + 3,
"{records:?}: {written:?}"
);
if records == Records::Json {
assert_eq!(&text[written.len()..], ", \"b\": 1", "{written:?}");
}
}
}
#[test]
fn a_failure_part_way_leaves_whole_records() {
let mut text: String = (0..3_000)
.map(|i| format!("{{\"id\": {i}, \"name\": \"n{i}\", \"v\": {i}}}\n"))
.collect();
text.push_str("{\"id\": 3000, \"name\": oops}\n");
let (r, out) = run_lines(&job(Format::Jsonl, Renderer::Csv, "."), &text);
assert!(
matches!(r, Err(ExportError::Transduce(ref f)) if f.committed_output),
"{r:?}"
);
assert!(
out.len() > 32 * 1024,
"past the renderer's buffer: {}",
out.len()
);
assert!(out.ends_with("\r\n"), "{:?}", &out[out.len() - 40..]);
for row in out.lines() {
assert_eq!(row.split(',').count(), 3, "{row:?}");
}
let (r, out) = run_lines(&job(Format::Jsonl, Renderer::Json, "."), &text);
assert!(r.is_err());
assert!(out.len() > 32 * 1024, "{}", out.len());
let closed: Value = serde_json::from_str(&format!("{out}]"))
.unwrap_or_else(|e| panic!("{e}: {:?}", &out[out.len() - 80..]));
let rows = closed.as_array().unwrap();
assert!(
rows.iter().all(|r| r["v"].is_number()),
"every element is whole"
);
}
fn events(json: &str) -> Vec<OwnedJsonEvent> {
let value = tabnas_json::parse(json).unwrap();
let mut rec = Vec::new();
tabnas_transduce::ValueSource(&value).run(&mut rec).unwrap();
rec
}
fn streamed(json: &str) -> Vec<OwnedJsonEvent> {
let (r, rec) = ParserSource::new(tabnas_json::make(), json)
.mode(SourceMode::Incremental {
prune: Prune::Never,
})
.grammar("json")
.run_owned(Vec::new());
r.unwrap();
rec
}
#[test]
fn the_plan_follows_the_format_and_the_start() {
assert_eq!(plan(Format::Jsonl, true), Some(Plan::Lines));
assert_eq!(plan(Format::Csv, true), Some(Plan::Lines));
assert_eq!(plan(Format::Tsv, true), Some(Plan::Lines));
assert_eq!(
plan(Format::Jsonl, false),
Some(Plan::Incremental { prune: true })
);
assert_eq!(plan(Format::Csv, false), Some(Plan::Materialize));
for f in [Format::Json, Format::Json5, Format::Jsonc, Format::Zon] {
assert_eq!(
plan(f, true),
Some(Plan::Incremental { prune: true }),
"{f}"
);
assert_eq!(
plan(f, false),
Some(Plan::Incremental { prune: true }),
"{f}"
);
}
for f in [Format::Yaml, Format::Jsonic, Format::Markdown] {
assert_eq!(
plan(f, true),
Some(Plan::Incremental { prune: false }),
"{f}"
);
}
for f in [Format::Toml, Format::Ini, Format::Xml, Format::Feed] {
assert_eq!(plan(f, true), Some(Plan::Materialize), "{f}");
}
assert_eq!(plan(Format::Text, true), None);
assert_eq!(Renderer::from_name(" CSV "), Some(Renderer::Part("csv")));
assert_eq!(Renderer::from_name("json"), Some(Renderer::Part("json")));
assert_eq!(Renderer::from_name("rss"), None);
assert_eq!(Renderer::from_name(" YAML "), Some(Renderer::Part("yaml")));
assert_eq!(Renderer::Part("yaml").name(), "yaml");
assert_eq!(Renderer::from_name("toml"), Some(Renderer::Part("toml")));
assert_eq!(Renderer::from_name("tsv"), None, "TSV is written as csv");
}
#[test]
fn the_scope_re_roots_the_stream_at_the_path() {
let doc = events(r#"{"a": [1, {"b": [true, "x"]}], "c": null}"#);
let scoped = |path: &str| {
let mut scope = Scope::new(crate::headless::parse_path(path).unwrap(), Vec::new());
let r = replay(&doc, &mut scope);
let missing = match scope.verdict() {
Some(Verdict::Missing(near)) => Some(near),
Some(other) => panic!("{other:?}"),
None => None,
};
(r, scope.inner, missing)
};
let (r, got, missing) = scoped(".a[1].b");
assert_eq!(r.unwrap(), Flow::Continue);
assert_eq!(got, events(r#"[true, "x"]"#));
assert_eq!(missing, None);
assert_eq!(scoped(".").1, doc);
assert_eq!(scoped(".a[1].b[1]").1, events(r#""x""#));
assert_eq!(scoped(".a.1.b").1, events(r#"[true, "x"]"#));
assert_eq!(scoped("/a/1/b/0").1, events("true"));
assert_eq!(scoped(".c").1, events("null"));
let (r, got, missing) = scoped(".a[1].nope");
assert_eq!(r.unwrap_err().code, Code::InputInvalid);
assert!(got.is_empty());
let near = missing.unwrap();
assert_eq!(near.path, ".a[1]");
assert_eq!(near.kind, "object");
assert_eq!(near.length, Some(1));
assert_eq!(near.keys, Some(vec!["b".to_string()]));
assert_eq!(near.depth, 2);
let near = scoped(".a[5]").2.unwrap();
assert_eq!((near.kind, near.length), ("array", Some(2)));
let near = scoped(".c.d").2.unwrap();
assert_eq!((near.kind, near.value.clone()), ("null", Some(Value::Null)));
let near = scoped(".zz").2.unwrap();
assert_eq!(near.depth, 0);
assert_eq!(near.keys, Some(vec!["a".to_string(), "c".to_string()]));
assert_eq!(near.length, Some(2), "counted to the end, past the miss");
}
#[test]
fn the_scope_refuses_what_a_stream_cannot_do() {
let scoped = |path: &str, json: &str| {
let mut scope = Scope::new(crate::headless::parse_path(path).unwrap(), Vec::new());
let r = replay(&streamed(json), &mut scope);
let verdict = scope.verdict();
(r, scope.inner, verdict)
};
let (r, got, verdict) = scoped("[-1]", r#"{"-1": 5, "x": 6}"#);
assert_eq!(r.unwrap(), Flow::Continue);
assert_eq!(got, streamed("5"));
assert_eq!(verdict, None);
let (r, got, verdict) = scoped(".a[-1].b", r#"{"a": [{"b": 1}, {"b": 2}]}"#);
assert_eq!(r.unwrap_err().code, Code::InputInvalid);
assert!(got.is_empty());
assert_eq!(
verdict,
Some(Verdict::FromEnd {
index: -1,
at: ".a".into()
})
);
let (r, got, verdict) = scoped(".rows", r#"{"rows": [1], "rows": [2]}"#);
let fail = r.unwrap_err();
assert_eq!(fail.code, Code::DuplicateMember);
assert_eq!(fail.path.as_deref(), Some("."));
assert!(fail.message.contains("\"rows\""), "{}", fail.message);
let mut first = streamed("[1]");
assert_eq!(first.pop(), Some(OwnedJsonEvent::End));
assert_eq!(got, first, "the first was streamed, and no end");
assert!(matches!(verdict, Some(Verdict::Duplicate(f)) if f == fail));
let (r, _, _) = scoped(
".a.rows",
r#"{"a": {"rows": [1]}, "b": 1, "a": {"rows": [2]}}"#,
);
let fail = r.unwrap_err();
assert_eq!(fail.code, Code::DuplicateMember);
assert!(fail.message.contains("\"a\" of ."), "{}", fail.message);
use OwnedJsonEvent as E;
let n = |v: f64| E::Number {
value: v,
lexeme: None,
};
let k = |s: &str| E::Key(s.into());
let doc = vec![
E::ObjectStart,
k("x"),
n(1.0),
k("x"),
n(2.0),
k("rows"),
E::ArrayStart,
E::ObjectStart,
k("k"),
n(1.0),
k("k"),
n(2.0),
E::ObjectEnd,
E::ArrayEnd,
E::ObjectEnd,
E::End,
];
let mut scope = Scope::new(crate::headless::parse_path(".rows").unwrap(), Vec::new());
assert_eq!(replay(&doc, &mut scope).unwrap(), Flow::Continue);
assert_eq!(
scope.inner,
doc[6..14]
.iter()
.cloned()
.chain([E::End])
.collect::<Vec<_>>()
);
}
#[test]
fn the_alarm_raises_the_sources_flag_only_where_no_guard_will() {
let alarm = Alarm {
lines: false,
parsed: Arc::new(AtomicBool::new(false)),
abort: AbortFlag::new(),
program: AbortFlag::new(),
};
alarm.fire();
assert!(!alarm.abort.is_aborted(), "the parser's guard has it");
assert!(
alarm.program.is_aborted(),
"the program, mid-item, has nothing else"
);
alarm.parsed.store(true, Ordering::Relaxed);
alarm.fire();
assert!(alarm.abort.is_aborted(), "the walk after the parse");
let alarm = Alarm {
lines: true,
parsed: Arc::new(AtomicBool::new(false)),
abort: AbortFlag::new(),
program: AbortFlag::new(),
};
alarm.fire();
assert!(alarm.abort.is_aborted(), "a line-by-line read");
assert!(alarm.program.is_aborted());
}
#[test]
fn a_grammar_panic_reports_as_the_loader_reports_one() {
let j = job(Format::Json, Renderer::Json, ".");
let failed = Failed {
why: Outcome::Panicked("boom".into()),
stop: None,
verdict: None,
from_sink: false,
lines: false,
};
match classify(&j, failed, &AtomicU64::new(0), &AtomicBool::new(false)) {
ExportError::Load { error, partial } => {
assert_eq!(error.code, "grammar");
assert_eq!(error.message, "json grammar failed: boom");
assert!(error.plain_report().contains("(stdin)"), "{}", error.report);
assert!(!partial);
}
other => panic!("{other:?}"),
}
assert_eq!(load::catch_grammar(|| panic!("x")).unwrap_err(), "x");
}
#[test]
fn rows_wrap_scalars_and_refuse_the_rest() {
let through = |json: &str| {
let mut rows = Rows::new(Vec::new());
let r = replay(&events(json), &mut rows);
r.map(|_| rows.inner)
};
assert_eq!(
through(r#"[1, "x", null]"#).unwrap(),
events(r#"[{"value": 1}, {"value": "x"}, {"value": null}]"#)
);
assert_eq!(
through(r#"[{"a": [1]}, {"b": {}}]"#).unwrap(),
events(r#"[{"a": [1]}, {"b": {}}]"#)
);
assert_eq!(through("[]").unwrap(), events("[]"));
let e = through(r#"{"a": 1}"#).unwrap_err();
assert_eq!(e.code, Code::InputInvalid);
assert!(e.message.contains("--path"), "{}", e.message);
assert_eq!(e.path.as_deref(), Some("."));
let e = through(r#"[{"a": 1}, [2]]"#).unwrap_err();
assert_eq!(e.path.as_deref(), Some("[1]"));
assert!(e.message.contains("is an array"), "{}", e.message);
let e = through(r#"[{"a": 1}, 2]"#).unwrap_err();
assert!(
e.message
.contains("is a number, but the first row was an object"),
"{}",
e.message
);
let e = through(r#"[2, {"a": 1}]"#).unwrap_err();
assert!(
e.message
.contains("is an object, but the first row was a scalar"),
"{}",
e.message
);
}
#[test]
fn csv_from_json_text_and_from_lines() {
let j = job(Format::Json, Renderer::Csv, ".");
let (r, out) = run_text(&j, r#"[{"a": 1.50, "b": "x"}, {"b": "y,z", "c": true}]"#);
r.unwrap();
assert_eq!(out, "\"a\",\"b\"\r\n\"1.50\",\"x\"\r\n\"\",\"y,z\"\r\n");
let (r, out) = run_text(&j, "[1, 2]");
r.unwrap();
assert_eq!(out, "\"value\"\r\n\"1\"\r\n\"2\"\r\n");
let (r, out) = run_text(&j, "[]");
r.unwrap();
assert_eq!(out, "");
let (r, _) = run_text(&j, "[{}]");
match r.unwrap_err() {
ExportError::Transduce(f) => assert_eq!(f.code, Code::TargetValueUnrepresentable),
other => panic!("{other:?}"),
}
let j = job(Format::Jsonl, Renderer::Csv, ".");
let (r, out) = run_lines(&j, "{\"n\": 1e2}\n\n{\"n\": 2}\n");
r.unwrap();
assert_eq!(out, "\"n\"\r\n\"1e2\"\r\n\"2\"\r\n");
let j = job(Format::Csv, Renderer::Csv, ".");
let (r, out) = run_lines(&j, "a,b\n1,\"x\"\"y\"\n");
r.unwrap();
assert_eq!(out, "\"a\",\"b\"\r\n\"1\",\"x\"\"y\"\r\n");
let j = job(Format::Tsv, Renderer::Json, ".");
let (r, out) = run_lines(&j, "a\tb\n1\t2\n");
r.unwrap();
assert_eq!(out, "[{\"a\":\"1\",\"b\":\"2\"}]\n");
}
#[test]
fn json_streams_the_value_at_the_path() {
let mut j = job(Format::Json, Renderer::Json, ".a");
let (r, out) = run_text(&j, r#"{"a": {"b": [1, 2.0, "x"]}, "c": 1}"#);
r.unwrap();
assert_eq!(out, "{\"b\":[1,2.0,\"x\"]}\n");
j.compact = false;
let (_, out) = run_text(&j, r#"{"a": {"b": [1]}}"#);
assert_eq!(out, "{\n \"b\": [\n 1\n ]\n}\n");
let j = job(Format::Toml, Renderer::Json, ".");
let (r, out) = run_text(&j, "a = 1\nb = [1.5, \"x\"]\n");
r.unwrap();
assert_eq!(out, "{\"a\":1,\"b\":[1.5,\"x\"]}\n");
}
#[test]
fn failures_are_sorted_by_what_they_mean() {
let j = job(Format::Json, Renderer::Json, ".");
let (r, out) = run_text(&j, "{\"a\": 1,\n \"b\": }");
match r.unwrap_err() {
ExportError::Transduce(f) => {
assert_eq!(f.code, Code::InputInvalid);
assert_eq!((f.row, f.column), (Some(2), Some(7)));
assert!(f.committed_output);
}
other => panic!("{other:?}"),
}
assert_eq!(out, "{\"a\":1");
let (r, out) = run_text(&j, "{\"a\": }");
match r.unwrap_err() {
ExportError::Transduce(f) => assert!(!f.committed_output),
other => panic!("{other:?}"),
}
assert_eq!(out, "");
let j = job(Format::Json, Renderer::Csv, ".rows");
match run_text(&j, r#"{"items": []}"#).0.unwrap_err() {
ExportError::NotFound { message, nearest } => {
assert_eq!(message, "no .rows in -: the root (.) has no key \"rows\"");
assert_eq!(nearest.keys, Some(vec!["items".to_string()]));
}
other => panic!("{other:?}"),
}
let j = job(Format::Json, Renderer::Csv, "[-1]");
match run_text(&j, "[[1], [2]]").0.unwrap_err() {
ExportError::Usage(m) => assert_eq!(
m,
"--render reads the input once, front to back, so it cannot count from the end \
of an array: [-1] in .[-1] names an item of the array at .; use --json --path, \
which reads the whole document"
),
other => panic!("{other:?}"),
}
let j = job(Format::Json, Renderer::Csv, ".a[-2]");
match run_text(&j, r#"{"a": [[1], [2]]}"#).0.unwrap_err() {
ExportError::Usage(m) => assert!(
m.contains(": [-2] in .a[-2] names an item of the array at .a;"),
"{m}"
),
other => panic!("{other:?}"),
}
let j = job(Format::Json, Renderer::Json, "[-1]");
let (r, out) = run_text(&j, r#"{"-1": [1]}"#);
r.unwrap();
assert_eq!(out, "[1]\n", "on an object, the key");
let j = job(Format::Json, Renderer::Csv, ".rows");
let (r, out) = run_text(&j, r#"{"rows": [{"v": 1}], "rows": [{"v": 2}]}"#);
match r.unwrap_err() {
ExportError::Transduce(f) => {
assert_eq!(f.code, Code::DuplicateMember);
assert_eq!(f.path.as_deref(), Some("."));
assert!(f.committed_output);
}
other => panic!("{other:?}"),
}
assert_eq!(out, "\"v\"\r\n\"1\"\r\n");
let j = job(Format::Json, Renderer::Csv, ".a.b");
let (r, out) = run_text(&j, r#"{"a": {"b": [{"x": 1}, [2]]}}"#);
match r.unwrap_err() {
ExportError::Transduce(f) => {
assert_eq!(f.path.as_deref(), Some(".a.b[1]"));
assert!(f.committed_output);
}
other => panic!("{other:?}"),
}
assert_eq!(out, "\"x\"\r\n\"1\"\r\n");
assert_eq!(
absolute(
Fail::input("x").at_path("column \"a\", row 1"),
&[Seg::Name("a".into())]
)
.path
.as_deref(),
Some("column \"a\", row 1")
);
let root = job(Format::Json, Renderer::Csv, ".");
let (r, out) = run_text(&root, r#"[{"x": 1}, [2]]"#);
match r.unwrap_err() {
ExportError::Transduce(f) => assert_eq!(f.path.as_deref(), Some(".[1]")),
other => panic!("{other:?}"),
}
assert_eq!(out, "\"x\"\r\n\"1\"\r\n");
let at = |p: &str, base: &[Seg]| absolute(Fail::input("x").at_path(p), base).path;
assert_eq!(at("[0].a", &[]).as_deref(), Some(".[0].a"));
assert_eq!(at(".", &[]).as_deref(), Some("."));
assert_eq!(at(".a", &[]).as_deref(), Some(".a"));
assert_eq!(at("[0]", &[Seg::Index(2)]).as_deref(), Some(".[2][0]"));
assert_eq!(at(".", &[Seg::Index(2)]).as_deref(), Some(".[2]"));
assert_eq!(path_text(&[]), ".");
assert_eq!(path_text(&[Seg::Index(-1)]), ".[-1]");
assert_eq!(
path_text(&[
Seg::Name("a b".into()),
Seg::Index(-2),
Seg::Name("c".into())
]),
r#"."a b"[-2].c"#
);
let deep = format!("{}1{}", "[".repeat(300), "]".repeat(300));
match run_text(&j, &deep).0.unwrap_err() {
ExportError::Load { error, partial } => {
assert_eq!(error.code, "too_deep");
assert!(error.message.contains("json grammar"), "{}", error.message);
assert!(error.line > 0);
assert!(!partial);
assert!(
error.plain_report().contains("(stdin):1:"),
"{}",
error.report
);
}
other => panic!("{other:?}"),
}
struct Gone;
impl Write for Gone {
fn write(&mut self, _: &[u8]) -> io::Result<usize> {
Err(io::Error::from(io::ErrorKind::BrokenPipe))
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
let j = job(Format::Json, Renderer::Json, ".");
assert!(matches!(
export(&j, Input::Text("[1]"), Box::new(Gone)).unwrap_err(),
ExportError::ReaderGone
));
struct Full;
impl Write for Full {
fn write(&mut self, _: &[u8]) -> io::Result<usize> {
Err(io::Error::from(io::ErrorKind::StorageFull))
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
match export(&j, Input::Text("[1]"), Box::new(Full)).unwrap_err() {
ExportError::Transduce(f) => assert_eq!(f.code, Code::OutputFailed),
other => panic!("{other:?}"),
}
}
fn whole(text: &str, format: Format) -> String {
let doc = load::parse(text, format).unwrap();
format!("{}\n", fmt::to_json_compact(&doc, 0))
}
#[test]
fn a_stream_refused_part_way_falls_back_to_the_whole_value() {
for (format, text) in [
(Format::Jsonic, "{a:1}\n{b:2}\n"),
(Format::Yaml, "a: 1\n---\nb: 2\n"),
(
Format::Yaml,
"base: &b\n x: 1\nderived:\n <<: *b\n y: 2\n",
),
(Format::Json5, "{a: {x: 1}, a: {y: 2}}"),
] {
let j = job(format, Renderer::Json, ".");
let (r, out) = run_text(&j, text);
r.unwrap_or_else(|e| panic!("{format}: {e:?}"));
assert_eq!(out, whole(text, format), "{format}: {text:?}");
}
for (format, text) in [
(Format::Jsonic, "{a:1}\n{a:2}\n"),
(Format::Yaml, "a: 1\n---\na: 2\n"),
] {
let j = job(format, Renderer::Csv, ".");
let (r, out) = run_text(&j, text);
r.unwrap_or_else(|e| panic!("{format}: {e:?}"));
assert_eq!(out, "\"a\"\r\n\"1\"\r\n\"2\"\r\n", "{format}");
}
for format in [Format::Json, Format::Jsonic] {
let j = job(format, Renderer::Csv, ".");
match run_text(&j, r#"{"a": 1}"#).0.unwrap_err() {
ExportError::Transduce(f) => {
assert_eq!(f.code, Code::InputInvalid, "{format}");
assert!(f.message.contains("--path"), "{format}: {}", f.message);
}
other => panic!("{format}: {other:?}"),
}
}
let long = "x".repeat(2 * STAGE_MAX);
let j = job(Format::Jsonic, Renderer::Json, ".");
for text in [
format!("{{a:'{long}', b:'{long}'}}\n{{c:2}}\n"),
format!("{{a:'{long}'}}\n{{b:2}}\n"),
] {
let (r, out) = run_text(&j, &text);
r.unwrap();
assert_eq!(out, whole(&text, Format::Jsonic));
}
let rows: String = (0..6_000)
.map(|i| format!("{{\"v\": {i}}},"))
.collect::<String>()
.trim_end_matches(',')
.to_string();
let j = job(Format::Json, Renderer::Json, ".rows");
let (r, out) = run_text(&j, &format!("{{\"rows\": [{rows}], \"rows\": [1]}}"));
match r.unwrap_err() {
ExportError::Transduce(f) => {
assert_eq!(f.code, Code::DuplicateMember);
assert!(f.committed_output);
}
other => panic!("{other:?}"),
}
assert!(out.len() > STAGE_MAX, "{}", out.len());
assert!(out.starts_with("[{\"v\":0},{\"v\":1},"), "{:?}", &out[..40]);
assert!(
out.ends_with("{\"v\":5999}]"),
"the first value, whole: {:?}",
&out[out.len() - 20..]
);
}
#[test]
fn a_stream_refused_after_output_left_is_a_failure_with_the_output_partial() {
let j = job(Format::Jsonic, Renderer::Json, ".");
let long = "x".repeat(200);
let text = format!("{{a:'{long}'}}\n{{b:2}}\n");
let out = Shared::default();
let r = export_staged(&j, Input::Text(&text), Box::new(out.clone()), 64);
match r.unwrap_err() {
ExportError::Transduce(f) => {
assert_eq!(f.code, Code::StreamabilityUnknown, "{}", f.message);
assert!(f.committed_output);
}
other => panic!("{other:?}"),
}
assert_eq!(out.text(), format!("{{\"a\":\"{long}\"}}"));
let (r, out) = run_text(&j, &text);
r.unwrap();
assert_eq!(out, whole(&text, Format::Jsonic));
}
#[test]
fn a_programs_own_refusal_does_not_run_it_again_over_the_whole_value() {
struct Refuses;
impl Sink for Refuses {
fn event(&mut self, _ev: JsonEvent<'_>) -> Result<Flow, Fail> {
Err(Fail::new(
Code::StreamabilityUnknown,
"the program's step cannot stream this",
))
}
}
for what in [What::Program { rows: None }, What::Part] {
let mut j = job(Format::Json, Renderer::Json, ".");
j.what = what.clone();
let builds = std::sync::atomic::AtomicUsize::new(0);
let r = run_program(
&j,
Input::Text("[1, 2, 3]"),
Box::new(Shared::default()),
Records::Json,
|_out, _abort| {
builds.fetch_add(1, Ordering::Relaxed);
Ok(Box::new(Refuses))
},
);
match r.unwrap_err() {
ExportError::Program(f) => {
assert_eq!(f.code, Code::StreamabilityUnknown, "{what:?}")
}
other => panic!("{what:?}: {other:?}"),
}
assert_eq!(builds.load(Ordering::Relaxed), 1, "{what:?}: chains built");
}
}
#[test]
fn json5_exports_a_continuation_across_crlf() {
let j = job(Format::Json5, Renderer::Json, ".");
let lf = "['a\\\nb', {k: 'c\\\nd'}]\n";
let (r, one) = run_text(&j, lf);
r.unwrap();
let (r, two) = run_text(&j, &lf.replace('\n', "\r\n"));
r.unwrap_or_else(|e| panic!("{e:?}"));
assert_eq!(two, one);
assert!(one.contains(r#""ab""#) && one.contains(r#""cd""#), "{one}");
}
#[test]
fn the_deadline_covers_the_walk_after_a_parse() {
struct Slow;
impl Sink for Slow {
fn event(&mut self, _ev: JsonEvent<'_>) -> Result<Flow, Fail> {
std::thread::sleep(Duration::from_millis(10));
Ok(Flow::Continue)
}
}
let toml: String = (0..60).map(|i| format!("k{i} = {i}\n")).collect();
let mut j = job(Format::Toml, Renderer::Json, ".");
j.timeout = Some(Duration::from_millis(100));
let failed = run(
&j,
Input::Text(&toml),
Scope::new(Vec::new(), Slow),
SourceMode::Materialize,
AbortFlag::new(),
AbortFlag::new(),
)
.unwrap_err();
match (&failed.why, failed.stop.as_ref().map(|s| s.why())) {
(Outcome::Failed(fail), Some(0)) => {
assert_eq!(fail.code, Code::Aborted, "the alarm stopped the walk");
}
(Outcome::Failed(_), Some(STOP_TIME)) => {}
other => panic!("{:?}", other.1),
}
match classify(&j, *failed, &AtomicU64::new(0), &AtomicBool::new(false)) {
ExportError::Load { error, .. } => assert_eq!(error.code, "timeout"),
other => panic!("{other:?}"),
}
j.timeout = Some(Duration::from_secs(120));
run(
&j,
Input::Text("a = 1\n"),
Scope::new(Vec::new(), Slow),
SourceMode::Materialize,
AbortFlag::new(),
AbortFlag::new(),
)
.unwrap();
}
#[test]
fn the_alarm_reaches_a_program_slow_on_one_item_in_every_mode() {
struct UntilAborted {
program: AbortFlag,
waited: Arc<AtomicBool>,
}
impl Sink for UntilAborted {
fn event(&mut self, _ev: JsonEvent<'_>) -> Result<Flow, Fail> {
let started = std::time::Instant::now();
while !self.program.is_aborted() {
if started.elapsed() > Duration::from_secs(10) {
return Ok(Flow::Continue);
}
std::thread::sleep(Duration::from_millis(2));
}
self.waited.store(true, Ordering::Relaxed);
let mut fail = Fail::aborted();
fail.row = Some(3);
fail.column = Some(25);
Err(fail)
}
}
for (format, mode) in [
(
Format::Json,
SourceMode::Incremental {
prune: Prune::Never,
},
),
(Format::Toml, SourceMode::Materialize),
] {
let text = match format {
Format::Json => "[1, 2, 3]",
_ => "a = 1\n",
};
let mut j = job(format, Renderer::Json, ".");
j.what = What::Program { rows: None };
j.timeout = Some(Duration::from_millis(2000));
let program = AbortFlag::new();
let waited = Arc::new(AtomicBool::new(false));
let started = std::time::Instant::now();
let failed = run(
&j,
Input::Text(text),
Scope::new(
Vec::new(),
UntilAborted {
program: program.clone(),
waited: waited.clone(),
},
),
mode,
AbortFlag::new(),
program,
)
.unwrap_err();
assert!(
started.elapsed() < Duration::from_secs(6),
"{format}: the run went on {:?} past the deadline",
started.elapsed()
);
assert!(
waited.load(Ordering::Relaxed),
"{format}: the alarm raised the flag"
);
assert!(failed.from_sink, "{format}: the sink's failure");
match classify(&j, *failed, &AtomicU64::new(0), &AtomicBool::new(false)) {
ExportError::Load { error, partial } => {
assert_eq!(error.code, "timeout", "{format}");
assert!(!partial, "{format}");
assert!(
error
.message
.contains("the run (the parse and the program)"),
"{format}: {}",
error.message
);
assert_eq!(
(error.line, error.col),
(0, 0),
"{format}: the program's span is no position in the input"
);
let report = error.plain_report();
assert!(
report.contains("--> (stdin)\n") && !report.contains("--> (stdin):"),
"{format}: {report}"
);
assert!(
!error.hint.contains("got this far"),
"{format}: the hint claims no position: {}",
error.hint
);
}
other => panic!("{format}: {other:?}"),
}
}
let mut j = job(Format::Json, Renderer::Json, ".");
j.timeout = Some(Duration::from_millis(100));
match run_text(&j, "[1, 2, 3]").0 {
Ok(()) => {}
Err(ExportError::Load { error, .. }) => {
assert_eq!(error.code, "timeout", "a busy machine's deadline");
assert!(error.message.starts_with("timeout: the parse ran longer"));
}
Err(other) => panic!("{other:?}"),
}
}
fn late_custom_parse(format: Format, text: &str) -> ExportError {
let mut j = job(format, Renderer::Json, ".");
j.timeout = Some(Duration::from_millis(1));
let ran = load::on_parse_thread(
j.timeout,
None,
|| {},
|deadline| {
let d = deadline.clone().expect("a deadline");
while !d.passed() {
std::thread::sleep(Duration::from_millis(1));
}
let mut parser = load::make_parser(format).unwrap().unwrap();
let stop = load::guard(&mut parser, None, load::MAX_RULE_DEPTH, || {});
materialize(
parser,
format,
text,
deadline,
&AbortFlag::new(),
&AtomicBool::new(false),
Scope::new(Vec::new(), Vec::<OwnedJsonEvent>::new()),
stop,
)
},
);
let failed = Failed {
why: ran.outcome,
stop: ran.stop,
verdict: ran.verdict,
from_sink: ran.from_sink,
lines: false,
};
classify(&j, failed, &AtomicU64::new(0), &AtomicBool::new(false))
}
#[test]
fn a_late_custom_parse_is_a_timeout_before_its_depth() {
use crate::grammar::{self, Definition};
let def = Definition::parse(
"--grammar-expr",
"impl-late-nest=doc = \"(\" doc \")\" / \"x\"\n",
)
.unwrap();
let format = Format::Custom(grammar::register(def, crate::load::Limits::NONE).unwrap());
let deep = "(".repeat(600) + "x" + &")".repeat(600);
for text in [deep.as_str(), "x)"] {
match late_custom_parse(format, text) {
ExportError::Load { error, partial } => {
assert_eq!(error.code, "timeout", "{text:.8}: {}", error.message);
assert!(!partial);
}
other => panic!("{text:.8}: {other:?}"),
}
}
let mut j = job(format, Renderer::Json, ".");
j.timeout = Some(Duration::from_secs(120));
match run_text(&j, &deep).0.unwrap_err() {
ExportError::Load { error, .. } => assert_eq!(error.code, "too_deep"),
other => panic!("{other:?}"),
}
match run_text(&j, "x)").0.unwrap_err() {
ExportError::Transduce(f) => assert_eq!(f.code, Code::InputInvalid),
other => panic!("{other:?}"),
}
}
#[test]
fn the_timeout_stops_both_kinds_of_source() {
let mut j = job(Format::Toml, Renderer::Json, ".");
j.timeout = Some(Duration::from_millis(1));
let toml: String = (0..3_000)
.map(|i| format!("[[item]]\nid = {i}\nname = \"item {i}\"\n\n"))
.collect();
match run_text(&j, &toml).0.unwrap_err() {
ExportError::Load { error, partial } => {
assert_eq!(error.code, "timeout");
assert!(error.line > 0, "how far it got");
assert!(!partial, "a walk writes nothing before the parse ends");
}
other => panic!("{other:?}"),
}
let mut j = job(Format::Jsonl, Renderer::Csv, ".");
let lines: String = (0..40_000)
.map(|i| format!("{{\"id\": {i}, \"name\": \"item {i}\"}}\n"))
.collect();
struct Slow<'a>(&'a [u8]);
impl io::Read for Slow<'_> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
std::thread::sleep(Duration::from_millis(2));
self.0.read(buf)
}
}
j.timeout = Some(Duration::from_millis(50));
let out = Shared::default();
let reader = Box::new(io::BufReader::new(Slow(lines.as_bytes())));
match export(&j, Input::Lines(reader), Box::new(out)).unwrap_err() {
ExportError::Load { error, .. } => {
assert_eq!(error.code, "timeout");
assert!(error.line > 0, "the record being read");
assert_eq!(error.col, 0, "no column");
assert_eq!(
error.plain_report().lines().nth(1),
Some(format!(" --> (stdin):{}", error.line).as_str())
);
}
other => panic!("{other:?}"),
}
j.timeout = Some(Duration::from_secs(120));
run_lines(&j, &lines).0.unwrap();
}
#[test]
fn a_position_without_a_column_names_the_line_alone() {
let j = job(Format::Jsonl, Renderer::Csv, ".");
let origin = |line, col| {
let e = positioned(&j, "timeout", "m".into(), "h", line, col);
(
e.line,
e.col,
e.plain_report().lines().nth(1).map(str::to_string),
)
};
let at = |text: &str| Some(format!(" --> {text}"));
assert_eq!(origin(3, 7), (3, 7, at("(stdin):3:7")));
assert_eq!(origin(3, 0), (3, 0, at("(stdin):3")));
assert_eq!(origin(0, 0), (0, 0, at("(stdin)")));
}
#[test]
fn an_empty_schema_is_held_back() {
use tabnas_transduce::Table;
let mut t = EmptyOk::new(Table::default());
t.table_event(TableEvent::Schema(&[])).unwrap();
t.table_event(TableEvent::End).unwrap();
assert!(!t.inner.ended, "nothing reached the renderer");
let mut t = EmptyOk::new(Table::default());
t.table_event(TableEvent::Schema(&[])).unwrap();
t.table_event(TableEvent::Row(&[])).unwrap();
assert!(t.inner.columns.is_empty() && t.inner.rows.len() == 1);
}
}