Skip to main content

cuenv/providers/
codegen.rs

1//! Codegen sync provider.
2//!
3//! Syncs codegen-generated files from CUE configuration.
4
5use std::any::Any;
6use std::path::Path;
7
8use async_trait::async_trait;
9use clap::{Arg, Command, arg};
10use cuenv_core::Result;
11use cuenv_core::manifest::{Base, Project};
12
13use crate::commands::CommandExecutor;
14use crate::commands::sync::functions;
15use crate::commands::sync::provider::{SyncMode, SyncOptions, SyncResult};
16use crate::provider::{Provider, SyncCapability};
17
18/// Codegen sync provider.
19///
20/// Syncs codegen-generated files from CUE configuration. Codegen modules are
21/// reusable templates that generate project files.
22///
23/// # Example
24///
25/// ```ignore
26/// use cuenv::Cuenv;
27/// use cuenv::providers::CodegenProvider;
28///
29/// Cuenv::builder()
30///     .with_sync_provider(CodegenProvider::new())
31///     .build()
32///     .run()
33/// ```
34pub struct CodegenProvider;
35
36impl CodegenProvider {
37    /// Create a new codegen provider.
38    #[must_use]
39    pub fn new() -> Self {
40        Self
41    }
42}
43
44impl Default for CodegenProvider {
45    fn default() -> Self {
46        Self::new()
47    }
48}
49
50impl Provider for CodegenProvider {
51    fn name(&self) -> &'static str {
52        "codegen"
53    }
54
55    fn description(&self) -> &'static str {
56        "Sync files from CUE codegen configurations"
57    }
58
59    fn as_any(&self) -> &dyn Any {
60        self
61    }
62
63    fn as_any_mut(&mut self) -> &mut dyn Any {
64        self
65    }
66}
67
68#[async_trait]
69impl SyncCapability for CodegenProvider {
70    fn build_sync_command(&self) -> Command {
71        Command::new(self.name())
72            .about(self.description())
73            .arg(arg!(-p --path <PATH> "Path to directory containing CUE files").default_value("."))
74            .arg(
75                Arg::new("package")
76                    .long("package")
77                    .help("Name of the CUE package to evaluate")
78                    .default_value("cuenv"),
79            )
80            .arg(arg!(--"dry-run" "Show what would be generated without writing files"))
81            .arg(arg!(--check "Check if files are in sync without making changes"))
82            .arg(arg!(-A --all "Sync all projects in the workspace"))
83            .arg(arg!(--diff "Show diff for files that would change"))
84    }
85
86    async fn sync_path(
87        &self,
88        path: &Path,
89        package: &str,
90        options: &SyncOptions,
91        executor: &CommandExecutor,
92    ) -> Result<SyncResult> {
93        let dry_run = options.mode == SyncMode::DryRun;
94        let check = options.mode == SyncMode::Check;
95
96        let path_str = path.to_str().ok_or_else(|| {
97            cuenv_core::Error::configuration(format!(
98                "Path contains invalid UTF-8: {}",
99                path.display()
100            ))
101        })?;
102
103        let codegen_options = functions::CodegenSyncOptions {
104            dry_run: dry_run.into(),
105            check,
106            diff: options.show_diff,
107        };
108        let request = functions::CodegenSyncRequest {
109            path: path_str,
110            package,
111            options: codegen_options,
112        };
113        let output = functions::execute_sync_codegen(request, executor).await?;
114
115        Ok(SyncResult::success(output))
116    }
117
118    async fn sync_workspace(
119        &self,
120        package: &str,
121        options: &SyncOptions,
122        executor: &CommandExecutor,
123    ) -> Result<SyncResult> {
124        let dry_run = options.mode == SyncMode::DryRun;
125        let check = options.mode == SyncMode::Check;
126
127        let cwd = std::env::current_dir().map_err(|e| {
128            cuenv_core::Error::configuration(format!("Failed to get current directory: {e}"))
129        })?;
130
131        // Collect project info before any async operations
132        let project_paths: Vec<(std::path::PathBuf, String)> = {
133            let module = executor.get_module(&cwd)?;
134            let mut paths = Vec::new();
135            for instance in module.projects() {
136                if let Ok(manifest) = instance.deserialize::<Project>()
137                    && manifest.codegen.is_some()
138                {
139                    paths.push((
140                        module.root.join(&instance.path),
141                        instance.path.display().to_string(),
142                    ));
143                }
144            }
145            paths
146        };
147
148        let mut outputs = Vec::new();
149        let mut had_error = false;
150
151        for (full_path, display_path) in project_paths {
152            let Some(path_str) = full_path.to_str() else {
153                outputs.push(format!(
154                    "{}: Error: Path contains invalid UTF-8",
155                    full_path.display()
156                ));
157                had_error = true;
158                continue;
159            };
160
161            let codegen_options = functions::CodegenSyncOptions {
162                dry_run: dry_run.into(),
163                check,
164                diff: options.show_diff,
165            };
166            let request = functions::CodegenSyncRequest {
167                path: path_str,
168                package,
169                options: codegen_options,
170            };
171            let result = functions::execute_sync_codegen(request, executor).await;
172
173            match result {
174                Ok(output) if !output.is_empty() => {
175                    let display = if display_path.is_empty() {
176                        "[root]".to_string()
177                    } else {
178                        display_path
179                    };
180                    outputs.push(format!("{display}:\n{output}"));
181                }
182                Ok(_) => {}
183                Err(e) => {
184                    outputs.push(format!("{display_path}: Error: {e}"));
185                    had_error = true;
186                }
187            }
188        }
189
190        if outputs.is_empty() {
191            Ok(SyncResult::success("No codegen configurations found."))
192        } else {
193            Ok(SyncResult {
194                output: outputs.join("\n\n"),
195                had_error,
196            })
197        }
198    }
199
200    fn has_config(&self, _manifest: &Base) -> bool {
201        // Codegen configs are only in Projects, not Base
202        false
203    }
204}
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209
210    #[test]
211    fn test_codegen_provider_name() {
212        let provider = CodegenProvider::new();
213        assert_eq!(provider.name(), "codegen");
214    }
215
216    #[test]
217    fn test_codegen_provider_description() {
218        let provider = CodegenProvider::new();
219        assert!(!provider.description().is_empty());
220        assert!(provider.description().contains("codegen"));
221    }
222
223    #[test]
224    fn test_codegen_provider_as_any() {
225        let provider = CodegenProvider::new();
226        let any = provider.as_any();
227        assert!(any.is::<CodegenProvider>());
228    }
229
230    #[test]
231    fn test_codegen_provider_as_any_mut() {
232        let mut provider = CodegenProvider::new();
233        let any = provider.as_any_mut();
234        assert!(any.is::<CodegenProvider>());
235    }
236
237    #[test]
238    fn test_codegen_provider_command() {
239        let provider = CodegenProvider::new();
240        let cmd = provider.build_sync_command();
241        assert_eq!(cmd.get_name(), "codegen");
242    }
243
244    #[test]
245    fn test_codegen_provider_command_has_args() {
246        let provider = CodegenProvider::new();
247        let cmd = provider.build_sync_command();
248
249        let args: Vec<_> = cmd.get_arguments().map(|a| a.get_id().as_str()).collect();
250        assert!(args.contains(&"path"));
251        assert!(args.contains(&"package"));
252        assert!(args.contains(&"dry-run"));
253        assert!(args.contains(&"check"));
254        assert!(args.contains(&"all"));
255        assert!(args.contains(&"diff"));
256    }
257
258    #[test]
259    fn test_codegen_provider_default() {
260        let provider = CodegenProvider;
261        assert_eq!(provider.name(), "codegen");
262    }
263
264    #[test]
265    fn test_codegen_provider_has_config() {
266        let provider = CodegenProvider::new();
267        let base = Base::default();
268        // Codegen configs are only in Projects, not Base
269        assert!(!provider.has_config(&base));
270    }
271}