#![allow(dead_code)]
use std::path::{Path, PathBuf};
use serde_json::Value as Json;
use tabnas_render::is_json_number;
use tabnas_support::{find_spec_dir, unescape, Failure, Row, Value};
use tabnas_transduce::{Cell, Fail, OwnedJsonEvent, PublicColumn, Sink, TableEvent, TableSink};
pub fn spec_dir() -> PathBuf {
find_spec_dir(Some(Path::new(env!("CARGO_MANIFEST_DIR"))))
.expect("a test/spec directory above rs/")
}
pub fn spec(file: &str) -> PathBuf {
spec_dir().join(file)
}
pub fn failure(fail: &Fail) -> Failure {
Failure::new(fail.code.as_str()).with_message(fail.to_string())
}
pub fn malformed(row: &Row, what: impl std::fmt::Display) -> ! {
panic!("{}: malformed fixture cell: {what}", row.location())
}
pub fn json_column(row: &Row, name: &str, default: &str) -> Json {
let cell = row.named(name);
let text = if cell.is_empty() { default } else { cell };
serde_json::from_str(text).unwrap_or_else(|e| malformed(row, format!("{name}: {e}")))
}
pub fn expected_text(cell: &str, _row: &Row) -> tabnas_support::Result<Value> {
Ok(Value::String(unescape(cell)))
}
pub fn parse_value(row: &Row, text: &str) -> f64 {
match text {
"NaN" => f64::NAN,
"Infinity" => f64::INFINITY,
"-Infinity" => f64::NEG_INFINITY,
_ => Some(text)
.filter(|t| is_json_number(t))
.and_then(|t| t.parse::<f64>().ok())
.filter(|v| v.is_finite())
.unwrap_or_else(|| malformed(row, format!("{text:?} is not a value"))),
}
}
pub fn number(row: &Row, object: &serde_json::Map<String, Json>) -> (f64, Option<String>) {
for k in object.keys() {
if k != "num" && k != "value" {
malformed(row, format!("unknown number field {k:?}"));
}
}
let text = |key: &str| match object.get(key) {
None => None,
Some(Json::String(s)) => Some(s.clone()),
Some(other) => malformed(row, format!("{key} must be a string, not {other}")),
};
let lexeme = text("num");
let value = match (text("value"), &lexeme) {
(Some(v), _) => parse_value(row, &v),
(None, Some(l)) if is_json_number(l) => l.parse::<f64>().unwrap_or(0.0),
(None, Some(_)) => 0.0,
(None, None) => malformed(row, "a number object names num, value or both"),
};
(value, lexeme)
}
pub fn cell(row: &Row, json: &Json) -> Cell {
match json {
Json::Null => Cell::Null,
Json::Bool(b) => Cell::Bool(*b),
Json::String(s) => Cell::String(s.as_str().into()),
Json::Object(o) if o.get("missing") == Some(&Json::Bool(true)) && o.len() == 1 => {
Cell::Missing
}
Json::Object(o) => {
let (value, lexeme) = number(row, o);
Cell::Number {
value,
lexeme: lexeme.map(Into::into),
}
}
other => malformed(row, format!("{other} is not a cell")),
}
}
pub enum TableEv {
Schema(Vec<PublicColumn>),
Row(Vec<Cell>),
End,
}
pub fn table_events(row: &Row, json: &Json) -> Vec<TableEv> {
let Json::Array(items) = json else {
malformed(row, "events is a JSON array")
};
items
.iter()
.map(|item| match item {
Json::String(s) if s == "end" => TableEv::End,
Json::Object(o) if o.len() == 1 => match o.iter().next() {
Some((k, Json::Array(labels))) if k == "schema" => TableEv::Schema(
labels
.iter()
.map(|l| match l {
Json::String(s) => PublicColumn::new(s.as_str()),
other => malformed(row, format!("label {other} is not a string")),
})
.collect(),
),
Some((k, Json::Array(cells))) if k == "row" => {
TableEv::Row(cells.iter().map(|c| cell(row, c)).collect())
}
_ => malformed(row, format!("{item} is not a table event")),
},
other => malformed(row, format!("{other} is not a table event")),
})
.collect()
}
pub fn feed_table(sink: &mut impl TableSink, events: &[TableEv]) -> Result<(), Fail> {
for ev in events {
let ev = match ev {
TableEv::Schema(columns) => TableEvent::Schema(columns),
TableEv::Row(cells) => TableEvent::Row(cells),
TableEv::End => TableEvent::End,
};
sink.table_event(ev)?;
}
Ok(())
}
pub fn json_events(row: &Row, json: &Json) -> Vec<OwnedJsonEvent> {
let Json::Array(items) = json else {
malformed(row, "events is a JSON array")
};
items
.iter()
.map(|item| match item {
Json::String(s) => match s.as_str() {
"{" => OwnedJsonEvent::ObjectStart,
"}" => OwnedJsonEvent::ObjectEnd,
"[" => OwnedJsonEvent::ArrayStart,
"]" => OwnedJsonEvent::ArrayEnd,
"end" => OwnedJsonEvent::End,
other => malformed(row, format!("{other:?} is not a JSON event")),
},
Json::Null => OwnedJsonEvent::Null,
Json::Bool(b) => OwnedJsonEvent::Bool(*b),
Json::Object(o) if o.len() == 1 && o.contains_key("key") => match &o["key"] {
Json::String(k) => OwnedJsonEvent::Key(k.as_str().into()),
other => malformed(row, format!("key {other} is not a string")),
},
Json::Object(o) if o.len() == 1 && o.contains_key("str") => match &o["str"] {
Json::String(s) => OwnedJsonEvent::String(s.as_str().into()),
other => malformed(row, format!("str {other} is not a string")),
},
Json::Object(o) => {
let (value, lexeme) = number(row, o);
OwnedJsonEvent::Number {
value,
lexeme: lexeme.map(Into::into),
}
}
other => malformed(row, format!("{other} is not a JSON event")),
})
.collect()
}
pub fn feed_json(sink: &mut impl Sink, events: &[OwnedJsonEvent]) -> Result<(), Fail> {
for ev in events {
sink.event(ev.as_event())?;
}
Ok(())
}
pub fn field<'a>(row: &Row, options: &'a Json, name: &str) -> Option<&'a Json> {
match options {
Json::Object(o) => o.get(name),
other => malformed(row, format!("options {other} is not an object")),
}
}
pub fn known_fields(row: &Row, options: &Json, known: &[&str]) {
if let Json::Object(o) = options {
for k in o.keys() {
if !known.contains(&k.as_str()) {
malformed(row, format!("unknown option {k:?}"));
}
}
}
}