use std::path::{Path, PathBuf};
use mlua::Lua;
use super::super::alc_toml;
use super::super::gendoc::register_preloads_pub;
use super::super::AppService;
fn is_safe_pkg_name(name: &str) -> bool {
!name.is_empty() && name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
}
const NARRATIVE_RENDER_LUA: &str = r#"
local extract = require("tools.docs.extract")
local projections = require("tools.docs.projections")
local pi = extract.build_pkg_info(_alc_pkg_name, _alc_init_path, _alc_pkg_name .. "/init.lua")
return projections.narrative_md(pi)
"#;
const INJECT_PKG_PRELOAD_LUA: &str = r#"
local src = _alc_init_src
local name = _alc_pkg_name
package.preload[name] = function()
return load(src, "@" .. name .. "/init.lua")()
end
"#;
impl AppService {
pub(crate) fn pkg_read_init_lua(
&self,
name: &str,
project_root: Option<&Path>,
) -> Result<String, String> {
let resolved_root = project_root
.map(|p| p.to_path_buf())
.or_else(|| self.resolve_root(None));
if let Some(root) = resolved_root {
match alc_toml::load_alc_local_toml(&root) {
Ok(Some(local)) => {
for vp in alc_toml::resolve_local_variant_pkgs(&root, &local) {
if vp.name == name {
let init_lua = vp.pkg_dir.join("init.lua");
return std::fs::read_to_string(&init_lua).map_err(|e| {
format!(
"pkg_read_init_lua: failed to read {}: {e}",
init_lua.display()
)
});
}
}
}
Ok(None) => {}
Err(e) => {
return Err(format!(
"pkg_read_init_lua: malformed alc.local.toml at {}: {e}",
root.display()
));
}
}
}
let global_init_lua: PathBuf = self
.log_config
.app_dir()
.packages_dir()
.join(name)
.join("init.lua");
match std::fs::metadata(&global_init_lua) {
Ok(_) => std::fs::read_to_string(&global_init_lua).map_err(|e| {
format!(
"pkg_read_init_lua: failed to read {}: {e}",
global_init_lua.display()
)
}),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
Err(format!("pkg not found: {name}"))
}
Err(e) => Err(format!(
"pkg_read_init_lua: I/O error for {}: {e}",
global_init_lua.display()
)),
}
}
fn pkg_resolve_init_path(&self, name: &str) -> Result<Option<PathBuf>, String> {
let resolved_root = self.resolve_root(None);
if let Some(root) = resolved_root {
match alc_toml::load_alc_local_toml(&root) {
Ok(Some(local)) => {
for vp in alc_toml::resolve_local_variant_pkgs(&root, &local) {
if vp.name == name {
return Ok(Some(vp.pkg_dir.join("init.lua")));
}
}
}
Ok(None) => {}
Err(e) => {
return Err(format!(
"pkg_resolve_init_path: malformed alc.local.toml: {e}"
));
}
}
}
let global_init_lua: PathBuf = self
.log_config
.app_dir()
.packages_dir()
.join(name)
.join("init.lua");
match std::fs::metadata(&global_init_lua) {
Ok(_) => Ok(Some(global_init_lua)),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(format!(
"pkg_resolve_init_path: I/O error for {}: {e}",
global_init_lua.display()
)),
}
}
pub(crate) async fn pkg_get_narrative_md(&self, name: &str) -> Result<Option<String>, String> {
if !is_safe_pkg_name(name) {
return Err(format!(
"pkg_get_narrative_md: invalid package name '{name}'"
));
}
let init_lua_path = match self.pkg_resolve_init_path(name)? {
Some(p) => p,
None => return Ok(None),
};
let init_src = std::fs::read_to_string(&init_lua_path).map_err(|e| {
format!(
"pkg_get_narrative_md: failed to read {}: {e}",
init_lua_path.display()
)
})?;
let init_path_str = init_lua_path.to_string_lossy().into_owned();
let name_owned = name.to_string();
tokio::task::spawn_blocking(move || {
let lua = Lua::new();
register_preloads_pub(&lua)?;
lua.globals()
.set("_alc_pkg_name", name_owned.clone())
.map_err(|e| {
format!("pkg_get_narrative_md: globals set _alc_pkg_name failed: {e}")
})?;
lua.globals().set("_alc_init_src", init_src).map_err(|e| {
format!("pkg_get_narrative_md: globals set _alc_init_src failed: {e}")
})?;
lua.load(INJECT_PKG_PRELOAD_LUA)
.set_name("@embedded:pkg_get_narrative_md/preload")
.exec()
.map_err(|e| {
format!(
"pkg_get_narrative_md: pkg preload inject failed for '{name_owned}': {e}"
)
})?;
lua.globals()
.set("_alc_init_path", init_path_str.clone())
.map_err(|e| {
format!("pkg_get_narrative_md: globals set _alc_init_path failed: {e}")
})?;
let markdown: String = lua
.load(NARRATIVE_RENDER_LUA)
.set_name("@embedded:pkg_get_narrative_md/render")
.eval()
.map_err(|e| {
format!("pkg_get_narrative_md: gendoc pipeline failed for '{name_owned}': {e}")
})?;
Ok(Some(markdown))
})
.await
.map_err(|e| format!("pkg_get_narrative_md: blocking task panicked: {e}"))?
}
}
#[cfg(test)]
mod tests {
use super::super::super::test_support::make_app_service_at;
#[tokio::test]
async fn read_with_malformed_local_toml_returns_err() {
let tmp = tempfile::tempdir().unwrap();
std::fs::write(tmp.path().join("alc.local.toml"), "not valid toml ][[[").unwrap();
let svc = make_app_service_at(tmp.path().to_path_buf()).await;
let err = svc
.pkg_read_init_lua("mypkg", Some(tmp.path()))
.unwrap_err();
assert!(
err.contains("malformed alc.local.toml"),
"expected malformed error, got: {err}"
);
}
#[tokio::test]
async fn read_global_pkg_ok() {
let tmp = tempfile::tempdir().unwrap();
let svc = make_app_service_at(tmp.path().to_path_buf()).await;
let pkg_dir = tmp.path().join("packages").join("mypkg");
std::fs::create_dir_all(&pkg_dir).unwrap();
std::fs::write(pkg_dir.join("init.lua"), "return {}").unwrap();
let result = svc.pkg_read_init_lua("mypkg", None).unwrap();
assert_eq!(result, "return {}");
}
#[tokio::test]
async fn read_missing_pkg_returns_err() {
let tmp = tempfile::tempdir().unwrap();
let svc = make_app_service_at(tmp.path().to_path_buf()).await;
let err = svc.pkg_read_init_lua("nonexistent", None).unwrap_err();
assert!(err.contains("pkg not found"), "got: {err}");
}
#[tokio::test]
async fn narrative_md_missing_pkg_returns_none() {
let tmp = tempfile::tempdir().unwrap();
let svc = make_app_service_at(tmp.path().to_path_buf()).await;
let result = svc.pkg_get_narrative_md("nonexistent").await.unwrap();
assert!(
result.is_none(),
"expected None for missing pkg, got: {result:?}"
);
}
#[tokio::test]
async fn narrative_md_invalid_name_returns_err() {
let tmp = tempfile::tempdir().unwrap();
let svc = make_app_service_at(tmp.path().to_path_buf()).await;
let err = svc.pkg_get_narrative_md("bad-name!").await.unwrap_err();
assert!(
err.contains("invalid package name"),
"expected invalid name error, got: {err}"
);
}
#[tokio::test]
async fn narrative_md_renders_docstring_sections() {
let tmp = tempfile::tempdir().unwrap();
let svc = make_app_service_at(tmp.path().to_path_buf()).await;
let pkg_dir = tmp.path().join("packages").join("mypkg");
std::fs::create_dir_all(&pkg_dir).unwrap();
std::fs::write(
pkg_dir.join("init.lua"),
r#"--- mypkg — a test package.
---
--- A short summary.
---
--- ## Algorithm
---
--- This is the algorithm section.
---
--- ## References
---
--- No references.
local M = {}
M.meta = { name = "mypkg", version = "0.1.0", category = "test", description = "A test pkg" }
return M
"#,
)
.unwrap();
let result = svc.pkg_get_narrative_md("mypkg").await.unwrap();
let markdown = result.expect("expected Some(markdown) for installed pkg");
assert!(markdown.contains("mypkg"), "title missing: {markdown}");
assert!(
markdown.contains("Algorithm"),
"Algorithm section missing: {markdown}"
);
assert!(
markdown.contains("References"),
"References section missing: {markdown}"
);
}
}