cuenv_ci/emitter.rs
1//! CI Pipeline Emitter Trait
2//!
3//! Defines the interface for emitting CI configurations from the intermediate
4//! representation (IR). Implementations of this trait generate orchestrator-native
5//! configurations (e.g., Buildkite, GitLab CI, Tekton).
6
7use crate::ir::IntermediateRepresentation;
8use thiserror::Error;
9
10/// Error types for emitter operations
11#[derive(Debug, Error)]
12pub enum EmitterError {
13 /// YAML/JSON serialization failed
14 #[error("Serialization failed: {0}")]
15 Serialization(String),
16
17 /// Invalid IR structure for this emitter
18 #[error("Invalid IR: {0}")]
19 InvalidIR(String),
20
21 /// Unsupported feature in IR for this emitter
22 #[error("Unsupported feature '{feature}' for {emitter} emitter")]
23 UnsupportedFeature {
24 feature: String,
25 emitter: &'static str,
26 },
27
28 /// IO error during emission
29 #[error("IO error: {0}")]
30 Io(#[from] std::io::Error),
31}
32
33/// Result type for emitter operations
34pub type EmitterResult<T> = std::result::Result<T, EmitterError>;
35
36/// Trait for CI configuration emitters
37///
38/// Implementations transform the IR into orchestrator-specific configurations.
39/// Each emitter is responsible for mapping IR concepts to the target format.
40///
41/// # Example
42///
43/// ```ignore
44/// use cuenv_ci::emitter::{Emitter, EmitterResult};
45/// use cuenv_ci::ir::IntermediateRepresentation;
46///
47/// struct MyEmitter;
48///
49/// impl Emitter for MyEmitter {
50/// fn emit(&self, ir: &IntermediateRepresentation) -> EmitterResult<String> {
51/// // Transform IR to target format
52/// Ok("# Generated CI config".to_string())
53/// }
54///
55/// fn format_name(&self) -> &'static str {
56/// "my-ci"
57/// }
58///
59/// fn file_extension(&self) -> &'static str {
60/// "yml"
61/// }
62/// }
63/// ```
64pub trait Emitter: Send + Sync {
65 /// Emit a CI configuration from the intermediate representation
66 ///
67 /// # Arguments
68 /// * `ir` - The compiled intermediate representation
69 ///
70 /// # Returns
71 /// The generated CI configuration as a string (typically YAML or JSON)
72 ///
73 /// # Errors
74 /// Returns `EmitterError` if the IR cannot be transformed or serialized
75 fn emit(&self, ir: &IntermediateRepresentation) -> EmitterResult<String>;
76
77 /// Get the format identifier for this emitter
78 ///
79 /// Used for CLI flag matching (e.g., "buildkite", "gitlab", "tekton")
80 fn format_name(&self) -> &'static str;
81
82 /// Get the file extension for output files
83 ///
84 /// Typically "yml" or "yaml" for most CI systems
85 fn file_extension(&self) -> &'static str;
86
87 /// Get a human-readable description of this emitter
88 fn description(&self) -> &'static str {
89 "CI configuration emitter"
90 }
91
92 /// Validate the IR before emission
93 ///
94 /// Override this to perform emitter-specific validation beyond
95 /// the standard IR validation.
96 ///
97 /// # Errors
98 /// Returns `EmitterError::InvalidIR` if validation fails
99 fn validate(&self, ir: &IntermediateRepresentation) -> EmitterResult<()> {
100 // Default: no additional validation
101 let _ = ir;
102 Ok(())
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109 use crate::ir::{IntermediateRepresentation, PipelineMetadata, StageConfiguration};
110
111 struct TestEmitter;
112
113 impl Emitter for TestEmitter {
114 fn emit(&self, ir: &IntermediateRepresentation) -> EmitterResult<String> {
115 Ok(format!("# Pipeline: {}", ir.pipeline.name))
116 }
117
118 fn format_name(&self) -> &'static str {
119 "test"
120 }
121
122 fn file_extension(&self) -> &'static str {
123 "yml"
124 }
125 }
126
127 #[test]
128 fn test_emitter_trait() {
129 let emitter = TestEmitter;
130 let ir = IntermediateRepresentation {
131 version: "1.4".to_string(),
132 pipeline: PipelineMetadata {
133 name: "my-pipeline".to_string(),
134 environment: None,
135 requires_onepassword: false,
136 project_name: None,
137 trigger: None,
138 },
139 runtimes: vec![],
140 stages: StageConfiguration::default(),
141 tasks: vec![],
142 };
143
144 let output = emitter.emit(&ir).unwrap();
145 assert_eq!(output, "# Pipeline: my-pipeline");
146 assert_eq!(emitter.format_name(), "test");
147 assert_eq!(emitter.file_extension(), "yml");
148 }
149
150 #[test]
151 fn test_default_validation() {
152 let emitter = TestEmitter;
153 let ir = IntermediateRepresentation {
154 version: "1.4".to_string(),
155 pipeline: PipelineMetadata {
156 name: "test".to_string(),
157 environment: None,
158 requires_onepassword: false,
159 project_name: None,
160 trigger: None,
161 },
162 runtimes: vec![],
163 stages: StageConfiguration::default(),
164 tasks: vec![],
165 };
166
167 assert!(emitter.validate(&ir).is_ok());
168 }
169}