Skip to main content

cuenv_core/
sync.rs

1//! Sync provider abstraction for file synchronization operations.
2//!
3//! This module defines the [`SyncProvider`] trait that allows different types of
4//! file synchronization (ignore files, codeowners, cubes, CI workflows) to be
5//! implemented in their respective crates and used uniformly by the CLI.
6//!
7//! # Architecture
8//!
9//! Each sync provider crate (cuenv-ignore, cuenv-codeowners, etc.) implements
10//! the [`SyncProvider`] trait. The CLI loads the CUE module once and passes it
11//! to providers, avoiding redundant evaluation.
12//!
13//! # Example
14//!
15//! ```rust,ignore
16//! use cuenv_core::sync::{SyncProvider, SyncOptions, SyncContext};
17//!
18//! // CLI loads module once
19//! let module = load_module(&cwd, package)?;
20//!
21//! // Pass to each provider
22//! for provider in providers {
23//!     let result = provider.sync(&SyncContext {
24//!         module: &module,
25//!         options: &options,
26//!     }).await?;
27//! }
28//! ```
29
30use crate::Result;
31use crate::manifest::Base;
32use crate::module::ModuleEvaluation;
33use async_trait::async_trait;
34use std::path::Path;
35
36/// Mode of operation for sync commands.
37#[derive(Debug, Clone, Default, PartialEq, Eq)]
38pub enum SyncMode {
39    /// Actually write files to disk.
40    #[default]
41    Write,
42    /// Preview what would change without writing files.
43    DryRun,
44    /// Check if files are in sync (error if not).
45    Check,
46}
47
48/// Options passed to sync operations.
49///
50/// These options control how synchronization behaves across all providers.
51#[derive(Debug, Clone, Default)]
52pub struct SyncOptions {
53    /// The sync operation mode.
54    pub mode: SyncMode,
55    /// Show diff for files that would change.
56    pub show_diff: bool,
57    /// Overwrite existing files without prompting (only applies in Write mode).
58    pub force: bool,
59}
60
61/// Result of a sync operation.
62#[derive(Debug, Clone)]
63pub struct SyncResult {
64    /// Output message describing what was synced.
65    pub output: String,
66    /// Whether any errors occurred during sync.
67    pub had_error: bool,
68}
69
70impl SyncResult {
71    /// Create a successful sync result.
72    #[must_use]
73    pub fn success(output: impl Into<String>) -> Self {
74        Self {
75            output: output.into(),
76            had_error: false,
77        }
78    }
79
80    /// Create an error sync result.
81    #[must_use]
82    pub fn error(output: impl Into<String>) -> Self {
83        Self {
84            output: output.into(),
85            had_error: true,
86        }
87    }
88
89    /// Create an empty result (no output, no error).
90    #[must_use]
91    pub fn empty() -> Self {
92        Self {
93            output: String::new(),
94            had_error: false,
95        }
96    }
97}
98
99/// Context for sync operations.
100///
101/// Provides access to the evaluated CUE module and sync options.
102pub struct SyncContext<'a> {
103    /// The evaluated CUE module containing all instances.
104    pub module: &'a ModuleEvaluation,
105    /// Options controlling sync behavior.
106    pub options: &'a SyncOptions,
107    /// Package name being synced.
108    pub package: &'a str,
109}
110
111/// Trait for sync providers.
112///
113/// Each provider (ignore, cubes, codeowners, ci) implements this trait
114/// to handle synchronization of its specific file type.
115///
116/// # Implementors
117///
118/// - `cuenv-ignore`: Generates .gitignore, .dockerignore, etc.
119/// - `cuenv-codeowners`: Generates CODEOWNERS files
120/// - `cuenv-codegen`: Generates files from CUE codegen templates
121/// - `cuenv-ci`: Generates CI workflow files
122#[async_trait]
123pub trait SyncProvider: Send + Sync {
124    /// Name of the sync provider (e.g., "ignore", "codegen").
125    ///
126    /// Used as the CLI subcommand name.
127    fn name(&self) -> &'static str;
128
129    /// Description for CLI help.
130    fn description(&self) -> &'static str;
131
132    /// Check if this provider has configuration for the given manifest.
133    ///
134    /// Used to determine which providers to run when syncing all.
135    fn has_config(&self, manifest: &Base) -> bool;
136
137    /// Sync files for a single path within the module.
138    ///
139    /// The path should be relative to the module root.
140    async fn sync_path(&self, path: &Path, ctx: &SyncContext<'_>) -> Result<SyncResult>;
141
142    /// Sync all applicable paths in the module.
143    ///
144    /// Iterates through all instances in the module that have configuration
145    /// for this provider and syncs each one.
146    async fn sync_all(&self, ctx: &SyncContext<'_>) -> Result<SyncResult>;
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152
153    #[test]
154    fn test_sync_result_success() {
155        let result = SyncResult::success("Created .gitignore");
156        assert_eq!(result.output, "Created .gitignore");
157        assert!(!result.had_error);
158    }
159
160    #[test]
161    fn test_sync_result_error() {
162        let result = SyncResult::error("Failed to write file");
163        assert_eq!(result.output, "Failed to write file");
164        assert!(result.had_error);
165    }
166
167    #[test]
168    fn test_sync_result_empty() {
169        let result = SyncResult::empty();
170        assert!(result.output.is_empty());
171        assert!(!result.had_error);
172    }
173
174    #[test]
175    fn test_sync_options_default() {
176        let options = SyncOptions::default();
177        assert_eq!(options.mode, SyncMode::Write);
178        assert!(!options.show_diff);
179        assert!(!options.force);
180    }
181
182    #[test]
183    fn test_sync_mode_default() {
184        assert_eq!(SyncMode::default(), SyncMode::Write);
185    }
186}