use crate::Error;
use crate::dir_context::PathResolver;
use crate::runtime::Runtime;
use crate::script::aip_modules::aip_csv::{lua_matrix_to_rows, lua_value_to_csv_string};
use crate::script::aip_modules::support::check_access_write;
use crate::script::support::{collect_string_sequence, expect_table};
use crate::support::W;
use crate::types::{CsvContent, CsvOptions, FileInfo};
use mlua::{FromLua as _, IntoLua, Lua, Value};
pub(super) fn file_load_csv_headers(lua: &Lua, runtime: &Runtime, path: String) -> mlua::Result<Value> {
let full_path =
runtime
.dir_context()
.resolve_path(runtime.session(), path.clone().into(), PathResolver::WksDir, None)?;
let headers = crate::support::csvs::load_csv_headers(&full_path, None).map_err(|e| {
Error::from(format!(
"aip.file.load_csv_headers - Failed to read csv file '{path}'.\nCause: {e}",
))
})?;
let headers_tbl = W(headers).into_lua(lua)?;
Ok(headers_tbl)
}
pub(super) fn file_load_csv(lua: &Lua, runtime: &Runtime, path: String, options: Option<Value>) -> mlua::Result<Value> {
let full_path =
runtime
.dir_context()
.resolve_path(runtime.session(), path.clone().into(), PathResolver::WksDir, None)?;
let opts = match options {
Some(v) => CsvOptions::from_lua(v, lua)?,
None => CsvOptions::default(),
};
let csv_content = crate::support::csvs::load_csv(&full_path, Some(opts)).map_err(|e| {
Error::from(format!(
"aip.file.load_csv - Failed to read csv file '{path}'.\nCause: {e}",
))
})?;
csv_content.into_lua(lua)
}
pub(super) fn file_save_as_csv(
lua: &Lua,
runtime: &Runtime,
path: String,
data: Value,
options: Option<Value>,
) -> mlua::Result<Value> {
let dir_context = runtime.dir_context();
let full_path = dir_context.resolve_path(runtime.session(), path.clone().into(), PathResolver::WksDir, None)?;
let wks_dir = dir_context.try_wks_dir_with_err_ctx("aip.file.save_as_csv requires a aipack workspace setup")?;
check_access_write(&full_path, wks_dir)?;
let opts = match options {
Some(v) => Some(CsvOptions::from_lua(v, lua)?),
None => None,
};
let has_header = opts.as_ref().and_then(|o| o.has_header);
let content = normalize_csv_payload(lua, data, has_header)?;
crate::support::csvs::save_csv(&full_path, &content, opts).map_err(|e| {
Error::from(format!(
"aip.file.save_as_csv - Failed to save csv file '{path}'.\nCause: {e}",
))
})?;
let file_info = FileInfo::new(runtime.dir_context(), path, &full_path);
file_info.into_lua(lua)
}
pub(super) fn file_save_records_as_csv(
lua: &Lua,
runtime: &Runtime,
path: String,
records: Value,
header_keys: Vec<String>,
options: Option<Value>,
) -> mlua::Result<Value> {
let dir_context = runtime.dir_context();
let full_path = dir_context.resolve_path(runtime.session(), path.clone().into(), PathResolver::WksDir, None)?;
let wks_dir =
dir_context.try_wks_dir_with_err_ctx("aip.file.save_records_as_csv requires a aipack workspace setup")?;
check_access_write(&full_path, wks_dir)?;
let records_tbl = expect_table(records, "aip.file.save_records_as_csv", "records")?;
let mut rows = Vec::new();
for rec_val in records_tbl.sequence_values::<Value>() {
let rec_val = rec_val?;
if let Value::Table(rec) = rec_val {
let mut row = Vec::new();
for key in &header_keys {
let val = rec.get::<Value>(key.as_str())?;
row.push(lua_value_to_csv_string(val)?);
}
rows.push(row);
} else {
return Err(mlua::Error::external("Records must be a list of tables"));
}
}
let content = CsvContent {
headers: header_keys,
rows,
};
let opts = match options {
Some(v) => Some(CsvOptions::from_lua(v, lua)?),
None => None,
};
crate::support::csvs::save_csv(&full_path, &content, opts).map_err(|e| {
Error::from(format!(
"aip.file.save_records_as_csv - Failed to save csv file '{path}'.\nCause: {e}",
))
})?;
let file_info = FileInfo::new(runtime.dir_context(), path, &full_path);
file_info.into_lua(lua)
}
pub(super) fn file_append_csv_rows(
lua: &Lua,
runtime: &Runtime,
path: String,
value_lists: Value,
options: Option<Value>,
) -> mlua::Result<Value> {
let dir_context = runtime.dir_context();
let full_path = dir_context.resolve_path(runtime.session(), path.clone().into(), PathResolver::WksDir, None)?;
let lock_handle = runtime.file_write_manager().lock_for_path(&full_path);
let _guard = lock_handle.lock();
let wks_dir = dir_context.try_wks_dir_with_err_ctx("aip.file.append_csv_rows requires a aipack workspace setup")?;
check_access_write(&full_path, wks_dir)?;
let rows = match value_lists {
Value::Table(t) => lua_matrix_to_rows(t)?,
_ => return Err(mlua::Error::external("value_lists must be a table (list of lists)")),
};
let content = CsvContent {
headers: Vec::new(),
rows,
};
let opts = match options {
Some(v) => Some(CsvOptions::from_lua(v, lua)?),
None => None,
};
crate::support::csvs::append_csv(&full_path, &content, opts).map_err(|e| {
Error::from(format!(
"aip.file.append_csv_rows - Failed to append rows to csv file '{path}'.\nCause: {e}",
))
})?;
let file_info = FileInfo::new(runtime.dir_context(), path, &full_path);
file_info.into_lua(lua)
}
pub(super) fn file_append_csv_row(
lua: &Lua,
runtime: &Runtime,
path: String,
values: Value,
options: Option<Value>,
) -> mlua::Result<Value> {
let dir_context = runtime.dir_context();
let full_path = dir_context.resolve_path(runtime.session(), path.clone().into(), PathResolver::WksDir, None)?;
let lock_handle = runtime.file_write_manager().lock_for_path(&full_path);
let _guard = lock_handle.lock();
let wks_dir = dir_context.try_wks_dir_with_err_ctx("aip.file.append_csv_row requires a aipack workspace setup")?;
check_access_write(&full_path, wks_dir)?;
let row = match values {
Value::Table(t) => {
let mut r = Vec::new();
for val in t.sequence_values::<Value>() {
r.push(lua_value_to_csv_string(val?)?);
}
r
}
_ => return Err(mlua::Error::external("values must be a table (list)")),
};
let content = CsvContent {
headers: Vec::new(),
rows: vec![row],
};
let opts = match options {
Some(v) => Some(CsvOptions::from_lua(v, lua)?),
None => None,
};
crate::support::csvs::append_csv(&full_path, &content, opts).map_err(|e| {
Error::from(format!(
"aip.file.append_csv_row - Failed to append row to csv file '{path}'.\nCause: {e}",
))
})?;
let file_info = FileInfo::new(runtime.dir_context(), path, &full_path);
file_info.into_lua(lua)
}
fn normalize_csv_payload(_lua: &Lua, data: Value, has_header_opt: Option<bool>) -> mlua::Result<CsvContent> {
let has_header = has_header_opt.unwrap_or(false);
if let Value::Table(t) = &data {
let rows_val = t.get::<Value>("rows")?;
let headers_val = t.get::<Value>("headers")?;
let is_structured = !rows_val.is_nil() || !headers_val.is_nil();
if is_structured {
let headers = match headers_val {
Value::Table(ht) => {
let seq = collect_string_sequence(Value::Table(ht), "CsvContent", "headers")?;
seq.into_iter().map(|s| s.to_string_lossy()).collect()
}
Value::Nil => Vec::new(),
other => {
return Err(mlua::Error::external(format!(
"'headers' must be a table, found {}",
other.type_name()
)));
}
};
let rows = match rows_val {
Value::Table(rt) => lua_matrix_to_rows(rt)?,
Value::Nil => Vec::new(),
other => {
return Err(mlua::Error::external(format!(
"'rows' must be a table, found {}",
other.type_name()
)));
}
};
return Ok(CsvContent { headers, rows });
}
let mut rows = lua_matrix_to_rows(t.clone())?;
let headers = if has_header && !rows.is_empty() {
rows.remove(0)
} else {
Vec::new()
};
Ok(CsvContent { headers, rows })
} else {
Err(mlua::Error::external(
"Data must be a table (matrix or structured {headers, rows})",
))
}
}
#[cfg(test)]
mod tests {
type Result<T> = core::result::Result<T, Box<dyn std::error::Error>>;
use crate::_test_support::{clean_sanbox_01_tmp_file, gen_sandbox_01_temp_file_path, run_reflective_agent};
use simple_fs::read_to_string;
use value_ext::JsonValueExt as _;
#[tokio::test]
async fn test_lua_file_csv_save_as_csv_matrix_ok() -> Result<()> {
let fx_path = gen_sandbox_01_temp_file_path("test_save_as_csv_matrix.csv");
let fx_lua = format!(
r#"
local data = {{
{{"name", "age"}},
{{"Alice", 30}},
{{"Bob", 25}}
}}
return aip.file.save_as_csv("{fx_path}", data, {{has_header = true}})
"#
);
let res = run_reflective_agent(&fx_lua, None).await?;
assert_eq!(res.x_get_str("path")?, fx_path.as_str());
let content = read_to_string(format!("tests-data/sandbox-01/{fx_path}"))?;
let lines: Vec<&str> = content.lines().collect();
assert_eq!(lines.len(), 3);
assert_eq!(lines[0], "name,age");
assert_eq!(lines[1], "Alice,30");
assert_eq!(lines[2], "Bob,25");
clean_sanbox_01_tmp_file(fx_path)?;
Ok(())
}
#[tokio::test]
async fn test_lua_file_csv_append_csv_rows_ok() -> Result<()> {
let fx_path = gen_sandbox_01_temp_file_path("test_append_csv_rows.csv");
let fx_lua_header = format!(
r#"
aip.file.save_as_csv("{fx_path}", {{headers={{"name", "age"}}, rows={{}}}})
"#
);
run_reflective_agent(&fx_lua_header, None).await?;
let fx_lua = format!(
r#"
local data = {{
{{"Alice", 30}},
{{"Bob", 25}}
}}
-- has_header should be ignored for append_csv_rows
return aip.file.append_csv_rows("{fx_path}", data, {{has_header = true}})
"#
);
run_reflective_agent(&fx_lua, None).await?;
let content = read_to_string(format!("tests-data/sandbox-01/{fx_path}"))?;
let lines: Vec<&str> = content.lines().collect();
assert_eq!(lines.len(), 3);
assert_eq!(lines[0], "name,age");
assert_eq!(lines[1], "Alice,30");
assert_eq!(lines[2], "Bob,25");
clean_sanbox_01_tmp_file(fx_path)?;
Ok(())
}
#[tokio::test]
async fn test_lua_file_csv_append_csv_row_ok() -> Result<()> {
let fx_path = gen_sandbox_01_temp_file_path("test_append_csv_row.csv");
let fx_lua = format!(
r#"
return aip.file.append_csv_row("{fx_path}", {{"Alice", 30}})
"#
);
run_reflective_agent(&fx_lua, None).await?;
let content = read_to_string(format!("tests-data/sandbox-01/{fx_path}"))?;
let lines: Vec<&str> = content.lines().collect();
assert_eq!(lines.len(), 1);
assert_eq!(lines[0], "Alice,30");
clean_sanbox_01_tmp_file(fx_path)?;
Ok(())
}
#[tokio::test]
async fn test_lua_file_csv_save_as_csv_only_headers() -> Result<()> {
let fx_path = gen_sandbox_01_temp_file_path("test_save_as_csv_only_headers.csv");
let fx_lua = format!(
r#"
local data = {{
headers = {{"name", "age"}}
}}
return aip.file.save_as_csv("{fx_path}", data)
"#
);
run_reflective_agent(&fx_lua, None).await?;
let content = read_to_string(format!("tests-data/sandbox-01/{fx_path}"))?;
let lines: Vec<&str> = content.lines().collect();
assert_eq!(lines.len(), 1);
assert_eq!(lines[0], "name,age");
clean_sanbox_01_tmp_file(fx_path)?;
Ok(())
}
#[tokio::test]
async fn test_lua_file_csv_save_as_csv_only_rows() -> Result<()> {
let fx_path = gen_sandbox_01_temp_file_path("test_save_as_csv_only_rows.csv");
let fx_lua = format!(
r#"
local data = {{
rows = {{ {{"Alice", 30}} }}
}}
return aip.file.save_as_csv("{fx_path}", data)
"#
);
run_reflective_agent(&fx_lua, None).await?;
let content = read_to_string(format!("tests-data/sandbox-01/{fx_path}"))?;
let lines: Vec<&str> = content.lines().collect();
assert_eq!(lines.len(), 1);
assert_eq!(lines[0], "Alice,30");
clean_sanbox_01_tmp_file(fx_path)?;
Ok(())
}
#[tokio::test]
async fn test_lua_file_csv_save_as_csv_empty_table() -> Result<()> {
let fx_path = gen_sandbox_01_temp_file_path("test_save_as_csv_empty.csv");
let fx_lua = format!(
r#"
local data = {{}}
return aip.file.save_as_csv("{fx_path}", data)
"#
);
run_reflective_agent(&fx_lua, None).await?;
assert!(std::path::Path::new(&format!("tests-data/sandbox-01/{fx_path}")).exists());
let content = read_to_string(format!("tests-data/sandbox-01/{fx_path}"))?;
assert!(content.is_empty());
clean_sanbox_01_tmp_file(fx_path)?;
Ok(())
}
}