1use std::path::{Path, PathBuf};
32
33#[cfg(feature = "tracing")]
34use tracing::{debug, info, trace};
35
36use crate::parse::{EnvEntry, EnvFile, ParseError};
37
38const DEFAULT_LOCAL_FILENAME: &str = ".env";
39
40pub struct EnvSync;
42
43impl EnvSync {
44 pub fn sync_with_options(options: EnvSyncOptions) -> Result<(), EnvSyncError> {
48 #[cfg(feature = "tracing")]
49 info!("Starting env sync");
50
51 let EnvSyncOptions {
52 local_file,
53 template_file,
54 } = options;
55
56 let local_path = local_file.unwrap_or_else(|| {
57 std::env::current_dir()
58 .unwrap_or_else(|_| PathBuf::from("."))
59 .join(DEFAULT_LOCAL_FILENAME)
60 });
61
62 #[cfg(feature = "tracing")]
63 debug!(?local_path, ?template_file, "Resolved file paths");
64
65 if !template_file.exists() {
66 return Err(EnvSyncError::TemplateNotFound(template_file));
67 }
68
69 if !local_path.exists() {
70 #[cfg(feature = "tracing")]
71 debug!("Creating local file: {:?}", local_path);
72 std::fs::write(&local_path, "").map_err(EnvSyncError::CreateLocal)?;
73 }
74
75 let local_str = std::fs::read_to_string(&local_path).map_err(EnvSyncError::LocalIo)?;
76 let template_str = std::fs::read_to_string(&template_file).map_err(EnvSyncError::TemplateIo)?;
77
78 let local_content = local_str
79 .as_str()
80 .try_into()
81 .map_err(EnvSyncError::LocalParse)?;
82
83 let template_content = template_str
84 .as_str()
85 .try_into()
86 .map_err(EnvSyncError::TemplateParse)?;
87
88 let synced = Self::sync(local_content, template_content)?;
89
90 Self::update_local(synced, local_path)
91 }
92
93 fn sync<'a>(local: EnvFile<'a>, mut template: EnvFile<'a>) -> Result<EnvFile<'a>, EnvSyncError> {
97 #[cfg(feature = "tracing")]
98 debug!(
99 "Starting sync of {} template entries",
100 template.entries.len()
101 );
102
103 for entry in &mut template.entries {
104 if let EnvEntry::Variable(template_var) = entry
105 && let Some(local_var) = local.get(&template_var.key)
106 {
107 #[cfg(feature = "tracing")]
108 trace!("Processing variable: {}", template_var.key);
109
110 if template_var.value.is_empty() && !local_var.value.is_empty() {
112 #[cfg(feature = "tracing")]
113 trace!(
114 "Copying local value for {}: {}",
115 template_var.key, local_var.value
116 );
117 template_var.value = local_var.value.clone();
118 }
119
120 if template_var.inline_comment.is_none() && local_var.inline_comment.is_some() {
122 #[cfg(feature = "tracing")]
123 trace!("Copying inline comment for {}", template_var.key);
124 template_var.inline_comment = local_var.inline_comment.clone();
125 }
126
127 if template_var.preceding_comments.is_empty() && !local_var.preceding_comments.is_empty() {
129 #[cfg(feature = "tracing")]
130 trace!(
131 "Copying {} preceding comments for {}",
132 local_var.preceding_comments.len(),
133 template_var.key
134 );
135 template_var.preceding_comments = local_var.preceding_comments.clone();
136 }
137 }
138 }
139
140 Ok(template)
141 }
142
143 fn update_local<P: AsRef<Path>>(local: EnvFile, local_path: P) -> Result<(), EnvSyncError> {
145 #[cfg(feature = "tracing")]
146 debug!("Writing synced content to {:?}", local_path.as_ref());
147
148 let content = local.to_string();
149 std::fs::write(local_path, content).map_err(EnvSyncError::Write)?;
150
151 #[cfg(feature = "tracing")]
152 info!("Sync completed successfully");
153
154 Ok(())
155 }
156}
157
158#[derive(Debug, thiserror::Error)]
160pub enum EnvSyncError {
161 #[error("Local file IO error: {0}")]
163 LocalIo(std::io::Error),
164 #[error("Local file parse error: {0}")]
166 LocalParse(ParseError),
167 #[error("Template file IO error: {0}")]
169 TemplateIo(std::io::Error),
170 #[error("Template file parse error: {0}")]
172 TemplateParse(ParseError),
173 #[error("Write error: {0}")]
175 Write(std::io::Error),
176 #[error("Failed to create local file: {0}")]
178 CreateLocal(std::io::Error),
179 #[error("Template file not found: {0}")]
181 TemplateNotFound(PathBuf),
182}
183
184pub struct EnvSyncOptions {
186 pub local_file: Option<PathBuf>,
188 pub template_file: PathBuf,
190}
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195
196 #[test]
197 fn test_sync() {
198 let local_content = "# Comment for KEY1\nKEY1=value1\nKEY2=value2 # inline comment\nKEY3=";
199 let template_content = "KEY1=\nKEY2=template_value\nKEY3=template_value3\nKEY4=new_key";
200
201 let local: EnvFile = local_content.try_into().unwrap();
202 let template: EnvFile = template_content.try_into().unwrap();
203
204 let synced = EnvSync::sync(local, template).unwrap();
205
206 let key1 = synced.get("KEY1").unwrap();
207 assert_eq!(key1.value, "value1");
208 assert_eq!(key1.preceding_comments.len(), 1);
209
210 let key2 = synced.get("KEY2").unwrap();
211 assert_eq!(key2.value, "template_value");
212 assert_eq!(
213 key2.inline_comment.as_ref().unwrap().to_string(),
214 "# inline comment"
215 );
216
217 assert_eq!(synced.get("KEY3").unwrap().value, "template_value3");
218 assert_eq!(synced.get("KEY4").unwrap().value, "new_key");
219 }
220
221 #[test]
222 fn test_template_not_found() {
223 use std::path::PathBuf;
224
225 let options = EnvSyncOptions {
226 local_file: None,
227 template_file: PathBuf::from("nonexistent.env.template"),
228 };
229
230 let result = EnvSync::sync_with_options(options);
231 assert!(result.is_err());
232
233 match result.unwrap_err() {
234 EnvSyncError::TemplateNotFound(path) => {
235 assert_eq!(path, PathBuf::from("nonexistent.env.template"));
236 }
237 _ => panic!("Expected TemplateNotFound error"),
238 }
239 }
240}