Skip to main content

hx_plugins/api/
fs.rs

1//! Filesystem API for plugins.
2//!
3//! Provides: (hx/read-file), (hx/write-file), (hx/file-exists?), (hx/glob), (hx/path-join)
4
5use crate::context::with_context;
6use crate::error::Result;
7use std::fs;
8use std::path::PathBuf;
9use steel::SteelVal;
10use steel::steel_vm::engine::Engine;
11use steel::steel_vm::register_fn::RegisterFn;
12
13/// Register filesystem API functions.
14pub fn register(engine: &mut Engine) -> Result<()> {
15    engine.register_fn("hx/read-file", read_file);
16    engine.register_fn("hx/write-file", write_file);
17    engine.register_fn("hx/file-exists?", file_exists);
18    engine.register_fn("hx/glob", glob_files);
19    engine.register_fn("hx/path-join", path_join);
20    engine.register_fn("hx/mkdir", mkdir);
21    Ok(())
22}
23
24/// Read a file's contents.
25fn read_file(path: String) -> std::result::Result<SteelVal, String> {
26    let resolved_path = resolve_path(&path);
27
28    fs::read_to_string(&resolved_path)
29        .map(|content| SteelVal::StringV(content.into()))
30        .map_err(|e| format!("Failed to read file '{}': {}", path, e))
31}
32
33/// Write content to a file.
34fn write_file(path: String, content: String) -> std::result::Result<SteelVal, String> {
35    let resolved_path = resolve_path(&path);
36
37    // Create parent directories if needed
38    if let Some(parent) = resolved_path.parent() {
39        fs::create_dir_all(parent).map_err(|e| format!("Failed to create directory: {}", e))?;
40    }
41
42    fs::write(&resolved_path, content)
43        .map(|_| SteelVal::Void)
44        .map_err(|e| format!("Failed to write file '{}': {}", path, e))
45}
46
47/// Check if a file exists.
48fn file_exists(path: String) -> SteelVal {
49    let resolved_path = resolve_path(&path);
50    SteelVal::BoolV(resolved_path.exists())
51}
52
53/// Find files matching a glob pattern.
54fn glob_files(pattern: String) -> SteelVal {
55    let resolved_pattern = resolve_path(&pattern);
56    let pattern_str = resolved_pattern.to_string_lossy();
57
58    match glob::glob(&pattern_str) {
59        Ok(paths) => {
60            let files: Vec<SteelVal> = paths
61                .filter_map(|p| p.ok())
62                .map(|p| SteelVal::StringV(p.to_string_lossy().to_string().into()))
63                .collect();
64            SteelVal::ListV(files.into())
65        }
66        Err(e) => {
67            eprintln!("Glob error: {}", e);
68            SteelVal::ListV(vec![].into())
69        }
70    }
71}
72
73/// Join path components.
74fn path_join(parts: Vec<SteelVal>) -> SteelVal {
75    let mut path = PathBuf::new();
76
77    for part in parts {
78        if let SteelVal::StringV(s) = part {
79            path.push(s.to_string());
80        }
81    }
82
83    SteelVal::StringV(path.to_string_lossy().to_string().into())
84}
85
86/// Create a directory (and parents).
87fn mkdir(path: String) -> std::result::Result<SteelVal, String> {
88    let resolved_path = resolve_path(&path);
89
90    fs::create_dir_all(&resolved_path)
91        .map(|_| SteelVal::Void)
92        .map_err(|e| format!("Failed to create directory '{}': {}", path, e))
93}
94
95/// Resolve a path relative to the project root.
96fn resolve_path(path: &str) -> PathBuf {
97    let path = PathBuf::from(path);
98
99    // If absolute, return as-is
100    if path.is_absolute() {
101        return path;
102    }
103
104    // Otherwise, resolve relative to project root
105    with_context(|ctx| ctx.project_root.join(&path)).unwrap_or(path)
106}