use crate::Error;
use crate::dir_context::PathResolver;
use crate::runtime::Runtime;
use crate::script::aip_modules::support::check_access_write;
use crate::script::{lua_value_list_to_serde_values, lua_value_to_serde_value, serde_value_to_lua_value};
use crate::support::jsons;
use crate::types::FileInfo;
use mlua::{IntoLua, Lua, Value};
use simple_fs::ensure_file_dir;
pub(super) fn file_load_json(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 json_value = jsons::load_json_to_serde_value(&full_path).map_err(|e| {
Error::from(format!(
"aip.file.load_json - Failed to read json file '{path}'.\nCause: {e}",
))
})?;
let json_value = json_value.unwrap_or_default();
let lua_value = serde_value_to_lua_value(lua, json_value)?;
Ok(lua_value)
}
pub(super) fn file_load_ndjson(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 json_values = simple_fs::load_ndjson(full_path).map_err(|e| {
Error::from(format!(
"aip.file.load_ndjson - Failed to load newline json file '{path}'.\nCause: {e}",
))
})?;
let json_value = serde_json::Value::Array(json_values);
let lua_values = serde_value_to_lua_value(lua, json_value)?;
Ok(lua_values)
}
pub(super) fn file_append_json_line(lua: &Lua, runtime: &Runtime, path: String, data: 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_json_line requires a aipack workspace setup")?;
check_access_write(&full_path, wks_dir)?;
let json_value = lua_value_to_serde_value(data).map_err(|e| {
Error::from(format!(
"aip.file.append_json_line - Failed to convert Lua data to JSON for file '{path}'.\nCause: {e}",
))
})?;
ensure_file_dir(&full_path).map_err(Error::from)?;
simple_fs::append_json_line(full_path.clone(), &json_value).map_err(|e| {
Error::from(format!(
"aip.file.append_json_line - Failed to append json line to '{path}'.\nCause: {e}",
))
})?;
let file_info = FileInfo::new(runtime.dir_context(), path, &full_path);
file_info.into_lua(lua)
}
pub(super) fn file_append_json_lines(lua: &Lua, runtime: &Runtime, path: String, data: Value) -> mlua::Result<Value> {
let json_values = lua_value_list_to_serde_values(data).map_err(|e| {
Error::from(format!(
"aip.file.append_json_lines - Failed to append json lines to '{path}'.\nCause: {e}",
))
})?;
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_json_lines requires a aipack workspace setup")?;
check_access_write(&full_path, wks_dir)?;
ensure_file_dir(&full_path).map_err(Error::from)?;
simple_fs::append_json_lines(full_path.clone(), &json_values).map_err(|e| {
Error::from(format!(
"aip.file.append_json_lines - Failed to append json line to '{path}'.\nCause: {e}",
))
})?;
let file_info = FileInfo::new(runtime.dir_context(), path, &full_path);
file_info.into_lua(lua)
}
#[cfg(test)]
mod tests {
type Result<T> = core::result::Result<T, Box<dyn std::error::Error>>;
use crate::_test_support::{
assert_contains, clean_sanbox_01_tmp_file, create_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_load_json_ok() -> Result<()> {
let fx_path = "other/test_load_json.json";
let res = run_reflective_agent(&format!(r#"return aip.file.load_json("{fx_path}")"#), None).await?;
assert_eq!(res.x_get_str("name")?, "Test JSON");
assert_eq!(res.x_get_f64("version")?, 1.2);
assert!(res.x_get_bool("enabled")?, "enabled should be true");
let items = res
.get("items")
.ok_or("should have items")?
.as_array()
.ok_or("should be array")?;
assert_eq!(items.len(), 2);
assert_eq!(items[0].as_str().ok_or("should have item")?, "item1");
assert_eq!(items[1].as_str().ok_or("should have item")?, "item2");
let nested: &serde_json::Value = res.get("nested").ok_or("should have nested")?;
assert_eq!(nested.x_get_str("key")?, "value");
let nullable = res.get("nullable").ok_or("should have nullable")?;
assert!(nullable.is_null(), "nullable should be json null");
Ok(())
}
#[tokio::test]
async fn test_lua_file_load_json_with_comment_and_trailing() -> Result<()> {
let fx_file = create_sanbox_01_tmp_file(
"test_lua_file_load_json_with_comment_and_trailing.ndjson",
r#"
{
// Here are smoe comment
"name": "Test JSON",
"version": 1.2,
"enabled": true,
"items": [
"item1",
"item2"
],
"nested": {
"key": "value"
},
// Trailing comma
"nullable": null,
}
"#,
)?;
let fx_path = fx_file.as_str();
let res = run_reflective_agent(&format!(r#"return aip.file.load_json("{fx_path}")"#), None).await?;
assert_eq!(res.x_get_str("name")?, "Test JSON");
assert_eq!(res.x_get_f64("version")?, 1.2);
assert!(res.x_get_bool("enabled")?, "enabled should be true");
let items = res
.get("items")
.ok_or("should have items")?
.as_array()
.ok_or("should be array")?;
assert_eq!(items.len(), 2);
assert_eq!(items[0].as_str().ok_or("should have item")?, "item1");
assert_eq!(items[1].as_str().ok_or("should have item")?, "item2");
let nested: &serde_json::Value = res.get("nested").ok_or("should have nested")?;
assert_eq!(nested.x_get_str("key")?, "value");
let nullable = res.get("nullable").ok_or("should have nullable")?;
assert!(nullable.is_null(), "nullable should be json null");
Ok(())
}
#[tokio::test]
async fn test_lua_file_load_json_file_not_found() -> Result<()> {
let fx_path = "other/non_existent_file.json";
let res = run_reflective_agent(&format!(r#"return aip.file.load_json("{fx_path}")"#), None).await;
let Err(err) = res else {
panic!("Should have returned an error");
};
assert_contains(&err.to_string(), "aip.file.load_json - Failed to read json file");
assert_contains(&err.to_string(), "non_existent_file.json");
Ok(())
}
#[tokio::test]
async fn test_lua_file_load_json_invalid_json() -> Result<()> {
let fx_path = "file-01.txt";
let res = run_reflective_agent(&format!(r#"return aip.file.load_json("{fx_path}")"#), None).await;
let Err(err) = res else {
panic!("Should have returned an error");
};
let err = err.to_string();
assert_contains(&err, "aip.file.load_json - Failed to read json");
assert_contains(&err, fx_path);
Ok(())
}
#[tokio::test]
async fn test_lua_file_load_jsonnd_ok() -> Result<()> {
let fx_path = "other/test_load_ndjson.ndjson";
let res = run_reflective_agent(&format!(r#"return aip.file.load_ndjson("{fx_path}")"#), None).await?;
let arr = res.as_array().ok_or("Result should be an array")?;
assert_eq!(arr.len(), 3, "Should have 3 items from the ndjson file");
let item1 = arr.first().ok_or("Should have item 1")?;
assert_eq!(item1.x_get_str("name")?, "item1");
assert_eq!(item1.x_get_i64("value")?, 10);
let item2 = arr.get(1).ok_or("Should have item 2")?;
assert_eq!(item2.x_get_str("name")?, "item2");
assert_eq!(item2.x_get_i64("value")?, 20);
assert!(item2.x_get_bool("active")?);
let item3 = arr.get(2).ok_or("Should have item 3")?;
assert_eq!(item3.x_get_str("name")?, "item3");
assert!(item3.get("value").ok_or("item3 should have value")?.is_null());
let tags = item3
.get("tags")
.ok_or("item3 should have tags")?
.as_array()
.ok_or("tags should be array")?;
assert_eq!(tags.len(), 2);
assert_eq!(tags[0].as_str().ok_or("tag should be string")?, "a");
assert_eq!(tags[1].as_str().ok_or("tag should be string")?, "b");
Ok(())
}
#[tokio::test]
async fn test_lua_file_load_jsonnd_file_not_found() -> Result<()> {
let fx_path = "other/non_existent_file.ndjson";
let res = run_reflective_agent(&format!(r#"return aip.file.load_ndjson("{fx_path}")"#), None).await;
let Err(err) = res else {
panic!("Should have returned an error");
};
assert_contains(
&err.to_string(),
"aip.file.load_ndjson - Failed to load newline json file",
);
assert_contains(&err.to_string(), "non_existent_file.ndjson");
Ok(())
}
#[tokio::test]
async fn test_lua_file_load_jsonnd_invalid_json_line() -> Result<()> {
let fx_file = create_sanbox_01_tmp_file(
"test_lua_file_load_jsonnd_invalid_json_line.ndjson",
r#"{"valid": true}
invalid json line here
{"another_valid": 123}
"#,
)?;
let fx_path = fx_file.as_str();
let res = run_reflective_agent(&format!(r#"return aip.file.load_ndjson("{fx_path}")"#), None).await;
let Err(err) = res else {
panic!("Should have returned an error");
};
assert_contains(
&err.to_string(),
"aip.file.load_ndjson - Failed to parse JSON on line 2",
);
assert_contains(&err.to_string(), fx_path);
clean_sanbox_01_tmp_file(fx_file)?;
Ok(())
}
#[tokio::test]
async fn test_lua_file_load_jsonnd_empty_file() -> Result<()> {
let fx_file = create_sanbox_01_tmp_file("test_lua_file_load_jsonnd_empty_file.ndjson", "")?;
let fx_path = fx_file.as_str();
let res = run_reflective_agent(&format!(r#"return aip.file.load_ndjson("{fx_path}")"#), None).await?;
let arr = res.as_array().ok_or("Result should be an array")?;
assert_eq!(arr.len(), 0, "Should have 0 items from an empty file");
clean_sanbox_01_tmp_file(fx_file)?;
Ok(())
}
#[tokio::test]
async fn test_lua_file_load_jsonnd_empty_lines_file() -> Result<()> {
let fx_file = create_sanbox_01_tmp_file(
"test_lua_file_load_jsonnd_empty_lines_file.ndjson",
r#"
{"valid": true}
{"another": "valid"}
"#,
)?;
let fx_path = fx_file.as_str();
let res = run_reflective_agent(&format!(r#"return aip.file.load_ndjson("{fx_path}")"#), None).await?;
let arr = res.as_array().ok_or("Result should be an array")?;
assert_eq!(arr.len(), 2, "Should have 2 items, skipping empty lines");
assert!(arr[0].x_get_bool("valid")?);
assert_eq!(arr[1].x_get_str("another")?, "valid");
clean_sanbox_01_tmp_file(fx_file)?;
Ok(())
}
#[tokio::test]
async fn test_lua_file_append_json_line_new_file_ok() -> Result<()> {
let fix_file = gen_sandbox_01_temp_file_path("test_lua_file_append_json_line_new_file.ndjson");
let fx_path = fix_file.as_str();
let fx_data1 = r#"{name = "item1", value = 123}"#;
let fx_data2 = r#"{name = "item2", active = true, tags = {"a", "b"}}"#;
let res1 = run_reflective_agent(
&format!(r#"return aip.file.append_json_line("{fx_path}", {fx_data1})"#),
None,
)
.await?;
let res2 = run_reflective_agent(
&format!(r#"return aip.file.append_json_line("{fx_path}", {fx_data2})"#),
None,
)
.await?;
assert_eq!(res1.x_get_str("path")?, fx_path);
assert_eq!(res2.x_get_str("path")?, fx_path);
assert!(res2.x_get_i64("size")? > res1.x_get_i64("size")?);
let full_path = format!("tests-data/sandbox-01/{fx_path}");
let content = read_to_string(&full_path)?;
let lines: Vec<&str> = content.lines().collect();
assert_eq!(lines.len(), 2, "Should have 2 lines");
assert_eq!(lines[0], r#"{"name":"item1","value":123}"#);
assert_eq!(lines[1], r#"{"active":true,"name":"item2","tags":["a","b"]}"#);
Ok(())
}
#[tokio::test]
async fn test_lua_file_append_json_line_existing_file_ok() -> Result<()> {
let fx_file_name = "test_lua_file_append_json_line_existing_file.ndjson";
let initial_content = r#"{"initial": true}
"#; let fx_file = create_sanbox_01_tmp_file(fx_file_name, initial_content)?;
let fx_path = fx_file.as_str();
let fx_data = r#"{appended = "yes", value = nil}"#;
let res = run_reflective_agent(
&format!(r#"return aip.file.append_json_line("{fx_path}", {fx_data})"#),
None,
)
.await?;
assert_eq!(res.x_get_str("path")?, fx_path);
assert!(res.x_get_i64("size")? > initial_content.len() as i64);
let full_path = format!("tests-data/sandbox-01/{fx_path}");
let content = read_to_string(&full_path)?;
let lines: Vec<&str> = content.lines().collect();
assert_eq!(lines.len(), 2, "Should have 2 lines (initial + appended)");
assert_eq!(lines[0], r#"{"initial": true}"#);
assert_eq!(lines[1], r#"{"appended":"yes"}"#);
Ok(())
}
#[tokio::test]
async fn test_lua_file_append_json_lines_new_file_ok() -> Result<()> {
let fix_file = gen_sandbox_01_temp_file_path("test_lua_file_append_json_lines_new_file.ndjson");
let fx_path = fix_file.as_str();
let fx_data = r#"
{
{name = "line1", value = 1},
{name = "line2", active = true},
{name = "line3", tags = {"c", "d"}, data = nil}
}
"#;
let res = run_reflective_agent(
&format!(r#"return aip.file.append_json_lines("{fx_path}", {fx_data})"#),
None,
)
.await?;
assert_eq!(res.x_get_str("path")?, fx_path);
assert!(res.x_get_i64("size")? > 0);
let full_path = format!("tests-data/sandbox-01/{fx_path}");
let content = read_to_string(&full_path)?;
let lines: Vec<&str> = content.lines().collect();
assert_eq!(lines.len(), 3, "Should have 3 lines");
assert_eq!(lines[0], r#"{"name":"line1","value":1}"#);
assert_eq!(lines[1], r#"{"active":true,"name":"line2"}"#);
assert_eq!(lines[2], r#"{"name":"line3","tags":["c","d"]}"#);
Ok(())
}
#[tokio::test]
async fn test_lua_file_append_json_lines_existing_file_ok() -> Result<()> {
let fx_file_name = "test_lua_file_append_json_lines_existing_file.ndjson";
let initial_content = r#"{"initial": true}
"#; let fx_file = create_sanbox_01_tmp_file(fx_file_name, initial_content)?;
let fx_path = fx_file.as_str();
let fx_data = r#"
{
{appended = "yes"},
{another = 123}
}
"#;
let res = run_reflective_agent(
&format!(r#"return aip.file.append_json_lines("{fx_path}", {fx_data})"#),
None,
)
.await?;
assert_eq!(res.x_get_str("path")?, fx_path);
assert!(res.x_get_i64("size")? > initial_content.len() as i64);
let full_path = format!("tests-data/sandbox-01/{fx_path}");
let content = read_to_string(&full_path)?;
let lines: Vec<&str> = content.lines().collect();
assert_eq!(lines.len(), 3, "Should have 3 lines (initial + 2 appended)");
assert_eq!(lines[0], r#"{"initial": true}"#);
assert_eq!(lines[1], r#"{"appended":"yes"}"#);
assert_eq!(lines[2], r#"{"another":123}"#);
Ok(())
}
#[tokio::test]
async fn test_lua_file_append_json_lines_empty_list_ok() -> Result<()> {
let fix_file = gen_sandbox_01_temp_file_path("test_lua_file_append_json_lines_empty_list.ndjson");
let fx_path = fix_file.as_str();
let fx_data = r#"{}"#;
let res = run_reflective_agent(
&format!(r#"return aip.file.append_json_lines("{fx_path}", {fx_data})"#),
None,
)
.await?;
assert_eq!(res.x_get_str("path")?, fx_path);
assert_eq!(res.x_get_i64("size")?, 0);
let full_path = format!("tests-data/sandbox-01/{fx_path}");
let content = read_to_string(&full_path)?;
assert_eq!(content, "", "File should be empty");
Ok(())
}
#[tokio::test]
async fn test_lua_file_append_json_lines_buffering_ok() -> Result<()> {
let fix_file = gen_sandbox_01_temp_file_path("test_lua_file_append_json_lines_buffering.ndjson");
let fx_path = fix_file.as_str();
let mut lua_list = String::from("{");
for i in 0..(100 + 5) {
lua_list.push_str(&format!(r#"{{idx = {i}, name = "name-{i}""#));
if i % 10 == 0 {
lua_list.push_str(", optional = nil");
}
lua_list.push_str("},");
}
lua_list.push('}');
let res = run_reflective_agent(
&format!(r#"return aip.file.append_json_lines("{fx_path}", {lua_list})"#),
None,
)
.await?;
assert_eq!(res.x_get_str("path")?, fx_path);
assert!(res.x_get_i64("size")? > 0);
let full_path = format!("tests-data/sandbox-01/{fx_path}");
let content = read_to_string(&full_path)?;
let lines: Vec<&str> = content.lines().collect();
assert_eq!(lines.len(), 100 + 5, "Should have correct number of lines");
assert_eq!(lines[0], r#"{"idx":0,"name":"name-0"}"#); assert_eq!(
lines.last().ok_or("Should have last")?,
&format!("{{\"idx\":{idx},\"name\":\"name-{idx}\"}}", idx = 100 + 4)
);
Ok(())
}
#[tokio::test]
async fn test_lua_file_append_json_lines_err_not_a_table() -> Result<()> {
let fix_file = gen_sandbox_01_temp_file_path("test_lua_file_append_json_lines_err_not_table.ndjson");
let fx_path = fix_file.as_str();
let fx_data = r#""just a string""#;
let result =
run_reflective_agent(&format!(r#"aip.file.append_json_lines("{fx_path}", {fx_data})"#), None).await;
let Err(err) = result else {
panic!("Should have returned an error");
};
assert_contains(
&err.to_string(),
"aip.file.append_json_lines - Failed to append json lines",
);
assert_contains(&err.to_string(), "but got string");
let _ = clean_sanbox_01_tmp_file(fix_file);
Ok(())
}
}