Skip to main content

cuenv_ci/emitter/
registry.rs

1//! Emitter Registry
2//!
3//! Provides a registry for CI configuration emitters, allowing dynamic
4//! lookup and discovery of available formats.
5
6use std::collections::HashMap;
7use std::sync::Arc;
8
9use super::{Emitter, EmitterError, EmitterResult};
10use crate::ir::IntermediateRepresentation;
11
12/// Registry for CI configuration emitters.
13///
14/// Provides a central registry for all available emitters, enabling:
15/// - Dynamic lookup by format name
16/// - Enumeration of available formats
17/// - Default emitter configuration
18///
19/// # Example
20///
21/// ```ignore
22/// use cuenv_ci::emitter::{EmitterRegistry, Emitter};
23///
24/// let mut registry = EmitterRegistry::new();
25/// registry.register(Box::new(MyEmitter));
26///
27/// // Look up by name
28/// let emitter = registry.get("my-format").unwrap();
29/// let output = emitter.emit(&ir)?;
30/// ```
31#[derive(Default)]
32pub struct EmitterRegistry {
33    emitters: HashMap<&'static str, Arc<dyn Emitter>>,
34}
35
36impl EmitterRegistry {
37    /// Create a new empty registry.
38    #[must_use]
39    pub fn new() -> Self {
40        Self {
41            emitters: HashMap::new(),
42        }
43    }
44
45    /// Register an emitter.
46    ///
47    /// The emitter's `format_name()` is used as the key.
48    /// If an emitter with the same name already exists, it is replaced.
49    pub fn register(&mut self, emitter: impl Emitter + 'static) {
50        let name = emitter.format_name();
51        self.emitters.insert(name, Arc::new(emitter));
52    }
53
54    /// Register an Arc-wrapped emitter.
55    ///
56    /// Useful when the emitter is already shared.
57    pub fn register_arc(&mut self, emitter: Arc<dyn Emitter>) {
58        let name = emitter.format_name();
59        self.emitters.insert(name, emitter);
60    }
61
62    /// Get an emitter by format name.
63    #[must_use]
64    pub fn get(&self, name: &str) -> Option<Arc<dyn Emitter>> {
65        self.emitters.get(name).cloned()
66    }
67
68    /// Check if an emitter is registered.
69    #[must_use]
70    pub fn has(&self, name: &str) -> bool {
71        self.emitters.contains_key(name)
72    }
73
74    /// Get all registered format names.
75    #[must_use]
76    pub fn formats(&self) -> Vec<&'static str> {
77        let mut names: Vec<_> = self.emitters.keys().copied().collect();
78        names.sort_unstable();
79        names
80    }
81
82    /// Get all registered emitters.
83    #[must_use]
84    pub fn all(&self) -> Vec<Arc<dyn Emitter>> {
85        self.emitters.values().cloned().collect()
86    }
87
88    /// Get the number of registered emitters.
89    #[must_use]
90    pub fn len(&self) -> usize {
91        self.emitters.len()
92    }
93
94    /// Check if the registry is empty.
95    #[must_use]
96    pub fn is_empty(&self) -> bool {
97        self.emitters.is_empty()
98    }
99
100    /// Emit using a specific format.
101    ///
102    /// # Errors
103    /// Returns error if the format is not found or emission fails.
104    pub fn emit(&self, format: &str, ir: &IntermediateRepresentation) -> EmitterResult<String> {
105        let emitter = self.get(format).ok_or_else(|| {
106            EmitterError::InvalidIR(format!(
107                "Unknown format '{}'. Available: {}",
108                format,
109                self.formats().join(", ")
110            ))
111        })?;
112
113        emitter.emit(ir)
114    }
115}
116
117/// Information about a registered emitter.
118#[derive(Debug, Clone)]
119pub struct EmitterInfo {
120    /// Format name (CLI flag value).
121    pub format: &'static str,
122    /// File extension.
123    pub extension: &'static str,
124    /// Human-readable description.
125    pub description: &'static str,
126}
127
128impl EmitterInfo {
129    /// Create emitter info from an emitter.
130    #[must_use]
131    pub fn from_emitter(emitter: &dyn Emitter) -> Self {
132        Self {
133            format: emitter.format_name(),
134            extension: emitter.file_extension(),
135            description: emitter.description(),
136        }
137    }
138}
139
140impl EmitterRegistry {
141    /// Get information about all registered emitters.
142    #[must_use]
143    pub fn info(&self) -> Vec<EmitterInfo> {
144        let mut infos: Vec<_> = self
145            .emitters
146            .values()
147            .map(|e| EmitterInfo::from_emitter(e.as_ref()))
148            .collect();
149        infos.sort_by_key(|i| i.format);
150        infos
151    }
152}
153
154/// Builder for creating an emitter registry with default emitters.
155#[derive(Default)]
156pub struct EmitterRegistryBuilder {
157    registry: EmitterRegistry,
158}
159
160impl EmitterRegistryBuilder {
161    /// Create a new builder.
162    #[must_use]
163    pub fn new() -> Self {
164        Self::default()
165    }
166
167    /// Add a custom emitter.
168    #[must_use]
169    pub fn with_emitter(mut self, emitter: impl Emitter + 'static) -> Self {
170        self.registry.register(emitter);
171        self
172    }
173
174    /// Build the registry.
175    #[must_use]
176    pub fn build(self) -> EmitterRegistry {
177        self.registry
178    }
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184    use cuenv_core::ci::PipelineMode;
185
186    struct TestEmitter {
187        name: &'static str,
188    }
189
190    impl Emitter for TestEmitter {
191        fn emit_thin(&self, ir: &IntermediateRepresentation) -> EmitterResult<String> {
192            Ok(format!("# {} thin - {}", self.name, ir.pipeline.name))
193        }
194
195        fn emit_expanded(&self, ir: &IntermediateRepresentation) -> EmitterResult<String> {
196            Ok(format!("# {} expanded - {}", self.name, ir.pipeline.name))
197        }
198
199        fn format_name(&self) -> &'static str {
200            self.name
201        }
202
203        fn file_extension(&self) -> &'static str {
204            "yml"
205        }
206
207        fn description(&self) -> &'static str {
208            "Test emitter"
209        }
210    }
211
212    #[test]
213    fn test_registry_new() {
214        let registry = EmitterRegistry::new();
215        assert!(registry.is_empty());
216        assert_eq!(registry.len(), 0);
217    }
218
219    #[test]
220    fn test_registry_register() {
221        let mut registry = EmitterRegistry::new();
222        registry.register(TestEmitter { name: "test" });
223
224        assert!(!registry.is_empty());
225        assert_eq!(registry.len(), 1);
226        assert!(registry.has("test"));
227    }
228
229    #[test]
230    fn test_registry_get() {
231        let mut registry = EmitterRegistry::new();
232        registry.register(TestEmitter { name: "test" });
233
234        let emitter = registry.get("test");
235        assert!(emitter.is_some());
236        assert_eq!(emitter.unwrap().format_name(), "test");
237
238        assert!(registry.get("nonexistent").is_none());
239    }
240
241    #[test]
242    fn test_registry_formats() {
243        let mut registry = EmitterRegistry::new();
244        registry.register(TestEmitter { name: "buildkite" });
245        registry.register(TestEmitter { name: "gitlab" });
246        registry.register(TestEmitter { name: "circleci" });
247
248        let formats = registry.formats();
249        assert_eq!(formats, vec!["buildkite", "circleci", "gitlab"]);
250    }
251
252    #[test]
253    fn test_registry_emit() {
254        let mut registry = EmitterRegistry::new();
255        registry.register(TestEmitter { name: "test" });
256
257        // Default mode is Thin, so emit() dispatches to emit_thin()
258        let ir = IntermediateRepresentation {
259            version: "1.5".to_string(),
260            pipeline: crate::ir::PipelineMetadata {
261                name: "my-pipeline".to_string(),
262                mode: PipelineMode::default(),
263                environment: None,
264                requires_onepassword: false,
265                project_name: None,
266                trigger: None,
267                pipeline_tasks: vec![],
268                pipeline_task_defs: vec![],
269            },
270            runtimes: vec![],
271            tasks: vec![],
272        };
273
274        let output = registry.emit("test", &ir).unwrap();
275        assert_eq!(output, "# test thin - my-pipeline");
276    }
277
278    #[test]
279    fn test_registry_emit_unknown_format() {
280        let registry = EmitterRegistry::new();
281        let ir = IntermediateRepresentation {
282            version: "1.5".to_string(),
283            pipeline: crate::ir::PipelineMetadata {
284                name: "test".to_string(),
285                mode: PipelineMode::default(),
286                environment: None,
287                requires_onepassword: false,
288                project_name: None,
289                trigger: None,
290                pipeline_tasks: vec![],
291                pipeline_task_defs: vec![],
292            },
293            runtimes: vec![],
294            tasks: vec![],
295        };
296
297        let result = registry.emit("unknown", &ir);
298        assert!(result.is_err());
299        assert!(result.unwrap_err().to_string().contains("Unknown format"));
300    }
301
302    #[test]
303    fn test_registry_info() {
304        let mut registry = EmitterRegistry::new();
305        registry.register(TestEmitter { name: "test" });
306
307        let infos = registry.info();
308        assert_eq!(infos.len(), 1);
309        assert_eq!(infos[0].format, "test");
310        assert_eq!(infos[0].extension, "yml");
311        assert_eq!(infos[0].description, "Test emitter");
312    }
313
314    #[test]
315    fn test_registry_register_replaces() {
316        let mut registry = EmitterRegistry::new();
317        registry.register(TestEmitter { name: "test" });
318        registry.register(TestEmitter { name: "test" });
319
320        assert_eq!(registry.len(), 1);
321    }
322}