Skip to main content

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::{
92        FetchedTool, ResolvedTool, ToolOptions, ToolResolveRequest,
93    };
94    use async_trait::async_trait;
95
96    struct MockProvider {
97        name: &'static str,
98    }
99
100    #[async_trait]
101    impl ToolProvider for MockProvider {
102        fn name(&self) -> &'static str {
103            self.name
104        }
105
106        fn description(&self) -> &'static str {
107            "Mock provider for testing"
108        }
109
110        fn can_handle(&self, source: &ToolSource) -> bool {
111            matches!(source, ToolSource::GitHub { .. }) && self.name == "github"
112        }
113
114        async fn resolve(&self, _request: &ToolResolveRequest<'_>) -> crate::Result<ResolvedTool> {
115            unimplemented!()
116        }
117
118        async fn fetch(
119            &self,
120            _resolved: &ResolvedTool,
121            _options: &ToolOptions,
122        ) -> crate::Result<FetchedTool> {
123            unimplemented!()
124        }
125
126        fn is_cached(&self, _resolved: &ResolvedTool, _options: &ToolOptions) -> bool {
127            false
128        }
129    }
130
131    #[test]
132    fn test_registry_register_and_get() {
133        let mut registry = ToolRegistry::new();
134        registry.register(MockProvider { name: "github" });
135
136        assert!(registry.get("github").is_some());
137        assert!(registry.get("nix").is_none());
138    }
139
140    #[test]
141    fn test_registry_find_for_source() {
142        let mut registry = ToolRegistry::new();
143        registry.register(MockProvider { name: "github" });
144
145        let source = ToolSource::GitHub {
146            repo: "org/repo".into(),
147            tag: "v1".into(),
148            asset: "file.zip".into(),
149            path: None,
150        };
151        assert!(registry.find_for_source(&source).is_some());
152
153        let source = ToolSource::Nix {
154            flake: "nixpkgs".into(),
155            package: "jq".into(),
156            output: None,
157        };
158        assert!(registry.find_for_source(&source).is_none());
159    }
160
161    #[test]
162    fn test_registry_iter() {
163        let mut registry = ToolRegistry::new();
164        registry.register(MockProvider { name: "github" });
165        registry.register(MockProvider { name: "nix" });
166
167        assert_eq!(registry.len(), 2);
168        assert!(!registry.is_empty());
169
170        let names: Vec<_> = registry.iter().map(|p| p.name()).collect();
171        assert!(names.contains(&"github"));
172        assert!(names.contains(&"nix"));
173    }
174
175    #[test]
176    fn test_registry_new() {
177        let registry = ToolRegistry::new();
178        assert!(registry.is_empty());
179        assert_eq!(registry.len(), 0);
180    }
181
182    #[test]
183    fn test_registry_default() {
184        let registry = ToolRegistry::default();
185        assert!(registry.is_empty());
186        assert_eq!(registry.len(), 0);
187    }
188
189    #[test]
190    fn test_registry_register_replaces_existing() {
191        let mut registry = ToolRegistry::new();
192        registry.register(MockProvider { name: "github" });
193        registry.register(MockProvider { name: "github" });
194
195        // Should still have only one provider
196        assert_eq!(registry.len(), 1);
197    }
198
199    #[test]
200    fn test_registry_register_arc() {
201        let mut registry = ToolRegistry::new();
202        let provider: Arc<dyn ToolProvider> = Arc::new(MockProvider { name: "github" });
203        registry.register_arc(provider);
204
205        assert!(registry.get("github").is_some());
206        assert_eq!(registry.len(), 1);
207    }
208
209    #[test]
210    fn test_registry_register_arc_replaces_existing() {
211        let mut registry = ToolRegistry::new();
212        registry.register(MockProvider { name: "github" });
213
214        let provider: Arc<dyn ToolProvider> = Arc::new(MockProvider { name: "github" });
215        registry.register_arc(provider);
216
217        // Should still have only one provider
218        assert_eq!(registry.len(), 1);
219    }
220
221    #[test]
222    fn test_registry_names() {
223        let mut registry = ToolRegistry::new();
224        registry.register(MockProvider { name: "github" });
225        registry.register(MockProvider { name: "nix" });
226        registry.register(MockProvider { name: "rustup" });
227
228        let names = registry.names();
229        assert_eq!(names.len(), 3);
230        assert!(names.contains(&"github"));
231        assert!(names.contains(&"nix"));
232        assert!(names.contains(&"rustup"));
233    }
234
235    #[test]
236    fn test_registry_names_empty() {
237        let registry = ToolRegistry::new();
238        let names = registry.names();
239        assert!(names.is_empty());
240    }
241
242    #[test]
243    fn test_registry_debug() {
244        let mut registry = ToolRegistry::new();
245        registry.register(MockProvider { name: "github" });
246
247        let debug_str = format!("{:?}", registry);
248        assert!(debug_str.contains("ToolRegistry"));
249        assert!(debug_str.contains("providers"));
250        assert!(debug_str.contains("github"));
251    }
252
253    #[test]
254    fn test_registry_debug_empty() {
255        let registry = ToolRegistry::new();
256        let debug_str = format!("{:?}", registry);
257        assert!(debug_str.contains("ToolRegistry"));
258    }
259
260    #[test]
261    fn test_registry_find_for_source_no_match() {
262        let mut registry = ToolRegistry::new();
263        // Register github provider that only handles GitHub sources
264        registry.register(MockProvider { name: "github" });
265
266        // Try to find a provider for a Rustup source - should return None
267        let source = ToolSource::Rustup {
268            toolchain: "stable".into(),
269            profile: None,
270            components: vec![],
271            targets: vec![],
272        };
273        assert!(registry.find_for_source(&source).is_none());
274    }
275
276    #[test]
277    fn test_registry_find_for_source_oci() {
278        let registry = ToolRegistry::new();
279
280        let source = ToolSource::Oci {
281            image: "alpine:latest".into(),
282            path: "bin/sh".into(),
283        };
284        // Empty registry has no provider
285        assert!(registry.find_for_source(&source).is_none());
286    }
287
288    #[test]
289    fn test_registry_get_nonexistent() {
290        let registry = ToolRegistry::new();
291        assert!(registry.get("nonexistent").is_none());
292        assert!(registry.get("github").is_none());
293        assert!(registry.get("").is_none());
294    }
295
296    #[test]
297    fn test_registry_iter_empty() {
298        let registry = ToolRegistry::new();
299        let count = registry.iter().count();
300        assert_eq!(count, 0);
301    }
302}