use crate::hub::get_hub;
use crate::run::{DirContext, PathResolver, RuntimeContext};
use crate::script::lua_script::helpers::{get_value_prop_as_string, to_vec_of_strings};
use crate::support::{files, paths, AsStrsExt};
use crate::types::{FileMeta, FileRecord};
use crate::{Error, Result};
use mlua::{FromLua, IntoLua, Lua, Value};
use simple_fs::{ensure_file_dir, iter_files, list_files, ListOptions, SPath};
use std::fs::write;
use std::io::Write;
pub(super) fn file_load(
lua: &Lua,
ctx: &RuntimeContext,
rel_path: String,
options: Option<Value>,
) -> mlua::Result<mlua::Value> {
let base_path = compute_base_dir(ctx.dir_context(), options.as_ref())?;
let rel_path = SPath::new(rel_path).map_err(Error::from)?;
let file_record = FileRecord::load(&base_path, &rel_path)?;
let res = file_record.into_lua(lua)?;
Ok(res)
}
pub(super) fn file_save(_lua: &Lua, ctx: &RuntimeContext, rel_path: String, content: String) -> mlua::Result<()> {
let path = ctx.dir_context().resolve_path(&rel_path, PathResolver::WorkspaceDir)?;
ensure_file_dir(&path).map_err(Error::from)?;
write(&path, content)?;
get_hub().publish_sync(format!("-> Lua utils.file.save called on: {}", rel_path));
Ok(())
}
pub(super) fn file_append(_lua: &Lua, ctx: &RuntimeContext, rel_path: String, content: String) -> mlua::Result<()> {
let path = ctx.dir_context().resolve_path(&rel_path, PathResolver::WorkspaceDir)?;
ensure_file_dir(&path).map_err(Error::from)?;
let mut file = std::fs::OpenOptions::new()
.append(true)
.create(true)
.open(&path)
.map_err(Error::from)?;
file.write_all(content.as_bytes())?;
Ok(())
}
pub(super) fn file_ensure_exists(
lua: &Lua,
ctx: &RuntimeContext,
path: String,
content: Option<String>,
options: Option<EnsureExistsOptions>,
) -> mlua::Result<mlua::Value> {
let options = options.unwrap_or_default();
let rel_path = SPath::new(path).map_err(Error::from)?;
let full_path = ctx.dir_context().resolve_path(&rel_path, PathResolver::WorkspaceDir)?;
if !full_path.exists() {
simple_fs::ensure_file_dir(&full_path).map_err(|err| Error::custom(err.to_string()))?;
let content = content.unwrap_or_default();
write(&full_path, content)?;
}
else if options.content_when_empty && files::is_file_empty(&full_path)? {
let content = content.unwrap_or_default();
write(full_path, content)?;
}
let file_meta = FileMeta::from(rel_path);
file_meta.into_lua(lua)
}
pub(super) fn file_list(
lua: &Lua,
ctx: &RuntimeContext,
include_globs: Value,
options: Option<Value>,
) -> mlua::Result<Value> {
let (base_path, include_globs) = base_dir_and_globs(ctx, include_globs, options.as_ref())?;
let sfiles = list_files(
&base_path,
Some(&include_globs.x_as_strs()),
Some(ListOptions::from_relative_glob(true)),
)
.map_err(Error::from)?;
let sfiles = sfiles
.into_iter()
.map(|f| {
let diff = f.diff(&base_path)?;
if diff.to_str().starts_with("..") {
Ok(SPath::from(f))
} else {
Ok(diff)
}
})
.collect::<simple_fs::Result<Vec<SPath>>>()
.map_err(|err| crate::Error::cc("Cannot list files to base", err))?;
let file_metas: Vec<FileMeta> = sfiles.into_iter().map(FileMeta::from).collect();
let res = file_metas.into_lua(lua)?;
Ok(res)
}
pub(super) fn file_list_load(
lua: &Lua,
ctx: &RuntimeContext,
include_globs: Value,
options: Option<Value>,
) -> mlua::Result<Value> {
let (base_path, include_globs) = base_dir_and_globs(ctx, include_globs, options.as_ref())?;
let sfiles = list_files(
&base_path,
Some(&include_globs.x_as_strs()),
Some(ListOptions::from_relative_glob(true)),
)
.map_err(Error::from)?;
let file_records = sfiles
.into_iter()
.map(|sfile| -> Result<FileRecord> {
let diff = sfile.diff(&base_path)?;
let (base_path, rel_path) = if diff.to_str().starts_with("..") {
(SPath::from(""), SPath::from(sfile))
} else {
(base_path.clone(), diff)
};
let file_record = FileRecord::load(&base_path, &rel_path)?;
Ok(file_record)
})
.collect::<Result<Vec<_>>>()?;
let res = file_records.into_lua(lua)?;
Ok(res)
}
pub(super) fn file_first(
lua: &Lua,
ctx: &RuntimeContext,
include_globs: Value,
options: Option<Value>,
) -> mlua::Result<Value> {
let (base_path, include_globs) = base_dir_and_globs(ctx, include_globs, options.as_ref())?;
let mut sfiles = iter_files(
&base_path,
Some(&include_globs.x_as_strs()),
Some(ListOptions::from_relative_glob(true)),
)
.map_err(Error::from)?;
let Some(sfile) = sfiles.next() else {
return Ok(Value::Nil);
};
let sfile = sfile
.diff(&base_path)
.map_err(|err| Error::cc("Cannot diff with base_path", err))?;
let res = FileMeta::from(sfile).into_lua(lua)?;
Ok(res)
}
#[derive(Debug, Default)]
pub struct EnsureExistsOptions {
content_when_empty: bool,
}
impl FromLua for EnsureExistsOptions {
fn from_lua(value: Value, _lua: &Lua) -> mlua::Result<Self> {
let table = value
.as_table()
.ok_or(crate::Error::custom("EnsureExistsOptions should be a table"))?;
let set_content_when_empty = table.get("content_when_empty")?;
Ok(Self {
content_when_empty: set_content_when_empty,
})
}
}
fn base_dir_and_globs(
ctx: &RuntimeContext,
include_globs: Value,
options: Option<&Value>,
) -> Result<(SPath, Vec<String>)> {
let globs: Vec<String> = to_vec_of_strings(include_globs, "file::file_list globs argument")?;
let base_dir = compute_base_dir(ctx.dir_context(), options)?;
Ok((base_dir, globs))
}
fn compute_base_dir(dir_context: &DirContext, options: Option<&Value>) -> Result<SPath> {
let workspace_path = dir_context.resolve_path("", PathResolver::WorkspaceDir)?;
let base_dir = get_value_prop_as_string(options, "base_dir", "utils.file... options fail")?;
let base_dir = match base_dir {
Some(base_dir) => {
if paths::is_relative(&base_dir) {
workspace_path.join_str(&base_dir)
} else {
SPath::from(base_dir)
}
}
None => workspace_path,
};
Ok(base_dir)
}
#[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, SANDBOX_01_DIR};
use std::path::Path;
use value_ext::JsonValueExt as _;
#[tokio::test]
async fn test_lua_file_load_simple_ok() -> Result<()> {
let fx_path = "./agent-script/agent-hello.devai";
let res = run_reflective_agent(&format!(r#"return utils.file.load("{fx_path}")"#), None).await?;
assert_contains(res.x_get_str("content")?, "from agent-hello.devai");
assert_eq!(res.x_get_str("path")?, fx_path);
assert_eq!(res.x_get_str("name")?, "agent-hello.devai");
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_lua_file_save_simple_ok() -> Result<()> {
let fx_dest_path = "./.tmp/test_file_save_simple_ok/agent-hello.devai";
let fx_content = "hello from test_file_save_simple_ok";
let _res = run_reflective_agent(
&format!(r#"return utils.file.save("{fx_dest_path}", "{fx_content}");"#),
None,
)
.await?;
let dest_path = Path::new(SANDBOX_01_DIR).join(fx_dest_path);
let file_content = std::fs::read_to_string(dest_path)?;
assert_eq!(file_content, fx_content);
Ok(())
}
#[tokio::test]
async fn test_lua_file_list_glob_direct() -> Result<()> {
let glob = "*.*";
let res = run_reflective_agent(&format!(r#"return utils.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_glob_deep() -> Result<()> {
let glob = "sub-dir-a/**/*.*";
let res = run_reflective_agent(&format!(r#"return utils.file.list("{glob}");"#), None).await?;
let res_paths = to_res_paths(&res);
assert_eq!(res_paths.len(), 2, "result length");
assert_contains(&res_paths, "sub-dir-a/sub-sub-dir/agent-hello-3.devai");
assert_contains(&res_paths, "sub-dir-a/sub-sub-dir/agent-hello-3.devai");
Ok(())
}
#[tokio::test]
async fn test_lua_file_list_glob_abs_with_wild() -> Result<()> {
let lua = setup_lua(super::super::init_module, "file")?;
let dir = Path::new("./tests-data/config");
let dir = dir
.canonicalize()
.map_err(|err| format!("Cannot canonicalize {dir:?} cause: {err}"))?;
let glob = format!("{}/*.*", dir.to_string_lossy());
let code = format!(r#"return utils.file.list("{glob}");"#);
let _res = eval_lua(&lua, &code)?;
Ok(())
}
#[test]
fn test_lua_file_list_glob_with_base_dir_all_nested() -> Result<()> {
let lua = setup_lua(super::super::init_module, "file")?;
let lua_code = r#"
local files = utils.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(), 2, ".files.len() should be 2");
assert_eq!(
"agent-hello-2.devai",
files.first().ok_or("Should have a least one file")?.x_get_str("name")?
);
assert_eq!(
"agent-hello-3.devai",
files.get(1).ok_or("Should have a least two file")?.x_get_str("name")?
);
Ok(())
}
#[test]
fn test_lua_file_list_glob_with_base_dir_one_level() -> Result<()> {
let lua = setup_lua(super::super::init_module, "file")?;
let lua_code = r#"
local files = utils.file.list({"agent-hello-*.devai"}, {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.devai",
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 utils.file.first("{glob}");"#), None).await?;
assert_eq!(res.x_get_str("name")?, "agent-hello-2.devai");
assert_eq!(res.x_get_str("path")?, "sub-dir-a/agent-hello-2.devai");
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 utils.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) -> Vec<&str> {
res.as_array()
.ok_or("should have array of path")
.unwrap()
.iter()
.map(|v| v.x_get_as::<&str>("path").unwrap_or_default())
.collect::<Vec<&str>>()
}
}