+thiserror, +tracing, +FennelAssertion
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))?;
}
}