1use super::*;
2
3pub(crate) fn set_item_string_preserving_decor(slot: &mut Item, new_string: &str) {
12 let mut replacement: Item = value(new_string);
13 if let Some(old_value) = slot.as_value()
14 && let Some(new_value) = replacement.as_value_mut()
15 {
16 *new_value.decor_mut() = old_value.decor().clone();
17 }
18 *slot = replacement;
19}
20
21fn read_root_version(doc: &DocumentMut) -> Option<String> {
34 let workspace_version: Option<&str> = doc
35 .get(TOML_WORKSPACE)
36 .and_then(|workspace: &Item| workspace.get(TOML_PACKAGE))
37 .and_then(|package: &Item| package.get(TOML_VERSION))
38 .and_then(|version_item: &Item| version_item.as_str());
39 let package_version: Option<&str> = doc
40 .get(TOML_PACKAGE)
41 .and_then(|package: &Item| package.get(TOML_VERSION))
42 .and_then(|version_item: &Item| version_item.as_str());
43 workspace_version
44 .or(package_version)
45 .map(|version: &str| version.to_string())
46}
47
48fn read_member_version(
60 member_doc: &DocumentMut,
61 workspace_version: Option<&str>,
62) -> Result<String, SyncError> {
63 let version_item: &Item = member_doc
64 .get(TOML_PACKAGE)
65 .and_then(|package: &Item| package.get(TOML_VERSION))
66 .ok_or_else(|| SyncError::MemberNameMissing(CARGO_TOML.to_string()))?;
67 if let Some(version) = version_item.as_str() {
68 return Ok(version.to_string());
69 }
70 let inherits: bool = version_item
71 .get(TOML_WORKSPACE)
72 .and_then(|workspace_item: &Item| workspace_item.as_bool())
73 .unwrap_or(false);
74 if inherits && let Some(version) = workspace_version {
75 return Ok(version.to_string());
76 }
77 Err(SyncError::WorkspaceVersionMissing(CARGO_TOML.to_string()))
78}
79
80fn read_workspace_members(doc: &DocumentMut) -> Result<Vec<String>, SyncError> {
90 let members: Vec<String> = doc
91 .get(TOML_WORKSPACE)
92 .and_then(|workspace: &Item| workspace.get(TOML_MEMBERS))
93 .and_then(|members_item: &Item| members_item.as_array())
94 .ok_or_else(|| SyncError::WorkspaceMembersMissing(CARGO_TOML.to_string()))?
95 .iter()
96 .filter_map(|member: &toml_edit::Value| member.as_str().map(|s: &str| s.to_string()))
97 .collect();
98 Ok(members)
99}
100
101fn read_member_crate_name(doc: &DocumentMut) -> Result<String, SyncError> {
111 let name: String = doc
112 .get(TOML_PACKAGE)
113 .and_then(|package: &Item| package.get(TOML_NAME))
114 .and_then(|name_item: &Item| name_item.as_str())
115 .ok_or_else(|| SyncError::MemberNameMissing(CARGO_TOML.to_string()))?
116 .to_string();
117 Ok(name)
118}
119
120fn find_dep_alias_for_member_path(deps: &dyn TableLike, member_path: &str) -> Option<String> {
132 for (alias, entry) in deps.iter() {
133 if let Some(path) = entry
134 .get(TOML_PATH)
135 .and_then(|path_item: &Item| path_item.as_str())
136 && path == member_path
137 {
138 return Some(alias.to_string());
139 }
140 }
141 None
142}
143
144fn scan_dep_entry(doc: &DocumentMut, member_path: &str) -> Option<(String, Option<String>)> {
157 let deps: &dyn TableLike = doc
158 .get(TOML_WORKSPACE)
159 .and_then(|workspace: &Item| workspace.get(TOML_DEPENDENCIES))
160 .and_then(|deps_item: &Item| deps_item.as_table_like())?;
161 let current_alias: String = find_dep_alias_for_member_path(deps, member_path)?;
162 let existing_version: Option<String> = deps
163 .get(¤t_alias)
164 .and_then(|entry: &Item| entry.get(TOML_VERSION))
165 .and_then(|version_item: &Item| version_item.as_str())
166 .map(|version: &str| version.to_string());
167 Some((current_alias, existing_version))
168}
169
170fn rewrite_entry_version(deps: &mut dyn TableLike, current_alias: &str, workspace_version: &str) {
179 let Some(entry) = deps.get_mut(current_alias) else {
180 return;
181 };
182 match entry.get_mut(TOML_VERSION) {
183 Some(version_slot) => set_item_string_preserving_decor(version_slot, workspace_version),
184 None => {
185 if let Some(entry_table) = entry.as_table_like_mut() {
186 entry_table.insert(TOML_VERSION, value(workspace_version));
187 }
188 }
189 }
190}
191
192pub async fn execute_sync(manifest_path: &str) -> Result<SyncReport, SyncError> {
230 let path: &Path = Path::new(manifest_path);
231 let content: String = read_to_string(path).await?;
232 let mut doc: DocumentMut = content
233 .parse()
234 .map_err(|_error: TomlError| SyncError::ManifestParseError)?;
235 let workspace_version: Option<String> = read_root_version(&doc);
236 let members: Vec<String> = read_workspace_members(&doc)?;
237 if members.is_empty() {
238 log::info!("sync: no workspace members, nothing to do");
239 return Ok(SyncReport {
240 workspace_version: workspace_version.unwrap_or_default(),
241 renamed_entries: Vec::new(),
242 versioned_entries: Vec::new(),
243 file_changed: false,
244 });
245 }
246 let mut renamed_entries: Vec<(String, String)> = Vec::new();
247 let mut versioned_entries: Vec<(String, String)> = Vec::new();
248 let mut needs_rewrite: bool = false;
249 let mut member_paths: Vec<String> = Vec::new();
253 if doc.get(TOML_PACKAGE).is_some() {
254 member_paths.push(".".to_string());
255 }
256 member_paths.extend(members.iter().cloned());
257 for member_path in &member_paths {
258 let member_manifest_path: PathBuf = path
259 .parent()
260 .unwrap_or_else(|| Path::new("."))
261 .join(member_path)
262 .join(CARGO_TOML);
263 if !member_manifest_path.exists() {
264 return Err(SyncError::MemberManifestMissing(
265 member_manifest_path.display().to_string(),
266 ));
267 }
268 let member_content: String = read_to_string(&member_manifest_path).await?;
269 let member_doc: DocumentMut = member_content
270 .parse()
271 .map_err(|_error: TomlError| SyncError::ManifestParseError)?;
272 let canonical_alias: String = read_member_crate_name(&member_doc)?;
273 let target_version: String = match &workspace_version {
277 Some(version) => version.clone(),
278 None => read_member_version(&member_doc, None)?,
279 };
280 let (current_alias, existing_version): (String, Option<String>) =
281 match scan_dep_entry(&doc, member_path) {
282 Some(scanned) => scanned,
283 None => {
284 log::info!(
285 "sync: {} -> no [workspace.dependencies] entry, skipping",
286 member_path
287 );
288 continue;
289 }
290 };
291 let alias_needs_rename: bool = current_alias != canonical_alias;
292 let version_needs_rewrite: bool =
293 existing_version.as_deref() != Some(target_version.as_str());
294 if !alias_needs_rename && !version_needs_rewrite {
295 continue;
296 }
297 needs_rewrite = true;
298 let deps: &mut dyn TableLike = doc
299 .get_mut(TOML_WORKSPACE)
300 .and_then(|workspace: &mut Item| workspace.get_mut(TOML_DEPENDENCIES))
301 .and_then(|deps_item: &mut Item| deps_item.as_table_like_mut())
302 .ok_or(SyncError::ManifestParseError)?;
303 rewrite_entry_version(deps, ¤t_alias, &target_version);
304 if alias_needs_rename {
305 if let Some(entry) = deps.remove(¤t_alias) {
306 deps.insert(&canonical_alias, entry);
307 }
308 log::info!(
309 "sync: {} renamed {} -> {}",
310 member_path,
311 current_alias,
312 canonical_alias
313 );
314 renamed_entries.push((current_alias, canonical_alias.clone()));
315 } else {
316 log::info!(
317 "sync: {} -> {} v{}",
318 member_path,
319 canonical_alias,
320 target_version
321 );
322 }
323 versioned_entries.push((member_path.clone(), canonical_alias));
324 }
325 let file_changed: bool = needs_rewrite;
326 let report_version: String =
327 workspace_version.unwrap_or_else(|| REPORT_PER_MEMBER_VERSION.to_string());
328 if file_changed {
329 write(path, doc.to_string()).await?;
330 log::info!(
331 "sync: wrote {} entries to v{}",
332 versioned_entries.len(),
333 report_version
334 );
335 } else {
336 log::info!("sync: already in sync: v{}", report_version);
337 }
338 Ok(SyncReport {
339 workspace_version: report_version,
340 renamed_entries,
341 versioned_entries,
342 file_changed,
343 })
344}