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}