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