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}