1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
//! Tool registry for mapping tool names to installation handlers.
//!
//! # Thread Safety
//!
//! The registry uses an `RwLock` for thread-safe access. Lock poisoning is treated
//! as a fatal error (panic) because:
//!
//! 1. This is a CLI tool, not a long-running service - recovery is unnecessary
//! 2. A poisoned lock indicates a prior panic during registration, which is a bug
//! 3. Continuing with potentially corrupted state would be unsafe
//!
//! The `expect()` calls on lock acquisition are intentional and document this design.
#![allow(dead_code)] // Public API for tool registration and lookup
use std::collections::HashMap;
use std::sync::{OnceLock, RwLock};
use crate::tools::common::InstallError;
/// Function signature for tool installation handlers.
pub type ToolAdder = fn(version: &str) -> Result<(), InstallError>;
/// Global registry mapping tool name -> handler.
/// Keys are stored in lowercase for case-insensitive lookups.
static REGISTRY: OnceLock<RwLock<HashMap<String, ToolAdder>>> = OnceLock::new();
#[inline]
fn registry() -> &'static RwLock<HashMap<String, ToolAdder>> {
REGISTRY.get_or_init(|| RwLock::new(HashMap::new()))
}
/// Register a tool handler under the given name.
/// Returns true if a new entry was inserted, false if an existing entry was replaced.
#[must_use = "indicates whether entry was new or replaced"]
pub fn register_tool(name: &str, handler: ToolAdder) -> bool {
let key = name.to_ascii_lowercase();
let mut map = registry().write().expect("registry rwlock poisoned");
map.insert(key, handler).is_none()
}
/// Retrieve a registered tool handler by name, if present.
/// Lookup is case-insensitive.
#[inline]
pub fn get_tool(name: &str) -> Option<ToolAdder> {
let key = name.to_ascii_lowercase();
let map = registry().read().expect("registry rwlock poisoned");
map.get(&key).copied()
}
/// List all registered tool names (lowercased), sorted for determinism.
pub fn registered_tool_names() -> Vec<String> {
let map = registry().read().expect("registry rwlock poisoned");
let mut names: Vec<String> = map.keys().cloned().collect();
names.sort();
names
}
/// Dispatch an added request to a registered tool by name and version.
/// Example: add("git", "latest") or add("docker", "24.01").
///
/// Resolution order:
/// 1. User-defined plugin tools (`~/.jarvy/tools.d/`). Plugins dispatch by
/// their declared `name` so the package fields belonging to that plugin
/// are the ones executed — no shared-handler ambiguity.
/// 2. Built-in tool registry.
///
/// Returns `InstallError::Parse("unknown tool")` if neither has a handler.
#[must_use = "this Result may contain an error that should be handled"]
pub fn add(name: &str, version: &str) -> Result<(), InstallError> {
// Plugins first: name-keyed dispatch is correct by construction.
match crate::tools::plugins::install_by_name(name, version) {
Ok(true) => return Ok(()),
Ok(false) => {}
Err(e) => return Err(e),
}
let key = name.to_ascii_lowercase();
let map = registry().read().expect("registry rwlock poisoned");
if let Some(handler) = map.get(&key) {
// clone the function pointer out while holding read lock
let f = *handler;
drop(map);
f(version)
} else {
Err(InstallError::Parse("unknown tool"))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn dummy_handler(_version: &str) -> Result<(), InstallError> {
Ok(())
}
#[test]
fn get_tool_returns_some_after_register() {
let name = "TeStToOl_get";
let _ = register_tool(name, dummy_handler);
let h = get_tool("testtool_get");
assert!(h.is_some());
let f = h.unwrap();
assert!(f("any").is_ok());
}
#[test]
fn get_tool_returns_none_for_unknown() {
let h = get_tool("definitely-unknown-tool-name");
assert!(h.is_none());
}
}
// Tool struct for registry usage: wraps a name and an add handler
#[derive(Clone)]
pub struct Tool {
pub name: String,
handler: ToolAdder,
}
impl Tool {
pub fn new(name: &str, handler: ToolAdder) -> Self {
Self {
name: name.to_string(),
handler,
}
}
/// Invoke this tool's add/install logic for a given version hint
pub fn add(&self, version: &str) -> Result<(), InstallError> {
(self.handler)(version)
}
}
/// Register a Tool by extracting its name and handler.
/// Returns true if inserted new, false if replaced existing.
pub fn register_tool_struct(tool: &Tool) -> bool {
register_tool(&tool.name, tool.handler)
}
/// Retrieve a Tool by name if present (case-insensitive).
/// The returned Tool carries the looked-up name in lowercase for consistency.
pub fn get_tool_struct(name: &str) -> Option<Tool> {
get_tool(name).map(|handler| Tool {
name: name.to_ascii_lowercase(),
handler,
})
}
#[cfg(test)]
mod tests2 {
use super::*;
fn dummy_struct_handler(_version: &str) -> Result<(), InstallError> {
Ok(())
}
#[test]
fn register_and_use_tool_struct() {
let my = Tool::new("TeStToOl_struct", dummy_struct_handler);
let _ = register_tool_struct(&my);
let fetched = get_tool_struct("testtool_struct").expect("tool should be present");
assert_eq!(fetched.name, "testtool_struct");
assert!(fetched.add("any").is_ok());
}
}