+thiserror, +tracing, +FennelAssertion
change voxwuspnrwsltkypzpskyqpylpxluzoo
commit c9644e2784612a99ebbf38e1e585fb95a784cc61
author Alpha Chen <alpha@kejadlen.dev>
date
parent vwsokokz
diff --git a/Cargo.lock b/Cargo.lock
index bf517f0..f322cf7 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -177,6 +177,8 @@ dependencies = [
  "anyhow",
  "clap",
  "mlua",
+ "thiserror",
+ "tracing",
  "tracing-subscriber",
 ]
 
@@ -331,6 +333,12 @@ dependencies = [
  "windows-link",
 ]
 
+[[package]]
+name = "pin-project-lite"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
+
 [[package]]
 name = "pkg-config"
 version = "0.3.32"
@@ -462,6 +470,26 @@ dependencies = [
  "unicode-ident",
 ]
 
+[[package]]
+name = "thiserror"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "thread_local"
 version = "1.1.9"
@@ -471,6 +499,28 @@ dependencies = [
  "cfg-if",
 ]
 
+[[package]]
+name = "tracing"
+version = "0.1.41"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
+dependencies = [
+ "pin-project-lite",
+ "tracing-attributes",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-attributes"
+version = "0.1.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "tracing-core"
 version = "0.1.34"
diff --git a/Cargo.toml b/Cargo.toml
index 4f9c13b..43381c4 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -7,4 +7,6 @@ edition = "2024"
 anyhow = "1.0"
 clap = { version = "4.5", features = ["derive"] }
 mlua = { version = "0.11", features = ["lua54", "vendored"] }
+thiserror = "1.0"
+tracing = "0.1"
 tracing-subscriber = "0.3"
diff --git a/src/main.rs b/src/main.rs
index f61af5b..9fabb0c 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,5 +1,14 @@
+use anyhow::{Result, anyhow, bail};
 use clap::{Parser, Subcommand};
 use mlua::prelude::*;
+use std::cell::RefCell;
+use std::collections::HashMap;
+use std::env;
+use std::fs;
+use std::path::Path;
+use std::rc::Rc;
+use thiserror::Error;
+use tracing::info;
 
 #[derive(Parser)]
 #[command(name = "frork")]
@@ -14,14 +23,272 @@ enum Commands {
     Check { script: String },
 }
 
-fn main() -> LuaResult<()> {
+#[derive(Error, Debug)]
+pub enum FrorkError {
+    #[error("No operation specified")]
+    NoOperation,
+    #[error("Unknown operation: {operation}")]
+    UnknownOperation { operation: String },
+    #[error("Symlink requires exactly 2 arguments: target and source")]
+    InvalidSymlinkArgs,
+}
+
+fn expand_tilde(path: &str) -> String {
+    if path.starts_with('~') {
+        if let Ok(home) = env::var("HOME") {
+            path.replacen('~', &home, 1)
+        } else {
+            path.to_string()
+        }
+    } else {
+        path.to_string()
+    }
+}
+
+#[derive(Debug)]
+enum Status {
+    Ok,
+    Missing,
+}
+
+trait AssertionType {
+    fn check(&self) -> Status;
+    fn display(&self) -> String;
+}
+
+type AssertionFactory = Box<dyn Fn(LuaMultiValue) -> Result<Box<dyn AssertionType>>>;
+
+struct Registry {
+    assertion_types: HashMap<String, AssertionFactory>,
+}
+
+impl Registry {
+    fn new() -> Self {
+        Self {
+            assertion_types: HashMap::new(),
+        }
+    }
+
+    fn register<F>(&mut self, name: &str, factory: F)
+    where
+        F: Fn(LuaMultiValue) -> Result<Box<dyn AssertionType>> + 'static,
+    {
+        self.assertion_types
+            .insert(name.to_string(), Box::new(factory));
+    }
+
+    fn create(
+        &self,
+        assertion_type: &str,
+        args: LuaMultiValue,
+    ) -> Result<Option<Box<dyn AssertionType>>> {
+        match self.assertion_types.get(assertion_type) {
+            Some(factory) => factory(args).map(Some),
+            None => Ok(None),
+        }
+    }
+}
+
+struct Symlink {
+    target: String,
+    source: String,
+}
+
+impl Symlink {
+    fn new(args: LuaMultiValue) -> Result<Self> {
+        let args: Vec<String> = args
+            .into_iter()
+            .map(|v| {
+                v.to_string()
+                    .map_err(|e| anyhow!("Failed to convert arg to string: {}", e))
+            })
+            .collect::<Result<Vec<_>>>()?;
+
+        if args.len() != 2 {
+            bail!(FrorkError::InvalidSymlinkArgs);
+        }
+        Ok(Self {
+            target: expand_tilde(&args[0]),
+            source: expand_tilde(&args[1]),
+        })
+    }
+}
+
+impl AssertionType for Symlink {
+    fn check(&self) -> Status {
+        if Path::new(&self.target).exists() {
+            // Check if it's a symlink pointing to the correct source
+            if let Ok(link_target) = fs::read_link(&self.target) {
+                if link_target == Path::new(&self.source) {
+                    Status::Ok
+                } else {
+                    unimplemented!(
+                        "symlink {} {} points to wrong target",
+                        self.target,
+                        self.source
+                    );
+                }
+            } else {
+                unimplemented!(
+                    "symlink {} {} target exists but is not a symlink",
+                    self.target,
+                    self.source
+                );
+            }
+        } else {
+            Status::Missing
+        }
+    }
+
+    fn display(&self) -> String {
+        format!("symlink {} {}", self.target, self.source)
+    }
+}
+
+struct FennelAssertion {
+    name: String,
+    args: LuaMultiValue,
+    status_fn: LuaFunction,
+}
+
+impl FennelAssertion {
+    fn new(name: &str, args: LuaMultiValue, status_fn: LuaFunction) -> Self {
+        Self {
+            name: name.to_string(),
+            args,
+            status_fn,
+        }
+    }
+}
+
+impl AssertionType for FennelAssertion {
+    fn check(&self) -> Status {
+        // Call the Lua function with args and convert result
+        match self.status_fn.call::<String>(self.args.clone()) {
+            Ok(result) => match result.as_str() {
+                "ok" => Status::Ok,
+                "missing" => Status::Missing,
+                _ => Status::Ok, // Default fallback
+            },
+            Err(_) => Status::Ok, // Default fallback on error
+        }
+    }
+
+    fn display(&self) -> String {
+        let args_str = self
+            .args
+            .iter()
+            .map(|v| v.to_string().unwrap_or_else(|_| "?".to_string()))
+            .collect::<Vec<_>>()
+            .join(" ");
+        format!("{} {}", self.name, args_str)
+    }
+}
+
+struct Frork {
+    registry: Rc<RefCell<Registry>>,
+}
+
+impl Frork {
+    fn new() -> Self {
+        let mut registry = Registry::new();
+        registry.register("symlink", |args| Ok(Box::new(Symlink::new(args)?)));
+
+        Self {
+            registry: Rc::new(RefCell::new(registry)),
+        }
+    }
+
+    fn ok(&self, args: LuaMultiValue) -> Result<()> {
+        if args.is_empty() {
+            bail!(FrorkError::NoOperation);
+        }
+
+        let mut args_iter = args.into_iter();
+        let operation = args_iter
+            .next()
+            .and_then(|v| v.to_string().ok())
+            .ok_or_else(|| anyhow!("First argument must be operation name"))?;
+
+        let assertion_args: LuaMultiValue = args_iter.collect();
+
+        match self.registry.borrow().create(&operation, assertion_args)? {
+            Some(assertion) => {
+                let status = assertion.check();
+                match status {
+                    Status::Ok => println!("ok: {}", assertion.display()),
+                    Status::Missing => println!("missing: {}", assertion.display()),
+                }
+                Ok(())
+            }
+            None => bail!(FrorkError::UnknownOperation {
+                operation: operation.to_string()
+            }),
+        }
+    }
+
+    fn register(&self, name: &str, table: LuaTable) -> Result<()> {
+        let status_fn: LuaFunction = table
+            .get("status")
+            .map_err(|e| anyhow!("Failed to get status function: {}", e))?;
+
+        let name_clone = name.to_string();
+        self.registry.borrow_mut().register(name, move |args| {
+            Ok(Box::new(FennelAssertion::new(
+                &name_clone,
+                args,
+                status_fn.clone(),
+            )))
+        });
+        info!("Registered assertion type: {}", name);
+        Ok(())
+    }
+
+    fn create_lua_module(&self, lua: &Lua) -> LuaResult<LuaTable> {
+        let frork_table = lua.create_table()?;
+        let frork = Rc::new(Self::new());
+
+        let frork_clone = frork.clone();
+        let ok_fn = lua.create_function(move |_lua, args: LuaMultiValue| {
+            frork_clone
+                .ok(args)
+                .map_err(|e| LuaError::RuntimeError(e.to_string()))
+        })?;
+
+        let frork_clone = frork.clone();
+        let register_fn = lua.create_function(move |_lua, (name, table): (String, LuaTable)| {
+            frork_clone
+                .register(&name, table)
+                .map_err(|e| LuaError::RuntimeError(e.to_string()))
+        })?;
+
+        frork_table.set("ok", ok_fn)?;
+        frork_table.set("register", register_fn)?;
+        Ok(frork_table)
+    }
+}
+
+fn main() -> Result<()> {
+    tracing_subscriber::fmt::init();
+
     let cli = Cli::parse();
 
     let lua = Lua::new();
 
     let fennel_code = include_str!("../fennel-1.6.0.lua");
-    let fennel_module = lua.load(fennel_code).eval::<LuaValue>()?;
-    lua.register_module("fennel", fennel_module)?;
+    let fennel_module = lua
+        .load(fennel_code)
+        .eval::<LuaValue>()
+        .map_err(|e| anyhow!("Failed to load Fennel: {}", e))?;
+    lua.register_module("fennel", fennel_module)
+        .map_err(|e| anyhow!("Failed to register Fennel module: {}", e))?;
+
+    let frork = Frork::new();
+    let frork_module = frork
+        .create_lua_module(&lua)
+        .map_err(|e| anyhow!("Failed to create Frork module: {}", e))?;
+    lua.register_module("frork", frork_module)
+        .map_err(|e| anyhow!("Failed to register Frork module: {}", e))?;
 
     match &cli.command {
         Commands::Check { script } => {
@@ -29,7 +296,8 @@ fn main() -> LuaResult<()> {
                 r#"require("fennel").install().dofile("{}")"#,
                 script
             ))
-            .exec()?;
+            .exec()
+            .map_err(|e| anyhow!("Failed to execute script '{}': {}", script, e))?;
         }
     }