use crate::dir_context::PathResolver;
use crate::runtime::Runtime;
use crate::script::LuaValueExt;
use crate::script::aip_modules::support::{
base_dir_and_globs, compute_base_dir, create_file_records, list_files_with_options,
};
use crate::script::support::into_option_string;
use crate::support::AsStrsExt;
use crate::types::{FileInfo, FileRecord, FileStats};
use mlua::{IntoLua, Lua, Value};
use simple_fs::{SMeta, SPath, iter_files};
pub(super) fn file_stats(
lua: &Lua,
runtime: &Runtime,
include_globs: Value,
options: Option<Value>,
) -> mlua::Result<Value> {
if include_globs.is_nil() {
return Ok(Value::Nil);
}
let (base_path, include_globs) = base_dir_and_globs(runtime, include_globs, options.as_ref())?;
let absolute = options.x_get_bool("absolute").unwrap_or(false);
let file_refs = list_files_with_options(runtime, base_path.as_ref(), &include_globs.x_as_strs(), absolute, false)?;
if file_refs.is_empty() {
return FileStats::default().into_lua(lua);
}
let smetas: Vec<&SMeta> = file_refs.iter().filter_map(|f_ref| f_ref.meta()).collect();
let mut total_size: u64 = 0;
let mut number_of_files: u64 = 0;
let mut ctime_first: Option<i64> = None;
let mut ctime_last: Option<i64> = None;
let mut mtime_first: Option<i64> = None;
let mut mtime_last: Option<i64> = None;
for smeta in smetas {
number_of_files += 1;
total_size += smeta.size;
let ctime = smeta.created_epoch_us;
let mtime = smeta.modified_epoch_us;
ctime_first = Some(match ctime_first {
Some(v) => v.min(ctime),
None => ctime,
});
ctime_last = Some(match ctime_last {
Some(v) => v.max(ctime),
None => ctime,
});
mtime_first = Some(match mtime_first {
Some(v) => v.min(mtime),
None => mtime,
});
mtime_last = Some(match mtime_last {
Some(v) => v.max(mtime),
None => mtime,
});
}
let file_stats = FileStats {
total_size,
number_of_files,
ctime_first: ctime_first.unwrap_or(0),
ctime_last: ctime_last.unwrap_or(0),
mtime_first: mtime_first.unwrap_or(0),
mtime_last: mtime_last.unwrap_or(0),
};
let res = file_stats.into_lua(lua)?;
Ok(res)
}
pub(super) fn file_load(
lua: &Lua,
runtime: &Runtime,
rel_path: String,
options: Option<Value>,
) -> mlua::Result<mlua::Value> {
let dir_context = runtime.dir_context();
let base_path = compute_base_dir(runtime, options.as_ref())?;
let full_path = dir_context.resolve_path(
runtime.session(),
(&rel_path).into(),
PathResolver::WksDir,
base_path.as_ref(),
)?;
let full_path = match (base_path, full_path.is_absolute()) {
(Some(base_path), false) => base_path.join(full_path),
_ => full_path,
};
let rel_path = SPath::new(rel_path);
let file_record = FileRecord::load_from_full_path(runtime.dir_context(), &full_path, rel_path)?;
let res = file_record.into_lua(lua)?;
Ok(res)
}
pub(super) fn file_exists(_lua: &Lua, runtime: &Runtime, path: String) -> mlua::Result<bool> {
Ok(crate::script::support::path_exists(runtime, &path))
}
pub(super) fn file_info(lua: &Lua, runtime: &Runtime, path: Value) -> mlua::Result<Value> {
let Some(path) = into_option_string(path, "aip.file.info")? else {
return Ok(Value::Nil);
};
if path.trim().is_empty() {
return Ok(Value::Nil);
}
let rel_path = SPath::new(path);
let full_path =
runtime
.dir_context()
.resolve_path(runtime.session(), rel_path.clone(), PathResolver::WksDir, None)?;
if !full_path.is_file() {
return Ok(Value::Nil);
}
let file_info = FileInfo::new(runtime.dir_context(), rel_path, &full_path);
file_info.into_lua(lua)
}
pub(super) fn file_list(
lua: &Lua,
runtime: &Runtime,
include_globs: Value,
options: Option<Value>,
) -> mlua::Result<Value> {
let (base_path, include_globs) = base_dir_and_globs(runtime, include_globs, options.as_ref())?;
let absolute = options.x_get_bool("absolute").unwrap_or(false);
let spaths = list_files_with_options(runtime, base_path.as_ref(), &include_globs.x_as_strs(), absolute, true)?;
let file_infos: Vec<FileInfo> = spaths
.into_iter()
.map(|f_ref| FileInfo::from_file_ref(runtime.dir_context(), f_ref))
.collect();
let res = file_infos.into_lua(lua)?;
Ok(res)
}
pub(super) fn file_list_load(
lua: &Lua,
runtime: &Runtime,
include_globs: Value,
options: Option<Value>,
) -> mlua::Result<Value> {
let (base_path, include_globs) = base_dir_and_globs(runtime, include_globs, options.as_ref())?;
let absolute = options.x_get_bool("absolute").unwrap_or(false);
let file_refs = list_files_with_options(runtime, base_path.as_ref(), &include_globs.x_as_strs(), absolute, true)?;
let file_records = create_file_records(runtime, file_refs, base_path.as_ref(), absolute)?;
let res = file_records.into_lua(lua)?;
Ok(res)
}
pub(super) fn file_first(
lua: &Lua,
runtime: &Runtime,
include_globs: Value,
options: Option<Value>,
) -> mlua::Result<Value> {
let (base_path, include_globs) = base_dir_and_globs(runtime, include_globs, options.as_ref())?;
let absolute = options.x_get_bool("absolute").unwrap_or(false);
let base_path = match base_path {
Some(base_path) => base_path.clone(),
None => runtime
.dir_context()
.wks_dir()
.ok_or(crate::Error::custom("Cannot create file records, no workspace"))?
.clone(),
};
let mut sfiles = iter_files(
&base_path,
Some(&include_globs.iter().map(|s| s.as_str()).collect::<Vec<&str>>()),
Some(simple_fs::ListOptions::from_relative_glob(true)),
)
.map_err(crate::Error::from)?;
let Some(sfile) = sfiles.next() else {
return Ok(Value::Nil);
};
let absolute_path = SPath::from(&sfile);
let spath = if absolute {
sfile
} else {
sfile
.try_diff(&base_path)
.map_err(|err| crate::Error::cc("Cannot diff with base_path", err))?
};
let res = FileInfo::new(runtime.dir_context(), spath, &absolute_path).into_lua(lua)?;
Ok(res)
}
#[cfg(test)]
mod tests {
type Result<T> = core::result::Result<T, Box<dyn std::error::Error>>;
use crate::_test_support::{assert_contains, eval_lua, run_reflective_agent, setup_lua};
use crate::script::aip_modules::aip_file;
use serde_json::Value;
use simple_fs::SPath;
use std::collections::HashMap;
use value_ext::JsonValueExt as _;
#[tokio::test]
async fn test_lua_file_load_simple_ok() -> Result<()> {
let fx_path = "./agent-script/agent-hello.aip";
let res = run_reflective_agent(&format!(r#"return aip.file.load("{fx_path}")"#), None).await?;
assert_contains(res.x_get_str("content")?, "from agent-hello.aip");
assert_eq!(res.x_get_str("path")?, fx_path);
assert_eq!(res.x_get_str("name")?, "agent-hello.aip");
Ok(())
}
#[tokio::test]
async fn test_lua_file_load_pack_ref_simple() -> Result<()> {
let fx_path = "ns_b@pack_b_2/main.aip";
let res = run_reflective_agent(&format!(r#"return aip.file.load("{fx_path}")"#), None).await?;
assert_contains(res.x_get_str("content")?, "custom ns_b@pack_b_2 main.aip");
assert_contains(res.x_get_str("path")?, "pack_b_2/main.aip");
assert_eq!(res.x_get_str("name")?, "main.aip");
Ok(())
}
#[tokio::test]
async fn test_lua_file_load_pack_ref_base_support() -> Result<()> {
let fx_path = "ns_b@pack_b_2$base/extra/test.txt";
let res = run_reflective_agent(&format!(r#"return aip.file.load("{fx_path}")"#), None).await?;
assert_contains(
res.x_get_str("content")?,
"Some support content - ..@..$base/extra/test.txt",
);
assert_contains(res.x_get_str("path")?, fx_path);
assert_eq!(res.x_get_str("name")?, "test.txt");
Ok(())
}
#[tokio::test]
async fn test_lua_file_load_pack_ref_workspace_support() -> Result<()> {
let fx_path = "ns_a@pack_a_1$workspace/extra/test.txt";
let res = run_reflective_agent(&format!(r#"return aip.file.load("{fx_path}")"#), None).await?;
assert_contains(
res.x_get_str("content")?,
"Some support content - ..@..$workspace/extra/test.txt",
);
assert_contains(res.x_get_str("path")?, fx_path);
assert_eq!(res.x_get_str("name")?, "test.txt");
Ok(())
}
#[tokio::test]
async fn test_lua_file_exists_true() -> Result<()> {
let lua = setup_lua(aip_file::init_module, "file").await?;
let paths = &[
"./agent-script/agent-hello.aip",
"agent-script/agent-hello.aip",
"./sub-dir-a/agent-hello-2.aip",
"sub-dir-a/agent-hello-2.aip",
"./sub-dir-a/",
"sub-dir-a",
"./sub-dir-a/",
"./sub-dir-a/../",
"./sub-dir-a/..",
"ns_b@pack_b_2/main.aip",
"ns_a@pack_a_1$workspace/extra/test.txt",
];
for path in paths {
let code = format!(r#"return aip.file.exists("{path}")"#);
let res = eval_lua(&lua, &code)?;
assert!(res.as_bool().ok_or("Result should be a bool")?, "Should exist: {path}");
}
Ok(())
}
#[tokio::test]
async fn test_lua_file_info_ok() -> Result<()> {
let fx_path = "./agent-script/agent-hello.aip";
let res = run_reflective_agent(&format!(r#"return aip.file.info("{fx_path}")"#), None).await?;
assert_eq!(res.x_get_str("name")?, "agent-hello.aip");
assert_eq!(res.x_get_str("path")?, fx_path);
Ok(())
}
#[tokio::test]
async fn test_lua_file_info_not_found() -> Result<()> {
let res = run_reflective_agent(r#"return aip.file.info("not/a/file.txt")"#, None).await?;
assert_eq!(res, serde_json::Value::Null, "Should have returned null");
Ok(())
}
#[tokio::test]
async fn test_lua_file_exists_false() -> Result<()> {
let lua = setup_lua(aip_file::init_module, "file").await?;
let paths = &[
"./no file .rs",
"some/no-file.md",
"./s do/",
"no-dir/at/all",
"non_existent_ns@non_existent_pack/file.txt",
];
for path in paths {
let code = format!(r#"return aip.file.exists("{path}")"#);
let res = eval_lua(&lua, &code);
let res = res?;
assert!(
!res.as_bool().ok_or("Result should be a bool")?,
"Should NOT exist: {path}"
);
}
Ok(())
}
#[tokio::test]
async fn test_lua_file_list_glob_direct() -> Result<()> {
let glob = "*.*";
let res = run_reflective_agent(&format!(r#"return aip.file.list("{glob}");"#), None).await?;
let res_paths = to_res_paths(&res)?;
assert_eq!(res_paths.len(), 2, "result length");
assert_contains(&res_paths, "file-01.txt");
assert_contains(&res_paths, "file-02.txt");
Ok(())
}
#[tokio::test]
async fn test_lua_file_list_support_workspace() -> Result<()> {
let glob = "ns_b@pack_b_2$base/**/*.*";
let res = run_reflective_agent(&format!(r#"return aip.file.list("{glob}");"#), None).await?;
let res = res.as_array().ok_or("Should return an array")?;
assert_eq!(res.len(), 1, "result length");
let item = res.first().ok_or("Should have one item")?;
assert_contains(item.x_get_str("path")?, "extra/test.txt");
Ok(())
}
#[tokio::test]
async fn test_lua_file_list_glob_deep() -> Result<()> {
let glob = "sub-dir-a/**/*.*";
let res = run_reflective_agent(&format!(r#"return aip.file.list("{glob}");"#), None).await?;
let res_paths = to_res_paths(&res)?;
assert_eq!(res_paths.len(), 3, "result length");
assert_contains(&res_paths, "sub-dir-a/sub-sub-dir/agent-hello-3.aip");
assert_contains(&res_paths, "sub-dir-a/sub-sub-dir/main.aip");
assert_contains(&res_paths, "sub-dir-a/agent-hello-2.aip");
Ok(())
}
#[tokio::test]
async fn test_lua_file_list_negative_glob_absolute() -> Result<()> {
let res = run_reflective_agent(
r#"return aip.file.list({"**/*.aip", "!sub-dir-a/**/*.aip"}, { absolute = true })"#,
None,
)
.await?;
let res_paths = to_res_paths(&res)?;
assert!(!res_paths.is_empty(), "Should have at least one aip file");
assert_contains(&res_paths, "agent-script/agent-hello.aip");
for path in res_paths {
assert!(
!path.contains("sub-dir-a/"),
"Negative glob should exclude sub-dir-a paths even when absolute = true. Got: {path}"
);
}
Ok(())
}
#[tokio::test]
async fn test_lua_file_first_negative_glob_absolute() -> Result<()> {
let res = run_reflective_agent(
r#"return aip.file.first({"sub-dir-a/**/*.*", "!sub-dir-a/agent-hello-2.aip", "!sub-dir-a/sub-sub-dir/agent-hello-3.aip"}, { absolute = true })"#,
None,
)
.await?;
assert_eq!(res.x_get_str("name")?, "main.aip");
assert_contains(res.x_get_str("path")?, "sub-dir-a/sub-sub-dir/main.aip");
Ok(())
}
#[tokio::test]
async fn test_lua_file_list_glob_abs_with_wild() -> Result<()> {
let lua = setup_lua(aip_file::init_module, "file").await?;
let dir = SPath::new("./tests-data/config");
let dir = dir
.canonicalize()
.map_err(|err| format!("Cannot canonicalize {dir:?} cause: {err}"))?;
let glob = format!("{dir}/*.*");
let code = format!(r#"return aip.file.list("{glob}");"#);
let res = eval_lua(&lua, &code)?;
let res = res.as_array().ok_or("Should be array")?;
assert_eq!(res.len(), 1);
let val = res.first().ok_or("Should have one item")?;
assert_eq!(val.x_get_str("name")?, "config-current-with-aliases.toml");
Ok(())
}
#[tokio::test]
async fn test_lua_file_list_glob_with_base_dir_all_nested() -> Result<()> {
let lua = setup_lua(super::super::init_module, "file").await?;
let lua_code = r#"
local files = aip.file.list({"**/*.*"}, {base_dir = "sub-dir-a"})
return { files = files }
"#;
let res = eval_lua(&lua, lua_code)?;
let files = res
.get("files")
.ok_or("Should have .files")?
.as_array()
.ok_or("file should be array")?;
assert_eq!(files.len(), 3, ".files.len() should be 3");
let file_by_name: HashMap<&str, &Value> =
files.iter().map(|v| (v.x_get_str("name").unwrap_or_default(), v)).collect();
let file = file_by_name.get("main.aip").ok_or("Should have 'main.aip'")?;
assert_eq!(file.x_get_str("path")?, "sub-sub-dir/main.aip");
assert!(file.x_get_i64("size")? >= 0, "Should have size >= 0");
let file = file_by_name.get("agent-hello-3.aip").ok_or("Should have 'agent-hello-3.aip'")?;
assert_eq!(file.x_get_str("path")?, "sub-sub-dir/agent-hello-3.aip");
assert!(file.x_get_i64("size")? >= 0, "Should have size >= 0");
let file = file_by_name.get("agent-hello-2.aip").ok_or("Should have 'agent-hello-2.aip'")?;
assert_eq!(file.x_get_str("path")?, "agent-hello-2.aip");
assert!(file.x_get_i64("size")? >= 0, "Should have size >= 0");
Ok(())
}
#[tokio::test]
async fn test_lua_file_list_glob_with_base_dir_one_level() -> Result<()> {
let lua = setup_lua(super::super::init_module, "file").await?;
let lua_code = r#"
local files = aip.file.list({"agent-hello-*.aip"}, {base_dir = "sub-dir-a"})
return { files = files }
"#;
let res = eval_lua(&lua, lua_code)?;
let files = res
.get("files")
.ok_or("Should have .files")?
.as_array()
.ok_or("file should be array")?;
assert_eq!(files.len(), 1, ".files.len() should be 1");
assert_eq!(
"agent-hello-2.aip",
files.first().ok_or("Should have a least one file")?.x_get_str("name")?
);
Ok(())
}
#[tokio::test]
async fn test_lua_file_first_glob_deep() -> Result<()> {
let glob = "sub-dir-a/**/*-2.*";
let res = run_reflective_agent(&format!(r#"return aip.file.first("{glob}");"#), None).await?;
assert_eq!(res.x_get_str("name")?, "agent-hello-2.aip");
assert_eq!(res.x_get_str("path")?, "sub-dir-a/agent-hello-2.aip");
Ok(())
}
#[tokio::test]
async fn test_lua_file_first_not_found() -> Result<()> {
let glob = "sub-dir-a/**/*-not-a-thing.*";
let res = run_reflective_agent(&format!(r#"return aip.file.first("{glob}")"#), None).await?;
assert_eq!(res, serde_json::Value::Null, "Should have returned null");
Ok(())
}
fn to_res_paths(res: &serde_json::Value) -> Result<Vec<&str>> {
let arr = res.as_array().ok_or("should have array of path")?;
let v = arr
.iter()
.map(|v| v.x_get_as::<&str>("path").unwrap_or_default())
.collect::<Vec<&str>>();
Ok(v)
}
}