use crate::Result;
use crate::runtime::Runtime;
use crate::script::lua_helpers::lua_value_to_serde_value;
use crate::support::W;
use crate::types::CsvOptions;
use mlua::{FromLua as _, IntoLua as _, Lua, Table, Value};
pub fn lua_value_to_csv_string(value: Value) -> mlua::Result<String> {
match value {
Value::String(s) => Ok(s.to_str()?.to_string()),
Value::Integer(i) => Ok(i.to_string()),
Value::Number(n) => Ok(n.to_string()),
Value::Boolean(b) => Ok(b.to_string()), Value::Nil => Ok("".to_string()),
Value::Table(t) => {
let serde_val = lua_value_to_serde_value(Value::Table(t)).map_err(mlua::Error::external)?;
serde_json::to_string(&serde_val).map_err(mlua::Error::external)
}
other => {
if other == Value::NULL {
Ok("".to_string())
} else {
Err(mlua::Error::external(format!(
"unsupported value type '{}'",
other.type_name()
)))
}
}
}
}
pub fn lua_matrix_to_rows(matrix: Table) -> mlua::Result<Vec<Vec<String>>> {
let mut rows = Vec::new();
for row_val in matrix.sequence_values::<Value>() {
let row_val = row_val?;
if let Value::Table(row_tbl) = row_val {
let mut row = Vec::new();
for cell_val in row_tbl.sequence_values::<Value>() {
row.push(lua_value_to_csv_string(cell_val?)?);
}
rows.push(row);
}
}
Ok(rows)
}
pub fn init_module(lua: &Lua, _runtime: &Runtime) -> Result<Table> {
let table = lua.create_table()?;
let parse_row_fn =
lua.create_function(|lua, (row, opts): (String, Option<Value>)| lua_parse_row(lua, row, opts))?;
let parse_fn =
lua.create_function(|lua, (content, opts): (String, Option<Value>)| lua_parse(lua, content, opts))?;
let values_to_row_fn =
lua.create_function(|lua, (values, opts): (Value, Option<Value>)| lua_values_to_row(lua, values, opts))?;
let value_lists_to_rows_fn =
lua.create_function(|lua, (lists, opts): (Value, Option<Value>)| lua_value_lists_to_rows(lua, lists, opts))?;
table.set("parse_row", parse_row_fn)?;
table.set("parse", parse_fn)?;
table.set("values_to_row", values_to_row_fn)?;
table.set("value_lists_to_rows", value_lists_to_rows_fn)?;
Ok(table)
}
fn lua_parse_row(lua: &Lua, row: String, opts_val: Option<Value>) -> mlua::Result<Table> {
let opts = match opts_val {
Some(v) => CsvOptions::from_lua(v, lua)?,
None => CsvOptions::default(),
};
let row_vec = crate::support::csvs::parse_csv_row(&row, Some(opts))?;
let table = W(row_vec)
.into_lua(lua)
.map_err(|e| mlua::Error::external(format!("Failed to convert row to lua table: {e}")))?;
match table {
Value::Table(t) => Ok(t),
_ => Err(mlua::Error::external("Expected a table")),
}
}
fn lua_parse(lua: &Lua, content: String, opts_val: Option<Value>) -> mlua::Result<Value> {
let opts = match opts_val {
Some(v) => CsvOptions::from_lua(v, lua)?,
None => CsvOptions::default(),
};
let csv_content = crate::support::csvs::parse_csv(&content, Some(opts))?;
csv_content.into_lua(lua)
}
fn lua_values_to_row(lua: &Lua, values: Value, opts_val: Option<Value>) -> mlua::Result<String> {
let opts = match opts_val {
Some(v) => Some(CsvOptions::from_lua(v, lua)?),
None => None,
};
values_to_row_inner(values, opts, "aip.csv.values_to_row")
}
fn lua_value_lists_to_rows(lua: &Lua, value_lists: Value, opts_val: Option<Value>) -> mlua::Result<Vec<String>> {
let table = match value_lists {
Value::Table(t) => t,
_ => {
return Err(mlua::Error::external(
"aip.csv.value_lists_to_rows - value_lists must be a table (list of lists)",
));
}
};
let opts = match opts_val {
Some(v) => Some(CsvOptions::from_lua(v, lua)?),
None => None,
};
let mut rows = Vec::new();
for (idx, item) in table.sequence_values::<Value>().enumerate() {
let item = item?;
let row = values_to_row_inner(item, opts.clone(), "aip.csv.value_lists_to_rows")
.map_err(|e| mlua::Error::external(format!("Row {}: {}", idx + 1, e)))?;
rows.push(row);
}
Ok(rows)
}
fn values_to_row_inner(values: Value, opts: Option<CsvOptions>, ctx: &str) -> mlua::Result<String> {
let table = match values {
Value::Table(t) => t,
_ => {
return Err(mlua::Error::external(format!("{ctx} - values must be a table (list)")));
}
};
let mut row_values = Vec::new();
for value in table.sequence_values::<Value>() {
let value = value?;
let s = lua_value_to_csv_string(value).map_err(|e| mlua::Error::external(format!("{ctx} - {e}")))?;
row_values.push(s);
}
let csv_row = crate::support::csvs::values_to_csv_row(&row_values, opts).map_err(mlua::Error::external)?;
Ok(csv_row)
}
#[cfg(test)]
mod tests {
type Result<T> = core::result::Result<T, Box<dyn std::error::Error>>;
use crate::_test_support::{eval_lua, setup_lua};
use crate::script::aip_modules::aip_csv;
use value_ext::JsonValueExt as _;
#[tokio::test]
async fn test_aip_csv_parse_row_simple() -> Result<()> {
let lua = setup_lua(aip_csv::init_module, "csv").await?;
let res = eval_lua(
&lua,
r#"
local row = 'a,"b,c",d'
return aip.csv.parse_row(row)
"#,
)?;
assert_eq!(res.x_get_str("/0")?, "a");
assert_eq!(res.x_get_str("/1")?, "b,c");
assert_eq!(res.x_get_str("/2")?, "d");
Ok(())
}
#[tokio::test]
async fn test_aip_csv_parse_with_header_and_comments() -> Result<()> {
let lua = setup_lua(aip_csv::init_module, "csv").await?;
let script = r##"
local content = [[
# comment
name,age
John,30
Jane,25
]]
local res = aip.csv.parse(content, { has_header = true, comment = "#", skip_empty_lines = true })
return res
"##;
let res = eval_lua(&lua, script)?;
assert_eq!(res.x_get_str("/headers/0")?, "name");
assert_eq!(res.x_get_str("/headers/1")?, "age");
assert_eq!(res.x_get_str("/rows/0/0")?, "John");
assert_eq!(res.x_get_str("/rows/0/1")?, "30");
assert_eq!(res.x_get_str("/rows/1/0")?, "Jane");
assert_eq!(res.x_get_str("/rows/1/1")?, "25");
Ok(())
}
#[tokio::test]
async fn test_aip_csv_values_to_row() -> Result<()> {
let lua = setup_lua(aip_csv::init_module, "csv").await?;
let res = eval_lua(&lua, r#"return aip.csv.values_to_row({"a", 123, true, nil})"#)?;
let s = res.as_str().ok_or("Should be string")?;
assert_eq!(s, "a,123,true");
let res = eval_lua(&lua, r#"return aip.csv.values_to_row({"a,b", 'c "d"'})"#)?;
let s = res.as_str().ok_or("Should be string")?;
assert_eq!(s, "\"a,b\",\"c \"\"d\"\"\"");
let res = eval_lua(&lua, r#"return aip.csv.values_to_row({"a", "b"}, {delimiter = ";"})"#)?;
let s = res.as_str().ok_or("Should be string")?;
assert_eq!(s, "a;b");
let res = eval_lua(&lua, r#"return aip.csv.values_to_row({"a", {b=1}})"#)?;
let s = res.as_str().ok_or("Should be string")?;
assert!(s.starts_with("a,"));
assert!(s.contains(r#"{""b"":1}"#));
Ok(())
}
#[tokio::test]
async fn test_aip_csv_values_to_row_special_chars() -> Result<()> {
let lua = setup_lua(aip_csv::init_module, "csv").await?;
let script = r#"
local val = {"line\nbreak", 12.34, ""}
return aip.csv.values_to_row(val)
"#;
let res = eval_lua(&lua, script)?;
let s = res.as_str().ok_or("Should be string")?;
assert_eq!(s, "\"line\nbreak\",12.34,");
Ok(())
}
#[tokio::test]
async fn test_aip_csv_values_to_row_error() -> Result<()> {
let lua = setup_lua(aip_csv::init_module, "csv").await?;
let script = r#"
local val = {"a", function() end}
return aip.csv.values_to_row(val)
"#;
let res = eval_lua(&lua, script);
assert!(res.is_err());
let err = res.err().ok_or("Should have err")?;
assert!(err.to_string().contains("unsupported value type 'function'"));
Ok(())
}
#[tokio::test]
async fn test_aip_csv_value_lists_to_rows() -> Result<()> {
let lua = setup_lua(aip_csv::init_module, "csv").await?;
let script = r#"
local lists = {
{"a", 1},
{"b,c", 2}
}
return aip.csv.value_lists_to_rows(lists)
"#;
let res = eval_lua(&lua, script)?;
let rows = res.as_array().ok_or("not an array")?;
assert_eq!(rows.len(), 2);
let r1 = rows[0].as_str().ok_or("Should have at least one row")?;
assert_eq!(r1, "a,1");
let r2 = rows[1].as_str().ok_or("Should have second row")?;
assert_eq!(r2, "\"b,c\",2");
Ok(())
}
#[tokio::test]
async fn test_aip_csv_value_lists_to_rows_with_options() -> Result<()> {
let lua = setup_lua(aip_csv::init_module, "csv").await?;
let script = r#"
local lists = {
{"a", 1},
{"b", 2}
}
return aip.csv.value_lists_to_rows(lists, {delimiter = "|"})
"#;
let res = eval_lua(&lua, script)?;
let rows = res.as_array().ok_or("not an array")?;
assert_eq!(rows.len(), 2);
let r1 = rows[0].as_str().ok_or("Should have at least one row")?;
assert_eq!(r1, "a|1");
let r2 = rows[1].as_str().ok_or("Should have second row")?;
assert_eq!(r2, "b|2");
Ok(())
}
#[tokio::test]
async fn test_aip_csv_parse_with_header_labels() -> Result<()> {
let lua = setup_lua(aip_csv::init_module, "csv").await?;
let script = r#"
local content = "ID,Full Name,Age\n1,Alice,30"
local opts = {
header_labels = {
id = "ID",
name = "Full Name"
}
}
return aip.csv.parse(content, opts)
"#;
let res = eval_lua(&lua, script)?;
assert_eq!(res.x_get_str("/headers/0")?, "id");
assert_eq!(res.x_get_str("/headers/1")?, "name");
assert_eq!(res.x_get_str("/headers/2")?, "Age");
assert_eq!(res.x_get_str("/rows/0/0")?, "1");
assert_eq!(res.x_get_str("/rows/0/1")?, "Alice");
assert_eq!(res.x_get_str("/rows/0/2")?, "30");
Ok(())
}
}