cuenv_core/tools/
registry.rs

1//! Tool provider registry.
2//!
3//! This module provides a registry for tool providers, allowing multiple
4//! providers to be registered and looked up by name or source type.
5
6use std::collections::HashMap;
7use std::sync::Arc;
8
9use super::provider::{ToolProvider, ToolSource};
10
11/// Registry of tool providers.
12///
13/// The registry maintains a collection of tool providers and provides
14/// lookup by name or source type.
15#[derive(Default)]
16pub struct ToolRegistry {
17    /// Providers indexed by name.
18    providers: HashMap<&'static str, Arc<dyn ToolProvider>>,
19}
20
21impl ToolRegistry {
22    /// Create a new empty registry.
23    #[must_use]
24    pub fn new() -> Self {
25        Self::default()
26    }
27
28    /// Register a tool provider.
29    ///
30    /// If a provider with the same name already exists, it will be replaced.
31    pub fn register<P: ToolProvider + 'static>(&mut self, provider: P) {
32        let name = provider.name();
33        self.providers.insert(name, Arc::new(provider));
34    }
35
36    /// Register a tool provider wrapped in Arc.
37    ///
38    /// Useful when the same provider instance needs to be shared.
39    pub fn register_arc(&mut self, provider: Arc<dyn ToolProvider>) {
40        let name = provider.name();
41        self.providers.insert(name, provider);
42    }
43
44    /// Get a provider by name.
45    #[must_use]
46    pub fn get(&self, name: &str) -> Option<&Arc<dyn ToolProvider>> {
47        self.providers.get(name)
48    }
49
50    /// Find a provider that can handle the given source.
51    #[must_use]
52    pub fn find_for_source(&self, source: &ToolSource) -> Option<&Arc<dyn ToolProvider>> {
53        self.providers.values().find(|p| p.can_handle(source))
54    }
55
56    /// Iterate over all registered providers.
57    pub fn iter(&self) -> impl Iterator<Item = &Arc<dyn ToolProvider>> {
58        self.providers.values()
59    }
60
61    /// Get the number of registered providers.
62    #[must_use]
63    pub fn len(&self) -> usize {
64        self.providers.len()
65    }
66
67    /// Check if the registry is empty.
68    #[must_use]
69    pub fn is_empty(&self) -> bool {
70        self.providers.is_empty()
71    }
72
73    /// Get all provider names.
74    #[must_use]
75    pub fn names(&self) -> Vec<&'static str> {
76        self.providers.keys().copied().collect()
77    }
78}
79
80impl std::fmt::Debug for ToolRegistry {
81    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82        f.debug_struct("ToolRegistry")
83            .field("providers", &self.names())
84            .finish()
85    }
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91    use crate::tools::provider::{FetchedTool, Platform, ResolvedTool, ToolOptions};
92    use async_trait::async_trait;
93
94    struct MockProvider {
95        name: &'static str,
96    }
97
98    #[async_trait]
99    impl ToolProvider for MockProvider {
100        fn name(&self) -> &'static str {
101            self.name
102        }
103
104        fn description(&self) -> &'static str {
105            "Mock provider for testing"
106        }
107
108        fn can_handle(&self, source: &ToolSource) -> bool {
109            matches!(source, ToolSource::GitHub { .. }) && self.name == "github"
110        }
111
112        async fn resolve(
113            &self,
114            _tool_name: &str,
115            _version: &str,
116            _platform: &Platform,
117            _config: &serde_json::Value,
118        ) -> crate::Result<ResolvedTool> {
119            unimplemented!()
120        }
121
122        async fn fetch(
123            &self,
124            _resolved: &ResolvedTool,
125            _options: &ToolOptions,
126        ) -> crate::Result<FetchedTool> {
127            unimplemented!()
128        }
129
130        fn is_cached(&self, _resolved: &ResolvedTool, _options: &ToolOptions) -> bool {
131            false
132        }
133    }
134
135    #[test]
136    fn test_registry_register_and_get() {
137        let mut registry = ToolRegistry::new();
138        registry.register(MockProvider { name: "github" });
139
140        assert!(registry.get("github").is_some());
141        assert!(registry.get("nix").is_none());
142    }
143
144    #[test]
145    fn test_registry_find_for_source() {
146        let mut registry = ToolRegistry::new();
147        registry.register(MockProvider { name: "github" });
148
149        let source = ToolSource::GitHub {
150            repo: "org/repo".into(),
151            tag: "v1".into(),
152            asset: "file.zip".into(),
153            path: None,
154        };
155        assert!(registry.find_for_source(&source).is_some());
156
157        let source = ToolSource::Nix {
158            flake: "nixpkgs".into(),
159            package: "jq".into(),
160            output: None,
161        };
162        assert!(registry.find_for_source(&source).is_none());
163    }
164
165    #[test]
166    fn test_registry_iter() {
167        let mut registry = ToolRegistry::new();
168        registry.register(MockProvider { name: "github" });
169        registry.register(MockProvider { name: "nix" });
170
171        assert_eq!(registry.len(), 2);
172        assert!(!registry.is_empty());
173
174        let names: Vec<_> = registry.iter().map(|p| p.name()).collect();
175        assert!(names.contains(&"github"));
176        assert!(names.contains(&"nix"));
177    }
178
179    #[test]
180    fn test_registry_new() {
181        let registry = ToolRegistry::new();
182        assert!(registry.is_empty());
183        assert_eq!(registry.len(), 0);
184    }
185
186    #[test]
187    fn test_registry_default() {
188        let registry = ToolRegistry::default();
189        assert!(registry.is_empty());
190        assert_eq!(registry.len(), 0);
191    }
192
193    #[test]
194    fn test_registry_register_replaces_existing() {
195        let mut registry = ToolRegistry::new();
196        registry.register(MockProvider { name: "github" });
197        registry.register(MockProvider { name: "github" });
198
199        // Should still have only one provider
200        assert_eq!(registry.len(), 1);
201    }
202
203    #[test]
204    fn test_registry_register_arc() {
205        let mut registry = ToolRegistry::new();
206        let provider: Arc<dyn ToolProvider> = Arc::new(MockProvider { name: "github" });
207        registry.register_arc(provider);
208
209        assert!(registry.get("github").is_some());
210        assert_eq!(registry.len(), 1);
211    }
212
213    #[test]
214    fn test_registry_register_arc_replaces_existing() {
215        let mut registry = ToolRegistry::new();
216        registry.register(MockProvider { name: "github" });
217
218        let provider: Arc<dyn ToolProvider> = Arc::new(MockProvider { name: "github" });
219        registry.register_arc(provider);
220
221        // Should still have only one provider
222        assert_eq!(registry.len(), 1);
223    }
224
225    #[test]
226    fn test_registry_names() {
227        let mut registry = ToolRegistry::new();
228        registry.register(MockProvider { name: "github" });
229        registry.register(MockProvider { name: "nix" });
230        registry.register(MockProvider { name: "rustup" });
231
232        let names = registry.names();
233        assert_eq!(names.len(), 3);
234        assert!(names.contains(&"github"));
235        assert!(names.contains(&"nix"));
236        assert!(names.contains(&"rustup"));
237    }
238
239    #[test]
240    fn test_registry_names_empty() {
241        let registry = ToolRegistry::new();
242        let names = registry.names();
243        assert!(names.is_empty());
244    }
245
246    #[test]
247    fn test_registry_debug() {
248        let mut registry = ToolRegistry::new();
249        registry.register(MockProvider { name: "github" });
250
251        let debug_str = format!("{:?}", registry);
252        assert!(debug_str.contains("ToolRegistry"));
253        assert!(debug_str.contains("providers"));
254        assert!(debug_str.contains("github"));
255    }
256
257    #[test]
258    fn test_registry_debug_empty() {
259        let registry = ToolRegistry::new();
260        let debug_str = format!("{:?}", registry);
261        assert!(debug_str.contains("ToolRegistry"));
262    }
263
264    #[test]
265    fn test_registry_find_for_source_no_match() {
266        let mut registry = ToolRegistry::new();
267        // Register github provider that only handles GitHub sources
268        registry.register(MockProvider { name: "github" });
269
270        // Try to find a provider for a Rustup source - should return None
271        let source = ToolSource::Rustup {
272            toolchain: "stable".into(),
273            profile: None,
274            components: vec![],
275            targets: vec![],
276        };
277        assert!(registry.find_for_source(&source).is_none());
278    }
279
280    #[test]
281    fn test_registry_find_for_source_oci() {
282        let registry = ToolRegistry::new();
283
284        let source = ToolSource::Oci {
285            image: "alpine:latest".into(),
286            path: "bin/sh".into(),
287        };
288        // Empty registry has no provider
289        assert!(registry.find_for_source(&source).is_none());
290    }
291
292    #[test]
293    fn test_registry_get_nonexistent() {
294        let registry = ToolRegistry::new();
295        assert!(registry.get("nonexistent").is_none());
296        assert!(registry.get("github").is_none());
297        assert!(registry.get("").is_none());
298    }
299
300    #[test]
301    fn test_registry_iter_empty() {
302        let registry = ToolRegistry::new();
303        let count = registry.iter().count();
304        assert_eq!(count, 0);
305    }
306}