Expose CI job registration via quire.ci Lua module
Replaces the global `job` function with a UserData-based `quire.ci`
module. Scripts now (require :quire.ci) and call (ci:job ...) instead
of relying on an injected global. Jobs accumulate directly in the
UserData via Rc<RefCell<Vec<Job>>>, removing the intermediate Lua
table extraction.

Assisted-by: GLM-5.1 via pi
change pxzxxqwpwvnmussxorrwlwzvtnqtmtmm
commit 741ec61375a228a9eb4ed892ad1ccdf3c8181c7b
author Alpha Chen <alpha@kejadlen.dev>
date
parent rnroxpvu
diff --git a/src/ci/pipeline.rs b/src/ci/pipeline.rs
index faa0b09..44ec8b2 100644
--- a/src/ci/pipeline.rs
+++ b/src/ci/pipeline.rs
@@ -1,7 +1,12 @@
 //! CI job graph: evaluation of `ci.fnl` and validation rules.
 
+use std::cell::RefCell;
+use std::rc::Rc;
+
+use mlua::UserData;
+
 use crate::Result;
-use crate::fennel::{Fennel, FennelError};
+use crate::fennel::Fennel;
 
 /// A registered job extracted from ci.fnl.
 pub struct Job {
@@ -36,50 +41,44 @@ impl Pipeline {
     }
 }
 
-/// Evaluate a ci.fnl source string, registering jobs via the `job` macro.
+/// The `quire.ci` module exposed to Fennel scripts via `require`.
 ///
-/// Injects a `job` global that accumulates into a registration table,
-/// evaluates the source, and extracts the registered jobs.
-pub(crate) fn eval_ci(fennel: &Fennel, source: &str, name: &str) -> Result<Pipeline> {
-    fennel.eval_raw(source, name, |lua| {
-        // Create a registration table. `job` will push into this.
-        let registry: mlua::Table = lua.create_table()?;
-        lua.globals().set("_quire_jobs", registry)?;
-
-        // Define the `job` global: (job id inputs run-fn)
-        let job_fn = lua.create_function(
-            |lua, (id, inputs, run_fn): (mlua::String, mlua::Table, mlua::Function)| {
-                let registry: mlua::Table = lua.globals().get("_quire_jobs")?;
-                let entry = lua.create_table()?;
-                entry.set("id", id)?;
-                entry.set("inputs", inputs)?;
-                entry.set("run", run_fn)?;
-                registry.push(entry)?;
+/// Registered as `package.loaded["quire.ci"]` so scripts can write:
+///
+/// ```fennel
+/// (local ci (require :quire.ci))
+/// (ci:job :build [:quire/push] (fn [_] nil))
+/// ```
+struct CiModule {
+    jobs: Rc<RefCell<Vec<Job>>>,
+}
+
+impl UserData for CiModule {
+    fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M) {
+        methods.add_method(
+            "job",
+            |_, this, (id, inputs, run_fn): (String, Vec<String>, mlua::Function)| {
+                this.jobs.borrow_mut().push(Job { id, inputs, run_fn });
                 Ok(())
             },
-        )?;
-        lua.globals().set("job", job_fn)?;
+        );
+    }
+}
+
+/// Evaluate a ci.fnl source string, registering jobs via the `quire.ci` module.
+///
+/// Injects `quire.ci` into `package.loaded` so scripts can
+/// `(require :quire.ci)`, evaluates the source, and takes the accumulated jobs.
+pub(crate) fn eval_ci(fennel: &Fennel, source: &str, name: &str) -> Result<Pipeline> {
+    let jobs = Rc::new(RefCell::new(Vec::new()));
 
+    fennel.eval_raw(source, name, |lua| {
+        let loaded: mlua::Table = lua.globals().get::<mlua::Table>("package")?.get("loaded")?;
+        loaded.set("quire.ci", CiModule { jobs: jobs.clone() })?;
         Ok(())
     })?;
 
-    // Extract the registration table.
-    let lua_err = |e: mlua::Error| FennelError::from_lua(source, name, e);
-    let registry: mlua::Table = fennel.lua().globals().get("_quire_jobs").map_err(lua_err)?;
-    let mut jobs = Vec::new();
-    for entry in registry.sequence_values::<mlua::Table>() {
-        let entry = entry.map_err(lua_err)?;
-        let id: String = entry.get("id").map_err(lua_err)?;
-        let inputs_table: mlua::Table = entry.get("inputs").map_err(lua_err)?;
-        let run_fn: mlua::Function = entry.get("run").map_err(lua_err)?;
-        let mut inputs = Vec::new();
-        for input in inputs_table.sequence_values::<String>() {
-            inputs.push(input.map_err(lua_err)?);
-        }
-        jobs.push(Job { id, inputs, run_fn });
-    }
-
-    Ok(Pipeline { jobs })
+    Ok(Pipeline { jobs: jobs.take() })
 }
 
 /// A validation error found in the job graph.
@@ -206,7 +205,8 @@ mod tests {
     #[test]
     fn eval_ci_registers_a_job() {
         let f = fennel();
-        let source = r#"(job :test [:quire/push] (fn [_] nil))"#;
+        let source = r#"(local ci (require :quire.ci))
+(ci:job :test [:quire/push] (fn [_] nil))"#;
         let result = eval_ci(&f, source, "ci.fnl").expect("eval should succeed");
         assert_eq!(result.jobs.len(), 1);
         assert_eq!(result.jobs[0].id, "test");
@@ -217,8 +217,9 @@ mod tests {
     fn eval_ci_registers_multiple_jobs() {
         let f = fennel();
         let source = r#"
-(job :build [:quire/push] (fn [_] nil))
-(job :test [:build] (fn [_] nil))
+(local ci (require :quire.ci))
+(ci:job :build [:quire/push] (fn [_] nil))
+(ci:job :test [:build] (fn [_] nil))
 "#;
         let result = eval_ci(&f, source, "ci.fnl").expect("eval should succeed");
         assert_eq!(result.jobs.len(), 2);
@@ -239,8 +240,9 @@ mod tests {
     fn validate_accepts_valid_config() {
         let f = fennel();
         let source = r#"
-(job :build [:quire/push] (fn [_] nil))
-(job :test [:build :quire/push] (fn [_] nil))
+(local ci (require :quire.ci))
+(ci:job :build [:quire/push] (fn [_] nil))
+(ci:job :test [:build :quire/push] (fn [_] nil))
 "#;
         let pipeline = eval_ci(&f, source, "ci.fnl").expect("eval should succeed");
         assert!(pipeline.validate().is_ok());
@@ -250,8 +252,9 @@ mod tests {
     fn validate_rejects_cycle() {
         let f = fennel();
         let source = r#"
-(job :a [:b] (fn [_] nil))
-(job :b [:a] (fn [_] nil))
+(local ci (require :quire.ci))
+(ci:job :a [:b] (fn [_] nil))
+(ci:job :b [:a] (fn [_] nil))
 "#;
         let pipeline = eval_ci(&f, source, "ci.fnl").expect("eval should succeed");
         let errs = pipeline.validate().unwrap_err();
@@ -265,9 +268,10 @@ mod tests {
     fn validate_cycle_only_reports_cycle_members() {
         let f = fennel();
         let source = r#"
-(job :a [:b :quire/push] (fn [_] nil))
-(job :b [:a :quire/push] (fn [_] nil))
-(job :clean [:quire/push] (fn [_] nil))
+(local ci (require :quire.ci))
+(ci:job :a [:b :quire/push] (fn [_] nil))
+(ci:job :b [:a :quire/push] (fn [_] nil))
+(ci:job :clean [:quire/push] (fn [_] nil))
 "#;
         let pipeline = eval_ci(&f, source, "ci.fnl").expect("eval should succeed");
         let errs = pipeline.validate().unwrap_err();
@@ -290,10 +294,11 @@ mod tests {
     fn validate_reports_disjoint_cycles_separately() {
         let f = fennel();
         let source = r#"
-(job :a [:b :quire/push] (fn [_] nil))
-(job :b [:a :quire/push] (fn [_] nil))
-(job :c [:d :quire/push] (fn [_] nil))
-(job :d [:c :quire/push] (fn [_] nil))
+(local ci (require :quire.ci))
+(ci:job :a [:b :quire/push] (fn [_] nil))
+(ci:job :b [:a :quire/push] (fn [_] nil))
+(ci:job :c [:d :quire/push] (fn [_] nil))
+(ci:job :d [:c :quire/push] (fn [_] nil))
 "#;
         let pipeline = eval_ci(&f, source, "ci.fnl").expect("eval should succeed");
         let errs = pipeline.validate().unwrap_err();
@@ -307,7 +312,8 @@ mod tests {
     #[test]
     fn validate_rejects_empty_inputs() {
         let f = fennel();
-        let source = r#"(job :setup [] (fn [_] nil))"#;
+        let source = r#"(local ci (require :quire.ci))
+(ci:job :setup [] (fn [_] nil))"#;
         let pipeline = eval_ci(&f, source, "ci.fnl").expect("eval should succeed");
         let errs = pipeline.validate().unwrap_err();
         assert!(
@@ -320,7 +326,8 @@ mod tests {
     #[test]
     fn validate_rejects_unreachable_jobs() {
         let f = fennel();
-        let source = r#"(job :orphan [:orphan] (fn [_] nil))"#;
+        let source = r#"(local ci (require :quire.ci))
+(ci:job :orphan [:orphan] (fn [_] nil))"#;
         let pipeline = eval_ci(&f, source, "ci.fnl").expect("eval should succeed");
         let errs = pipeline.validate().unwrap_err();
         assert!(
@@ -334,7 +341,8 @@ mod tests {
     #[test]
     fn validate_rejects_slash_in_job_id() {
         let f = fennel();
-        let source = r#"(job :foo/bar [:quire/push] (fn [_] nil))"#;
+        let source = r#"(local ci (require :quire.ci))
+(ci:job :foo/bar [:quire/push] (fn [_] nil))"#;
         let pipeline = eval_ci(&f, source, "ci.fnl").expect("eval should succeed");
         let errs = pipeline.validate().unwrap_err();
         assert!(
diff --git a/src/fennel.rs b/src/fennel.rs
index 0466b4b..2647ae5 100644
--- a/src/fennel.rs
+++ b/src/fennel.rs
@@ -52,14 +52,6 @@ pub struct Fennel {
 }
 
 impl Fennel {
-    /// Access the underlying Lua VM.
-    ///
-    /// Needed for extracting evaluation results that don't deserialize
-    /// directly into a Rust type (e.g. the CI job registry).
-    pub(crate) fn lua(&self) -> &Lua {
-        &self.lua
-    }
-
     /// Create a new Fennel instance.
     ///
     /// Loads the vendored `fennel.lua` into a fresh Lua VM and registers it