use crate::Result;
use crate::dir_context::PathResolver;
use crate::runtime::Runtime;
use crate::script::support::{into_option_string, into_vec_of_strings};
use crate::support::W;
use crate::types::FileInfo;
use mlua::{FromLua, IntoLua, Lua, MultiValue, Table, Value, Variadic};
use simple_fs::{SPath, SortByGlobsOptions, get_glob_set, sort_by_globs};
use std::path::Path;
pub fn init_module(lua: &Lua, runtime: &Runtime) -> Result<Table> {
let table = lua.create_table()?;
let path_split_fn = lua.create_function(path_split)?;
let rt = runtime.clone();
let path_exists_fn = lua.create_function(move |_lua, path: String| path_exists(&rt, path))?;
let rt = runtime.clone();
let path_resolve_fn = lua.create_function(move |_lua, path: String| path_resolve(&rt, path))?;
let rt = runtime.clone();
let path_is_file_fn = lua.create_function(move |_lua, path: String| path_is_file(&rt, path))?;
let rt = runtime.clone();
let path_is_dir_fn = lua.create_function(move |_lua, path: String| path_is_dir(&rt, path))?;
let rt = runtime.clone();
let path_diff_fn = lua
.create_function(move |_lua, (file_path, base_path): (String, String)| path_diff(&rt, file_path, base_path))?;
let path_join =
lua.create_function(move |lua, (base, args): (String, Variadic<Value>)| path_join(lua, base, args))?;
let rt = runtime.clone();
let path_parse_fn = lua.create_function(move |lua, value: Value| path_parse(lua, &rt, value))?;
let path_parent_fn = lua.create_function(move |_lua, path: String| path_parent(path))?;
let path_matches_glob_fn =
lua.create_function(move |_lua, (path, globs): (Value, Value)| path_matches_glob(path, globs))?;
let path_sort_by_globs_fn = lua.create_function(move |lua, (files, globs, options): (Value, Value, Value)| {
path_sort_by_globs(lua, files, globs, options)
})?;
table.set("parse", path_parse_fn)?;
table.set("resolve", path_resolve_fn)?;
table.set("join", path_join)?;
table.set("exists", path_exists_fn)?;
table.set("is_file", path_is_file_fn)?;
table.set("is_dir", path_is_dir_fn)?;
table.set("diff", path_diff_fn)?;
table.set("parent", path_parent_fn)?;
table.set("matches_glob", path_matches_glob_fn)?;
table.set("split", path_split_fn)?;
table.set("sort_by_globs", path_sort_by_globs_fn)?;
Ok(table)
}
fn path_parse(lua: &Lua, runtime: &Runtime, path: Value) -> mlua::Result<Value> {
let Some(path) = into_option_string(path, "aip.path.parse")? else {
return Ok(Value::Nil);
};
let spath = SPath::new(path);
let meta = FileInfo::new(runtime.dir_context(), spath, false);
meta.into_lua(lua)
}
fn path_split(lua: &Lua, path: String) -> mlua::Result<MultiValue> {
let path = SPath::from(path);
let parent = path.parent().map(|p| p.to_string()).unwrap_or_default();
let file_name = path.file_name().unwrap_or_default().to_string();
Ok(MultiValue::from_vec(vec![
mlua::Value::String(lua.create_string(parent)?),
mlua::Value::String(lua.create_string(file_name)?),
]))
}
fn path_join(lua: &Lua, base: String, parts: Variadic<Value>) -> mlua::Result<Value> {
let base = SPath::from(base);
let mut parts_str = String::new();
for part in parts {
let sub_parts = into_vec_of_strings(part, "aip.path.join")?;
parts_str.push_str(&sub_parts.join("/"))
}
let res = base.join(parts_str).to_string();
let res = res.into_lua(lua)?;
Ok(res)
}
fn path_resolve(runtime: &Runtime, path: String) -> mlua::Result<String> {
let path = runtime
.dir_context()
.resolve_path(runtime.session(), (&path).into(), PathResolver::WksDir, None)?;
Ok(path.to_string())
}
fn path_exists(runtime: &Runtime, path: String) -> mlua::Result<bool> {
Ok(crate::script::support::path_exists(runtime, &path))
}
fn path_is_file(runtime: &Runtime, path: String) -> mlua::Result<bool> {
let path = runtime
.dir_context()
.resolve_path(runtime.session(), (&path).into(), PathResolver::WksDir, None)?;
Ok(path.is_file())
}
fn path_diff(runtime: &Runtime, file_path: String, base_path: String) -> mlua::Result<String> {
let dir_context = runtime.dir_context();
let file_path = dir_context.maybe_tilde_path_into_home(SPath::from(file_path));
let base_path = dir_context.maybe_tilde_path_into_home(SPath::from(base_path));
let diff = file_path.diff(base_path).map(|p| p.to_string()).unwrap_or_default();
Ok(diff)
}
fn path_is_dir(runtime: &Runtime, path: String) -> mlua::Result<bool> {
let path = runtime
.dir_context()
.resolve_path(runtime.session(), (&path).into(), PathResolver::WksDir, None)?;
Ok(path.is_dir())
}
fn path_parent(path: String) -> mlua::Result<Option<String>> {
match Path::new(&path).parent() {
Some(parent) => match parent.to_str() {
Some(parent_str) => Ok(Some(parent_str.to_string())),
None => Ok(None),
},
None => Ok(None),
}
}
fn path_matches_glob(path: Value, globs: Value) -> mlua::Result<Value> {
let Some(path) = into_option_string(path, "aip.path.matches_glob")? else {
return Ok(Value::Nil);
};
let patterns = into_vec_of_strings(globs, "aip.path.matches_glob")?;
if patterns.is_empty() {
return Ok(Value::Boolean(false));
}
let glob_refs: Vec<&str> = patterns.iter().map(|s| s.as_str()).collect();
let glob_set = get_glob_set(&glob_refs).map_err(|err| crate::Error::custom(err.to_string()))?;
let is_match = glob_set.is_match(&path);
Ok(Value::Boolean(is_match))
}
fn path_sort_by_globs(lua: &Lua, files: Value, globs: Value, options: Value) -> mlua::Result<Value> {
let glob_patterns = into_vec_of_strings(globs, "aip.path.sort_by_globs")?;
let glob_refs: Vec<&str> = glob_patterns.iter().map(|s| s.as_str()).collect();
let sort_options = W::<SortByGlobsOptions>::from_lua(options, lua)?.0;
let files_table = files
.as_table()
.ok_or_else(|| mlua::Error::runtime("aip.path.sort_by_globs 'files' argument must be a list (table)"))?;
struct SortedItem {
path: SPath,
value: Value,
}
impl AsRef<SPath> for SortedItem {
fn as_ref(&self) -> &SPath {
&self.path
}
}
let mut items: Vec<SortedItem> = Vec::new();
for pair in files_table.clone().sequence_values::<Value>() {
let val = pair?;
let path_str = match &val {
Value::String(s) => s
.to_str()
.map(|s| s.to_string())
.map_err(|e| mlua::Error::runtime(format!("aip.path.sort_by_globs file string error: {e}")))?,
Value::Table(tbl) => tbl
.get::<Value>("path")
.ok()
.and_then(|v| v.as_string().map(|v| v.to_string_lossy()))
.ok_or_else(|| {
mlua::Error::runtime("aip.path.sort_by_globs each file table must have a 'path' string field")
})?,
_ => {
return Err(mlua::Error::runtime(
"aip.path.sort_by_globs each file must be a string or a table with a 'path' field",
));
}
};
items.push(SortedItem {
path: SPath::from(path_str),
value: val,
});
}
let sorted = sort_by_globs(items, &glob_refs, sort_options)
.map_err(|e| mlua::Error::runtime(format!("aip.path.sort_by_globs error: {e}")))?;
let result = lua.create_table()?;
for (i, item) in sorted.into_iter().enumerate() {
result.set(i + 1, item.value)?;
}
Ok(Value::Table(result))
}
#[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_path;
#[tokio::test]
async fn test_lua_path_exists_true() -> Result<()> {
let lua = setup_lua(aip_path::init_module, "path").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/..",
];
for path in paths {
let code = format!(r#"return aip.path.exists("{path}")"#);
let res = eval_lua(&lua, &code)?;
assert!(res.as_bool().ok_or("Result should be a bool")?, "'{path}' should exist");
}
Ok(())
}
#[tokio::test]
async fn test_lua_path_exists_false() -> Result<()> {
let lua = setup_lua(aip_path::init_module, "path").await?;
let paths = &["./no file .rs", "some/no-file.md", "./s do/", "no-dir/at/all"];
for path in paths {
let code = format!(r#"return aip.path.exists("{path}")"#);
let res = eval_lua(&lua, &code)?;
assert!(
!res.as_bool().ok_or("Result should be a bool")?,
"'{path}' should NOT exist"
);
}
Ok(())
}
#[tokio::test]
async fn test_lua_path_is_file_true() -> Result<()> {
let lua = setup_lua(aip_path::init_module, "path").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/../agent-script/agent-hello.aip",
];
for path in paths {
let code = format!(r#"return aip.path.is_file("{path}")"#);
let res = eval_lua(&lua, &code)?;
assert!(
res.as_bool().ok_or("Result should be a bool")?,
"'{path}' should be a file"
);
}
Ok(())
}
#[tokio::test]
async fn test_lua_path_is_file_false() -> Result<()> {
let lua = setup_lua(aip_path::init_module, "path").await?;
let paths = &["./no-file", "no-file.txt", "sub-dir-a/"];
for path in paths {
let code = format!(r#"return aip.path.is_file("{path}")"#);
let res = eval_lua(&lua, &code)?;
assert!(
!res.as_bool().ok_or("Result should be a bool")?,
"'{path}' should NOT be a file"
);
}
Ok(())
}
#[tokio::test]
async fn test_lua_path_is_dir_true() -> Result<()> {
let lua = setup_lua(aip_path::init_module, "path").await?;
let paths = &["./sub-dir-a", "sub-dir-a", "./sub-dir-a/.."];
for path in paths {
let code = format!(r#"return aip.path.is_dir("{path}")"#);
let res = eval_lua(&lua, &code)?;
assert!(
res.as_bool().ok_or("Result should be a bool")?,
"'{path}' should be a directory"
);
}
Ok(())
}
#[tokio::test]
async fn test_lua_path_is_dir_false() -> Result<()> {
let lua = setup_lua(aip_path::init_module, "path").await?;
let paths = &[
"./agent-hello.aipack",
"agent-hello.aipack",
"./sub-dir-a/agent-hello-2.aipack",
"./sub-dir-a/other-path",
"nofile.txt",
"./s rc/",
];
for path in paths {
let code = format!(r#"return aip.path.is_dir("{path}")"#);
let res = eval_lua(&lua, &code)?;
assert!(
!res.as_bool().ok_or("Result should be a bool")?,
"'{path}' should NOT be a directory"
);
}
Ok(())
}
#[tokio::test]
async fn test_lua_path_parent() -> Result<()> {
let lua = setup_lua(aip_path::init_module, "path").await?;
let paths = &[
("./agent-hello.aipack", "."),
("./", ""),
(".", ""),
("./sub-dir/file.txt", "./sub-dir"),
("./sub-dir/file", "./sub-dir"),
("./sub-dir/", "."),
("./sub-dir", "."),
];
for (path, expected) in paths {
let code = format!(r#"return aip.path.parent("{path}")"#);
let res = eval_lua(&lua, &code)?;
let result = res.as_str().ok_or("Should be a string")?;
assert_eq!(result, *expected, "Parent mismatch for path: {path}");
}
Ok(())
}
#[tokio::test]
async fn test_lua_path_join() -> Result<()> {
let lua = setup_lua(aip_path::init_module, "path").await?;
let data = [
("./", r#""file.txt""#, "./file.txt"),
("./", r#"{"my-dir","file.txt"}"#, "./my-dir/file.txt"),
("", r#"{"my-dir","file.txt"}"#, "my-dir/file.txt"),
("", r#"{"my-dir/","//file.txt"}"#, "my-dir/file.txt"),
("some-base/", r#""my-dir/","file.txt""#, "some-base/my-dir/file.txt"),
("a", r#""b", "c""#, "a/bc"),
("a/", r#""b/", "c/""#, "a/b/c/"),
(
"root/",
r#"{"user", "docs"}, "projectA", {"report", "final.pdf"}"#,
"root/user/docsprojectAreport/final.pdf",
),
];
for (base, args, expected) in data {
let code = format!(r#"return aip.path.join("{base}", {args})"#);
let res = eval_lua(&lua, &code)?;
let res = res.as_str().ok_or("Should have returned string")?;
assert_eq!(res, expected);
}
Ok(())
}
#[tokio::test]
async fn test_lua_path_split() -> Result<()> {
let lua = setup_lua(aip_path::init_module, "path").await?;
let paths = &[
("some/path/to_file.md", "some/path", "to_file.md"),
("folder/file.txt", "folder", "file.txt"),
("justafile.md", "", "justafile.md"),
("/absolute/path/file.log", "/absolute/path", "file.log"),
("/file_at_root", "/", "file_at_root"),
("trailing/slash/", "trailing", "slash"),
];
for (path, expected_parent, expected_filename) in paths {
let code = format!(
r#"
local parent, filename = aip.path.split("{path}")
return {{ parent, filename }}
"#
);
let res = eval_lua(&lua, &code)?;
let res_array = res.as_array().ok_or("Expected an array from Lua function")?;
let parent = res_array
.first()
.and_then(|v| v.as_str())
.ok_or("First value should be a string")?;
let filename = res_array
.get(1)
.and_then(|v| v.as_str())
.ok_or("Second value should be a string")?;
assert_eq!(parent, *expected_parent, "Parent mismatch for path: {path}");
assert_eq!(filename, *expected_filename, "Filename mismatch for path: {path}");
}
Ok(())
}
#[tokio::test]
async fn test_lua_path_sort_by_globs_strings() -> Result<()> {
let lua = setup_lua(aip_path::init_module, "path").await?;
let code = r#"
local files = {"src/main.rs", "README.md", "src/lib.rs", "Cargo.toml"}
return aip.path.sort_by_globs(files, {"*.toml", "*.md"})
"#;
let res = eval_lua(&lua, code)?;
let arr = res.as_array().ok_or("Expected array")?;
assert_eq!(arr.len(), 4);
assert_eq!(arr[0].as_str().ok_or("str")?, "Cargo.toml");
assert_eq!(arr[1].as_str().ok_or("str")?, "README.md");
assert_eq!(arr[2].as_str().ok_or("str")?, "src/lib.rs");
assert_eq!(arr[3].as_str().ok_or("str")?, "src/main.rs");
Ok(())
}
#[tokio::test]
async fn test_lua_path_sort_by_globs_no_match_start() -> Result<()> {
let lua = setup_lua(aip_path::init_module, "path").await?;
let code = r#"
local files = {"src/main.rs", "README.md", "src/lib.rs", "Cargo.toml"}
return aip.path.sort_by_globs(files, {"*.toml"}, "start")
"#;
let res = eval_lua(&lua, code)?;
let arr = res.as_array().ok_or("Expected array")?;
assert_eq!(arr.len(), 4);
assert_eq!(arr[0].as_str().ok_or("str")?, "README.md");
assert_eq!(arr[1].as_str().ok_or("str")?, "src/lib.rs");
assert_eq!(arr[2].as_str().ok_or("str")?, "src/main.rs");
assert_eq!(arr[3].as_str().ok_or("str")?, "Cargo.toml");
Ok(())
}
#[tokio::test]
async fn test_lua_path_sort_by_globs_table_options() -> Result<()> {
let lua = setup_lua(aip_path::init_module, "path").await?;
let code = r#"
local files = {"src/main.rs", "README.md", "Cargo.toml"}
return aip.path.sort_by_globs(files, {"*.toml", "*.md"}, {no_match_position = "start"})
"#;
let res = eval_lua(&lua, code)?;
let arr = res.as_array().ok_or("Expected array")?;
assert_eq!(arr.len(), 3);
assert_eq!(arr[0].as_str().ok_or("str")?, "src/main.rs");
assert_eq!(arr[1].as_str().ok_or("str")?, "Cargo.toml");
assert_eq!(arr[2].as_str().ok_or("str")?, "README.md");
Ok(())
}
}