#[cfg(test)]
use crate::ast::{PolydatNode, Value};
thread_local! {
static DATA_BASE_DIR: std::cell::RefCell<Option<std::path::PathBuf>> =
const { std::cell::RefCell::new(None) };
}
pub fn set_data_base_dir(dir: Option<std::path::PathBuf>) -> Option<std::path::PathBuf> {
DATA_BASE_DIR.with(|d| d.replace(dir))
}
fn resolve_data_path(filename: &str) -> std::borrow::Cow<'_, str> {
let p = std::path::Path::new(filename);
if p.is_absolute() || p.exists() {
return std::borrow::Cow::Borrowed(filename);
}
DATA_BASE_DIR.with(|d| {
if let Some(base) = d.borrow().as_ref() {
let candidate = base.join(filename);
if candidate.exists() {
return std::borrow::Cow::Owned(candidate.to_string_lossy().into_owned());
}
}
std::borrow::Cow::Borrowed(filename)
})
}
fn read_csv_column(filename: &str, column: &str) -> Vec<String> {
let content = std::fs::read_to_string(resolve_data_path(filename).as_ref())
.unwrap_or_else(|e| panic!("csv_field: failed to read '{filename}': {e}"));
let mut lines = content.lines();
let header_line = lines.next()
.unwrap_or_else(|| panic!("csv_field: '{filename}' is empty"));
let headers: Vec<&str> = split_csv_line(header_line);
let col_idx = if let Ok(idx) = column.parse::<usize>() {
idx
} else {
headers.iter().position(|h| h.trim() == column)
.unwrap_or_else(|| panic!(
"csv_field: column '{column}' not found in '{filename}'. Available: {}",
headers.join(", ")
))
};
let mut values = Vec::new();
for line in lines {
let fields: Vec<&str> = split_csv_line(line);
let val = fields.get(col_idx).unwrap_or(&"").trim().to_string();
values.push(val);
}
if values.is_empty() {
panic!("csv_field: '{filename}' has no data rows");
}
values
}
#[crate::polydat_node(category = Data)]
fn csv_field(
ordinal: u64,
filename: crate::derive_support::Const<&str>,
column: crate::derive_support::Const<&str>,
#[poly_const(read_csv_column, from = (filename, column))]
values: &Vec<String>,
) -> String {
let _ = filename;
let _ = column;
let idx = ordinal as usize % values.len();
values[idx].clone()
}
fn read_csv_data_rows(filename: &str) -> Vec<String> {
let content = std::fs::read_to_string(resolve_data_path(filename).as_ref())
.unwrap_or_else(|e| panic!("csv_row: failed to read '{filename}': {e}"));
let rows: Vec<String> = content.lines()
.skip(1) .filter(|l| !l.trim().is_empty())
.map(|l| l.to_string())
.collect();
if rows.is_empty() {
panic!("csv_row: '{filename}' has no data rows");
}
rows
}
fn read_csv_row_count(filename: &str) -> u64 {
let content = std::fs::read_to_string(resolve_data_path(filename).as_ref())
.unwrap_or_else(|e| panic!("csv_row_count: failed to read '{filename}': {e}"));
content.lines()
.skip(1)
.filter(|l| !l.trim().is_empty())
.count() as u64
}
#[crate::polydat_node(category = Data)]
fn csv_row(
ordinal: u64,
filename: crate::derive_support::Const<&str>,
#[poly_const(read_csv_data_rows, from = filename)]
rows: &Vec<String>,
) -> String {
let idx = ordinal as usize % rows.len();
rows[idx].clone()
}
#[crate::polydat_node(category = Data)]
fn csv_row_count(
filename: crate::derive_support::Const<&str>,
#[poly_const(read_csv_row_count, from = filename)]
count: &u64,
) -> u64 {
*count
}
fn read_jsonl_field(filename: &str, path: &str) -> Vec<String> {
let content = std::fs::read_to_string(resolve_data_path(filename).as_ref())
.unwrap_or_else(|e| panic!("jsonl_field: failed to read '{filename}': {e}"));
let mut values = Vec::new();
for (line_num, line) in content.lines().enumerate() {
let trimmed = line.trim();
if trimmed.is_empty() { continue; }
let parsed: serde_json::Value = serde_json::from_str(trimmed)
.unwrap_or_else(|e| panic!(
"jsonl_field: parse error at line {}: {e}", line_num + 1));
let val = resolve_json_path(&parsed, path);
values.push(val);
}
if values.is_empty() {
panic!("jsonl_field: '{filename}' has no lines");
}
values
}
#[crate::polydat_node(category = Data)]
fn jsonl_field(
ordinal: u64,
filename: crate::derive_support::Const<&str>,
path: crate::derive_support::Const<&str>,
#[poly_const(read_jsonl_field, from = (filename, path))]
values: &Vec<String>,
) -> String {
let _ = filename;
let _ = path;
let idx = ordinal as usize % values.len();
values[idx].clone()
}
fn read_jsonl_lines(filename: &str) -> Vec<String> {
let content = std::fs::read_to_string(resolve_data_path(filename).as_ref())
.unwrap_or_else(|e| panic!("jsonl_row: failed to read '{filename}': {e}"));
let rows: Vec<String> = content.lines()
.filter(|l| !l.trim().is_empty())
.map(|l| l.to_string())
.collect();
if rows.is_empty() {
panic!("jsonl_row: '{filename}' has no lines");
}
rows
}
fn read_jsonl_row_count(filename: &str) -> u64 {
let content = std::fs::read_to_string(resolve_data_path(filename).as_ref())
.unwrap_or_else(|e| panic!("jsonl_row_count: failed to read '{filename}': {e}"));
content.lines()
.filter(|l| !l.trim().is_empty())
.count() as u64
}
#[crate::polydat_node(category = Data)]
fn jsonl_row(
ordinal: u64,
filename: crate::derive_support::Const<&str>,
#[poly_const(read_jsonl_lines, from = filename)]
rows: &Vec<String>,
) -> String {
let idx = ordinal as usize % rows.len();
rows[idx].clone()
}
#[crate::polydat_node(category = Data)]
fn jsonl_row_count(
filename: crate::derive_support::Const<&str>,
#[poly_const(read_jsonl_row_count, from = filename)]
count: &u64,
) -> u64 {
*count
}
fn split_csv_line(line: &str) -> Vec<&str> {
line.split(',').collect()
}
fn resolve_json_path(value: &serde_json::Value, path: &str) -> String {
let mut current = value;
for key in path.split('.') {
match current {
serde_json::Value::Object(map) => {
current = match map.get(key) {
Some(v) => v,
None => return String::new(),
};
}
serde_json::Value::Array(arr) => {
if let Ok(idx) = key.parse::<usize>() {
current = match arr.get(idx) {
Some(v) => v,
None => return String::new(),
};
} else {
return String::new();
}
}
_ => return String::new(),
}
}
match current {
serde_json::Value::String(s) => s.clone(),
serde_json::Value::Null => String::new(),
other => other.to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
fn write_temp_csv(name: &str, content: &str) -> String {
let path = std::env::temp_dir().join(name);
let mut f = std::fs::File::create(&path).unwrap();
f.write_all(content.as_bytes()).unwrap();
path.to_str().unwrap().to_string()
}
#[test]
fn csv_field_by_name() {
let path = write_temp_csv("test_csv_field.csv", "name,age,city\nalice,30,paris\nbob,25,london\n");
let node = CsvField::new(path, "name".to_string());
let mut out = [Value::None];
node.eval(&[Value::U64(0)], &mut out);
assert_eq!(out[0].to_display_string(), "alice");
node.eval(&[Value::U64(1)], &mut out);
assert_eq!(out[0].to_display_string(), "bob");
node.eval(&[Value::U64(2)], &mut out);
assert_eq!(out[0].to_display_string(), "alice");
}
#[test]
fn relative_path_resolves_against_data_base_dir() {
let dir = std::env::temp_dir().join("nbrs_datafile_base_test");
std::fs::create_dir_all(&dir).unwrap();
let file = dir.join("base_rows.jsonl");
std::fs::write(&file, "{\"v\": 7}\n").unwrap();
let prev = set_data_base_dir(None);
assert_eq!(resolve_data_path("base_rows.jsonl").as_ref(), "base_rows.jsonl");
set_data_base_dir(Some(dir.clone()));
assert_eq!(resolve_data_path("base_rows.jsonl").as_ref(), file.to_string_lossy());
assert_eq!(read_jsonl_field("base_rows.jsonl", "v"), vec!["7".to_string()]);
let abs = file.to_string_lossy().into_owned();
assert_eq!(resolve_data_path(&abs).as_ref(), abs);
set_data_base_dir(prev);
}
#[test]
fn csv_field_by_index() {
let path = write_temp_csv("test_csv_idx.csv", "name,age,city\nalice,30,paris\n");
let node = CsvField::new(path, "1".to_string());
let mut out = [Value::None];
node.eval(&[Value::U64(0)], &mut out);
assert_eq!(out[0].to_display_string(), "30");
}
#[test]
fn csv_row_returns_full_line() {
let path = write_temp_csv("test_csv_row.csv", "a,b,c\n1,2,3\n4,5,6\n");
let node = CsvRow::new(path);
let mut out = [Value::None];
node.eval(&[Value::U64(0)], &mut out);
assert_eq!(out[0].to_display_string(), "1,2,3");
}
#[test]
fn csv_row_count_excludes_header() {
let path = write_temp_csv("test_csv_count.csv", "h1,h2\na,b\nc,d\ne,f\n");
let node = CsvRowCount::new(path);
let mut out = [Value::None];
node.eval(&[], &mut out);
assert_eq!(out[0].as_u64(), 3);
}
#[test]
fn jsonl_field_top_level() {
let path = write_temp_csv("test_jsonl_field.jsonl",
"{\"name\":\"alice\",\"age\":30}\n{\"name\":\"bob\",\"age\":25}\n");
let node = JsonlField::new(path, "name".to_string());
let mut out = [Value::None];
node.eval(&[Value::U64(0)], &mut out);
assert_eq!(out[0].to_display_string(), "alice");
node.eval(&[Value::U64(1)], &mut out);
assert_eq!(out[0].to_display_string(), "bob");
}
#[test]
fn jsonl_field_nested_path() {
let path = write_temp_csv("test_jsonl_nested.jsonl",
"{\"user\":{\"name\":\"alice\"}}\n{\"user\":{\"name\":\"bob\"}}\n");
let node = JsonlField::new(path, "user.name".to_string());
let mut out = [Value::None];
node.eval(&[Value::U64(0)], &mut out);
assert_eq!(out[0].to_display_string(), "alice");
}
#[test]
fn jsonl_row_returns_full_json() {
let path = write_temp_csv("test_jsonl_row.jsonl",
"{\"a\":1}\n{\"b\":2}\n");
let node = JsonlRow::new(path);
let mut out = [Value::None];
node.eval(&[Value::U64(0)], &mut out);
assert!(out[0].to_display_string().contains("\"a\":1"));
}
#[test]
fn jsonl_row_count() {
let path = write_temp_csv("test_jsonl_count.jsonl",
"{\"a\":1}\n{\"b\":2}\n{\"c\":3}\n");
let node = JsonlRowCount::new(path);
let mut out = [Value::None];
node.eval(&[], &mut out);
assert_eq!(out[0].as_u64(), 3);
}
}