1use crate::{
5 hakari::{DepFormatVersion, WorkspaceHackLineStyle},
6 helpers::VersionDisplay,
7};
8use atomicwrites::{AtomicFile, OverwriteBehavior};
9use camino::{Utf8Path, Utf8PathBuf};
10use guppy::{
11 Version,
12 graph::{DependencyDirection, PackageGraph, PackageMetadata, PackageSet},
13};
14use owo_colors::{OwoColorize, Style};
15use std::{borrow::Cow, cmp::Ordering, collections::BTreeMap, error, fmt, fs, io, io::Write};
16use toml_edit::{
17 Array, DocumentMut, Formatted, InlineTable, Item, Table, TableLike, TomlError, Value,
18};
19
20#[derive(Clone, Debug)]
22pub struct WorkspaceOps<'g, 'a> {
23 graph: &'g PackageGraph,
24 ops: Vec<WorkspaceOp<'g, 'a>>,
25}
26
27impl<'g, 'a> WorkspaceOps<'g, 'a> {
28 pub(crate) fn new(
29 graph: &'g PackageGraph,
30 ops: impl IntoIterator<Item = WorkspaceOp<'g, 'a>>,
31 ) -> Self {
32 Self {
33 graph,
34 ops: ops.into_iter().collect(),
35 }
36 }
37
38 #[cfg(test)]
39 pub(crate) fn ops(&self) -> &[WorkspaceOp<'g, 'a>] {
40 &self.ops
41 }
42
43 #[inline]
45 pub fn display<'ops>(&'ops self) -> WorkspaceOpsDisplay<'g, 'a, 'ops> {
46 WorkspaceOpsDisplay::new(self)
47 }
48
49 #[inline]
51 pub fn is_empty(&self) -> bool {
52 self.ops.is_empty()
53 }
54
55 pub fn apply(&self) -> Result<(), ApplyError> {
59 let workspace_root = self.graph.workspace().root();
60 let canonical_workspace_root = workspace_root.canonicalize_utf8().map_err(|error| {
61 ApplyError::io(
62 "unable to canonicalize workspace root",
63 workspace_root.to_owned(),
64 error,
65 )
66 })?;
67 for op in &self.ops {
68 op.apply(&canonical_workspace_root)?;
69 }
70 Ok(())
71 }
72}
73
74#[derive(Clone, Debug)]
75pub(crate) enum WorkspaceOp<'g, 'a> {
76 NewCrate {
77 crate_path: &'a Utf8Path,
78 files: BTreeMap<Cow<'a, Utf8Path>, Cow<'a, [u8]>>,
79 root_files: BTreeMap<Cow<'a, Utf8Path>, Cow<'a, [u8]>>,
80 },
81 AddDependency {
82 name: &'a str,
83 crate_path: &'a Utf8Path,
84 version: &'a Version,
85 dep_format: DepFormatVersion,
86 line_style: WorkspaceHackLineStyle,
87 add_to: PackageSet<'g>,
88 },
89 RemoveDependency {
90 name: &'a str,
91 remove_from: PackageSet<'g>,
92 },
93}
94
95impl<'g> WorkspaceOp<'g, '_> {
96 fn apply(&self, canonical_workspace_root: &Utf8Path) -> Result<(), ApplyError> {
97 match self {
98 WorkspaceOp::NewCrate {
99 crate_path,
100 files,
101 root_files,
102 } => {
103 Self::create_new_crate(canonical_workspace_root, crate_path, files)?;
104 let crate_path = canonical_rel_path(crate_path, canonical_workspace_root)?;
106
107 for (rel_path, contents) in root_files {
108 let abs_path = canonical_workspace_root.join(rel_path.as_ref());
109 let parent = abs_path.parent().expect("abs path should have a parent");
110 std::fs::create_dir_all(parent)
111 .map_err(|err| ApplyError::io("error creating directories", parent, err))?;
112 write_contents(contents, &abs_path)?;
113 }
114
115 Self::add_to_root_toml(canonical_workspace_root, &crate_path)
116 }
117 WorkspaceOp::AddDependency {
118 name,
119 crate_path,
120 version,
121 dep_format,
122 line_style,
123 add_to,
124 } => {
125 let crate_path = canonical_rel_path(crate_path, canonical_workspace_root)?;
126 for package in add_to.packages(DependencyDirection::Reverse) {
127 Self::add_to_cargo_toml(
128 name,
129 version,
130 &crate_path,
131 *dep_format,
132 *line_style,
133 package,
134 )?;
135 }
136 Ok(())
137 }
138 WorkspaceOp::RemoveDependency { name, remove_from } => {
139 for package in remove_from.packages(DependencyDirection::Reverse) {
140 Self::remove_from_cargo_toml(name, package)?;
141 }
142 Ok(())
143 }
144 }
145 }
146
147 fn create_new_crate(
152 workspace_root: &Utf8Path,
153 crate_path: &Utf8Path,
154 files: &BTreeMap<Cow<'_, Utf8Path>, Cow<'_, [u8]>>,
155 ) -> Result<(), ApplyError> {
156 let abs_path = workspace_root.join(crate_path);
157 for (path, contents) in files {
158 let mut dir_path = match path.parent() {
160 Some(parent) => abs_path.join(parent),
161 None => abs_path.clone(),
162 };
163 std::fs::create_dir_all(&dir_path)
164 .map_err(|err| ApplyError::io("error creating directories", &dir_path, err))?;
165
166 dir_path.push(
168 path.file_name().ok_or_else(|| {
169 ApplyError::misc("does not contain a file name", path.as_ref())
170 })?,
171 );
172 write_contents(contents, &dir_path)?;
173 }
174 Ok(())
175 }
176
177 fn add_to_root_toml(
178 workspace_root: &Utf8Path,
179 crate_path: &Utf8Path,
180 ) -> Result<(), ApplyError> {
181 let root_toml_path = workspace_root.join("Cargo.toml");
182
183 let mut doc = read_toml(&root_toml_path)?;
184 let members = Self::get_workspace_members_array(&root_toml_path, &mut doc)?;
185
186 let add = |members: &mut Array, idx: usize| {
187 let existing = if idx < members.len() {
189 members.get(idx).expect("valid idx")
190 } else {
191 members.get(members.len() - 1).expect("valid idx")
192 };
193
194 let write_path = with_forward_slashes(crate_path).into_string();
195 let write_path = decorate(existing, write_path);
196 members.insert_formatted(idx, write_path);
197 };
198
199 let mut written = false;
200 for idx in 0..members.len() {
201 let member = members.get(idx).expect("valid idx");
202 match member.as_str() {
203 Some(path) => {
204 let path = Utf8Path::new(path);
205 match path.cmp(crate_path) {
208 Ordering::Greater => {
209 add(members, idx);
210 written = true;
211 break;
212 }
213 Ordering::Equal => {
214 written = true;
216 break;
217 }
218 Ordering::Less => {}
219 }
220 }
221 None => {
222 return Err(ApplyError::misc(
223 "workspace.members contains non-strings",
224 root_toml_path,
225 ));
226 }
227 }
228 }
229
230 if !written {
231 add(members, members.len());
232 }
233
234 write_document(&doc, &root_toml_path)
235 }
236
237 fn get_workspace_members_array<'doc>(
238 root_toml_path: &Utf8Path,
239 doc: &'doc mut DocumentMut,
240 ) -> Result<&'doc mut Array, ApplyError> {
241 let doc_table = doc.as_table_mut();
242 let workspace_table = match doc_table.get_mut("workspace") {
243 Some(Item::Table(workspace_table)) => workspace_table,
244 Some(other) => {
245 return Err(ApplyError::misc(
246 format!(
247 "expected [workspace] to be a table, found {}",
248 other.type_name()
249 ),
250 root_toml_path,
251 ));
252 }
253 None => {
254 return Err(ApplyError::misc(
255 "[workspace] section not found",
256 root_toml_path,
257 ));
258 }
259 };
260
261 let members = match workspace_table.get_mut("members") {
262 Some(Item::Value(members)) => match members.as_array_mut() {
263 Some(members) => members,
264 None => {
265 return Err(ApplyError::misc(
266 "workspace.members is not an array",
267 root_toml_path,
268 ));
269 }
270 },
271 Some(other) => {
272 return Err(ApplyError::misc(
273 format!(
274 "expected workspace.members to be an array, found {}",
275 other.type_name()
276 ),
277 root_toml_path,
278 ));
279 }
280 None => {
281 return Err(ApplyError::misc(
282 "workspace.members not found",
283 root_toml_path,
284 ));
285 }
286 };
287 Ok(members)
288 }
289
290 fn add_to_cargo_toml(
291 name: &str,
292 version: &Version,
293 crate_path: &Utf8Path,
294 dep_format: DepFormatVersion,
295 line_style: WorkspaceHackLineStyle,
296 package: PackageMetadata<'g>,
297 ) -> Result<(), ApplyError> {
298 let manifest_path = package.manifest_path();
299 let mut doc = read_toml(manifest_path)?;
300
301 let package_path = package
302 .source()
303 .workspace_path()
304 .expect("package should be in workspace");
305 let path = pathdiff::diff_utf8_paths(crate_path, package_path)
307 .expect("both new_path and package_path are relative");
308
309 let path_table = Self::inline_table_for_add(version, dep_format, line_style, &path);
310
311 add_dependency_to_document(&mut doc, name, path_table)
312 .map_err(|error| ApplyError::misc(error.message(), manifest_path))?;
313
314 write_document(&doc, manifest_path)
315 }
316
317 fn inline_table_for_add(
318 version: &Version,
319 dep_format: DepFormatVersion,
320 line_style: WorkspaceHackLineStyle,
321 path: &Utf8Path,
322 ) -> InlineTable {
323 let mut itable = InlineTable::new();
324
325 match line_style {
326 WorkspaceHackLineStyle::Full => {
327 let version_str = format!(
330 "{}",
331 VersionDisplay::new(version, false, dep_format < DepFormatVersion::V3)
332 );
333 if dep_format >= DepFormatVersion::V2 {
334 itable.insert("version", version_str.into());
335 }
336
337 let mut path = Formatted::new(with_forward_slashes(path).into_string());
338 if dep_format == DepFormatVersion::V1 {
339 path.decor_mut().set_suffix("");
343 }
344 itable.insert("path", Value::String(path));
345
346 if dep_format == DepFormatVersion::V2 {
347 itable.fmt();
348 }
349 itable
350 }
351 WorkspaceHackLineStyle::VersionOnly => {
352 let version_str = format!("{}", VersionDisplay::new(version, false, false));
355 itable.insert("version", version_str.into());
356 itable
357 }
358 WorkspaceHackLineStyle::WorkspaceDotted => {
359 itable.insert("workspace", true.into());
362 itable.set_dotted(true);
363 itable
364 }
365 }
366 }
367
368 fn remove_from_cargo_toml(name: &str, package: PackageMetadata<'g>) -> Result<(), ApplyError> {
369 let manifest_path = package.manifest_path();
370 let mut doc = read_toml(manifest_path)?;
371 remove_dependency_from_document(&mut doc, name)
372 .map_err(|error| ApplyError::misc(error.message(), manifest_path))?;
373
374 write_document(&doc, manifest_path)
375 }
376}
377
378#[derive(Clone, Copy, Debug, Eq, PartialEq)]
384enum DependencySection {
385 Normal,
386 Dev,
387 Build,
388}
389
390impl DependencySection {
391 const ALL: [Self; 3] = [Self::Normal, Self::Dev, Self::Build];
392
393 fn key(self) -> &'static str {
394 match self {
395 DependencySection::Normal => "dependencies",
396 DependencySection::Dev => "dev-dependencies",
397 DependencySection::Build => "build-dependencies",
398 }
399 }
400}
401
402#[derive(Clone, Debug, Eq, PartialEq)]
405enum NotATableError {
406 Section { section: DependencySection },
408 Target,
410 Platform { platform: String },
412 PlatformSection {
414 platform: String,
415 section: DependencySection,
416 },
417}
418
419impl NotATableError {
420 fn message(&self) -> String {
421 match self {
422 NotATableError::Section { section } => {
423 format!("[{}] is not a table", section.key())
424 }
425 NotATableError::Target => "[target] is not a table".to_owned(),
426 NotATableError::Platform { platform } => {
427 format!("[target.'{platform}'] is not a table")
428 }
429 NotATableError::PlatformSection { platform, section } => {
430 format!("[target.'{platform}'.{}] is not a table", section.key())
431 }
432 }
433 }
434}
435
436fn add_dependency_to_document(
445 doc: &mut DocumentMut,
446 name: &str,
447 dep: InlineTable,
448) -> Result<(), NotATableError> {
449 let dep_table = get_or_insert_dependency_section(doc, DependencySection::Normal)?;
450 dep_table.insert(name, Item::Value(Value::InlineTable(dep)));
451 if let Some(dev_table) = get_dependency_section(doc, DependencySection::Dev)? {
452 dev_table.remove(name);
453 }
454 Ok(())
455}
456
457fn remove_dependency_from_document(
460 doc: &mut DocumentMut,
461 name: &str,
462) -> Result<(), NotATableError> {
463 for section in DependencySection::ALL {
466 if let Some(dep_table) = get_dependency_section(doc, section)? {
467 dep_table.remove(name);
468 }
469 }
470
471 let Some(target_item) = doc.as_table_mut().get_mut("target") else {
472 return Ok(());
473 };
474 let Some(target_table) = target_item.as_table_like_mut() else {
475 return Err(NotATableError::Target);
476 };
477 for (platform, platform_item) in target_table.iter_mut() {
478 let platform = platform.get();
479 let Some(platform_table) = platform_item.as_table_like_mut() else {
480 return Err(NotATableError::Platform {
481 platform: platform.to_owned(),
482 });
483 };
484 for section in DependencySection::ALL {
485 let Some(section_item) = platform_table.get_mut(section.key()) else {
486 continue;
487 };
488 let Some(dep_table) = section_item.as_table_like_mut() else {
489 return Err(NotATableError::PlatformSection {
490 platform: platform.to_owned(),
491 section,
492 });
493 };
494 dep_table.remove(name);
495 }
496 }
497 Ok(())
498}
499
500fn get_dependency_section(
502 doc: &mut DocumentMut,
503 section: DependencySection,
504) -> Result<Option<&mut dyn TableLike>, NotATableError> {
505 let key = section.key();
506 match doc.as_table_mut().get_mut(key) {
507 Some(item) => match item.as_table_like_mut() {
508 Some(table) => Ok(Some(table)),
509 None => Err(NotATableError::Section { section }),
510 },
511 None => Ok(None),
512 }
513}
514
515fn get_or_insert_dependency_section(
516 doc: &mut DocumentMut,
517 section: DependencySection,
518) -> Result<&mut dyn TableLike, NotATableError> {
519 let key = section.key();
520 let doc_table = doc.as_table_mut();
521
522 if doc_table.contains_key(key) {
523 match doc_table
524 .get_mut(key)
525 .expect("just checked for presence of section")
526 .as_table_like_mut()
527 {
528 Some(table) => Ok(table),
529 None => Err(NotATableError::Section { section }),
530 }
531 } else {
532 let mut new_table = Table::new();
534 new_table.set_implicit(true);
535 doc_table.insert(key, Item::Table(new_table));
536 let table = doc_table
537 .get_mut(key)
538 .expect("was just inserted")
539 .as_table_like_mut()
540 .expect("was just inserted");
541 Ok(table)
542 }
543}
544
545fn decorate(existing: &Value, new: impl Into<Value>) -> Value {
546 let decor = existing.decor();
547 new.into().decorated(
548 decor.prefix().cloned().unwrap_or_default(),
549 decor.suffix().cloned().unwrap_or_default(),
550 )
551}
552
553fn with_forward_slashes(path: &Utf8Path) -> Utf8PathBuf {
555 let components: Vec<_> = path.iter().collect();
556 components.join("/").into()
557}
558
559fn canonical_rel_path(
564 path: &Utf8Path,
565 canonical_base: &Utf8Path,
566) -> Result<Utf8PathBuf, ApplyError> {
567 let abs_path = canonical_base.join(path);
568 let canonical_path = abs_path
570 .canonicalize_utf8()
571 .map_err(|err| ApplyError::io("error canonicalizing path", &abs_path, err))?;
572 canonical_path
573 .strip_prefix(canonical_base)
574 .map_err(|_| {
575 ApplyError::misc(
577 format!("canonical path is not within base path {canonical_base}"),
578 &abs_path,
579 )
580 })
581 .map(|p| p.to_owned())
582}
583
584fn read_toml(manifest_path: &Utf8Path) -> Result<DocumentMut, ApplyError> {
589 let toml = fs::read_to_string(manifest_path)
590 .map_err(|err| ApplyError::io("error reading TOML file", manifest_path, err))?;
591 toml.parse::<DocumentMut>()
592 .map_err(|err| ApplyError::toml("error deserializing TOML file", manifest_path, err))
593}
594
595fn write_contents(contents: &[u8], path: &Utf8Path) -> Result<(), ApplyError> {
596 write_atomic(path, |file| file.write_all(contents))
597}
598
599fn write_document(document: &DocumentMut, path: &Utf8Path) -> Result<(), ApplyError> {
600 write_atomic(path, |file| write!(file, "{document}"))
601}
602
603fn write_atomic(
604 path: &Utf8Path,
605 cb: impl FnOnce(&mut fs::File) -> Result<(), io::Error>,
606) -> Result<(), ApplyError> {
607 let atomic_file = AtomicFile::new(path, OverwriteBehavior::AllowOverwrite);
608 match atomic_file.write(cb) {
609 Ok(()) => Ok(()),
610 Err(atomicwrites::Error::Internal(err)) | Err(atomicwrites::Error::User(err)) => {
611 Err(ApplyError::io("error writing file", path, err))
612 }
613 }
614}
615
616#[derive(Debug)]
618pub struct ApplyError {
619 message: String,
620 path: Utf8PathBuf,
621 kind: Box<ApplyErrorKind>,
622}
623
624impl ApplyError {
625 #[inline]
627 pub fn message(&self) -> &str {
628 &self.message
629 }
630
631 #[inline]
633 pub fn path(&self) -> &Utf8Path {
634 &self.path
635 }
636
637 fn io(message: impl Into<String>, path: impl Into<Utf8PathBuf>, err: io::Error) -> Self {
641 Self {
642 message: message.into(),
643 path: path.into(),
644 kind: Box::new(ApplyErrorKind::Io { err }),
645 }
646 }
647
648 fn toml(
649 message: impl Into<String>,
650 path: impl Into<Utf8PathBuf>,
651 err: toml_edit::TomlError,
652 ) -> Self {
653 Self {
654 message: message.into(),
655 path: path.into(),
656 kind: Box::new(ApplyErrorKind::Toml { err }),
657 }
658 }
659
660 fn misc(message: impl Into<String>, path: impl Into<Utf8PathBuf>) -> Self {
661 Self {
662 message: message.into(),
663 path: path.into(),
664 kind: Box::new(ApplyErrorKind::Misc),
665 }
666 }
667}
668
669impl fmt::Display for ApplyError {
670 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
671 write!(f, "for path {}, {}", self.path, self.message)
672 }
673}
674
675impl error::Error for ApplyError {
676 fn source(&self) -> Option<&(dyn error::Error + 'static)> {
677 match &*self.kind {
678 ApplyErrorKind::Io { err } => Some(err),
679 ApplyErrorKind::Toml { err } => Some(err),
680 ApplyErrorKind::Misc => None,
681 }
682 }
683}
684
685#[derive(Debug)]
686enum ApplyErrorKind {
687 Io { err: io::Error },
688 Toml { err: TomlError },
689 Misc,
690}
691
692#[derive(Clone, Debug)]
694pub struct WorkspaceOpsDisplay<'g, 'a, 'ops> {
695 ops: &'ops WorkspaceOps<'g, 'a>,
696 styles: Box<Styles>,
697}
698
699impl<'g, 'a, 'ops> WorkspaceOpsDisplay<'g, 'a, 'ops> {
700 fn new(ops: &'ops WorkspaceOps<'g, 'a>) -> Self {
701 Self {
702 ops,
703 styles: Box::default(),
704 }
705 }
706
707 pub fn colorize(&mut self) -> &mut Self {
709 self.styles.colorize();
710 self
711 }
712}
713
714impl fmt::Display for WorkspaceOpsDisplay<'_, '_, '_> {
715 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
716 let workspace_root = self.ops.graph.workspace().root();
717 let workspace_root_manifest = workspace_root.join("Cargo.toml");
718 for op in &self.ops.ops {
719 match op {
720 WorkspaceOp::NewCrate {
721 crate_path,
722 files,
723 root_files,
724 } => {
725 write!(
726 f,
727 "* {} at {}",
728 "create crate".style(self.styles.create_bold_style),
729 crate_path.style(self.styles.create_bold_style),
730 )?;
731 if !files.is_empty() {
732 writeln!(f, ", with files:")?;
733 for file in files.keys() {
734 writeln!(f, " - {}", file.style(self.styles.create_style))?;
735 }
736 } else {
737 writeln!(f)?;
738 }
739 writeln!(
740 f,
741 "* {} at {} to {}",
742 "add crate".style(self.styles.add_bold_style),
743 crate_path.style(self.styles.add_style),
744 workspace_root_manifest.style(self.styles.add_to_style),
745 )?;
746 if !root_files.is_empty() {
747 writeln!(
748 f,
749 "* {} at workspace root:",
750 "create files".style(self.styles.create_bold_style)
751 )?;
752 for file in root_files.keys() {
753 writeln!(f, " - {}", file.style(self.styles.create_style))?;
754 }
755 }
756 }
757 WorkspaceOp::AddDependency {
758 name,
759 version,
760 crate_path,
761 dep_format: _,
762 line_style: _,
763 add_to,
764 } => {
765 writeln!(
766 f,
767 "* {} {} v{} (at path {}) to packages:",
768 "add or update dependency".style(self.styles.add_bold_style),
769 name.style(self.styles.add_style),
770 version.style(self.styles.add_style),
771 crate_path.style(self.styles.add_style),
772 )?;
773 for (name, path) in package_names_paths(add_to) {
774 writeln!(
775 f,
776 " - {} (at path {})",
777 name.style(self.styles.add_to_bold_style),
778 path.style(self.styles.add_to_style)
779 )?;
780 }
781 }
782 WorkspaceOp::RemoveDependency { name, remove_from } => {
783 writeln!(
784 f,
785 "* {} {} from packages:",
786 "remove dependency".style(self.styles.remove_bold_style),
787 name.style(self.styles.remove_style),
788 )?;
789 for (name, path) in package_names_paths(remove_from) {
790 writeln!(
791 f,
792 " - {} (at path {})",
793 name.style(self.styles.remove_from_bold_style),
794 path.style(self.styles.remove_from_style)
795 )?;
796 }
797 }
798 }
799 }
800 Ok(())
801 }
802}
803
804#[derive(Clone, Debug, Default)]
805struct Styles {
806 create_style: Style,
807 add_style: Style,
808 add_to_style: Style,
809 remove_style: Style,
810 remove_from_style: Style,
811 create_bold_style: Style,
812 add_bold_style: Style,
813 add_to_bold_style: Style,
814 remove_bold_style: Style,
815 remove_from_bold_style: Style,
816}
817
818impl Styles {
819 fn colorize(&mut self) {
820 self.create_style = Style::new().green();
821 self.add_style = Style::new().cyan();
822 self.add_to_style = Style::new().blue();
823 self.remove_style = Style::new().red();
824 self.remove_from_style = Style::new().purple();
825 self.create_bold_style = self.create_style.bold();
826 self.add_bold_style = self.add_style.bold();
827 self.add_to_bold_style = self.add_to_style.bold();
828 self.remove_bold_style = self.remove_style.bold();
829 self.remove_from_bold_style = self.remove_from_style.bold();
830 }
831}
832
833fn package_names_paths<'g>(package_set: &PackageSet<'g>) -> Vec<(&'g str, &'g Utf8Path)> {
834 let mut package_names_paths: Vec<_> = package_set
835 .packages(DependencyDirection::Forward)
836 .map(|package| {
837 (
838 package.name(),
839 package
840 .source()
841 .workspace_path()
842 .expect("workspace package"),
843 )
844 })
845 .collect();
846 package_names_paths.sort_unstable();
847 package_names_paths
848}
849
850#[cfg(test)]
851mod tests {
852 use super::*;
853
854 fn hack_dep() -> InlineTable {
855 WorkspaceOp::inline_table_for_add(
856 &"0.1.0".parse().expect("valid version"),
857 DepFormatVersion::V4,
858 WorkspaceHackLineStyle::Full,
859 "../workspace-hack".into(),
860 )
861 }
862
863 fn parse(toml: &str) -> DocumentMut {
864 toml.parse().expect("test manifest is valid TOML")
865 }
866
867 #[test]
868 fn add_dependency_creates_section() {
869 let mut doc = parse(
870 r#"[package]
871name = "foo"
872"#,
873 );
874 add_dependency_to_document(&mut doc, "workspace-hack", hack_dep())
875 .expect("[dependencies] is created");
876 assert_eq!(
877 doc.to_string(),
878 r#"[package]
879name = "foo"
880
881[dependencies]
882workspace-hack = { version = "0.1", path = "../workspace-hack" }
883"#,
884 );
885 }
886
887 #[test]
888 fn add_dependency_replaces_existing_line() {
889 let mut doc = parse(
890 r#"[dependencies]
891workspace-hack = { path = "../old" }
892other = "1"
893"#,
894 );
895 add_dependency_to_document(&mut doc, "workspace-hack", hack_dep())
896 .expect("[dependencies] is a table");
897 assert_eq!(
898 doc.to_string(),
899 r#"[dependencies]
900workspace-hack = { version = "0.1", path = "../workspace-hack" }
901other = "1"
902"#,
903 "the existing line is replaced in place"
904 );
905 }
906
907 #[test]
908 fn add_dependency_drops_dev_dependency_line() {
909 let mut doc = parse(
910 r#"[package]
911name = "foo"
912
913[dev-dependencies]
914workspace-hack = { path = "../workspace-hack" }
915other = "1"
916
917[build-dependencies]
918workspace-hack = { path = "../workspace-hack" }
919"#,
920 );
921 add_dependency_to_document(&mut doc, "workspace-hack", hack_dep())
922 .expect("[dependencies] is created");
923 assert_eq!(
924 doc.to_string(),
925 r#"[package]
926name = "foo"
927
928[dev-dependencies]
929other = "1"
930
931[build-dependencies]
932workspace-hack = { path = "../workspace-hack" }
933
934[dependencies]
935workspace-hack = { version = "0.1", path = "../workspace-hack" }
936"#,
937 "the dev-dependency line is dropped, the build-dependency line is kept"
938 );
939 }
940
941 #[test]
942 fn add_dependency_rejects_non_table_dev_section() {
943 let mut doc = parse(
944 "dev-dependencies = 1
945",
946 );
947 assert_eq!(
948 add_dependency_to_document(&mut doc, "workspace-hack", hack_dep()),
949 Err(NotATableError::Section {
950 section: DependencySection::Dev
951 }),
952 );
953 }
954
955 #[test]
956 fn add_dependency_rejects_non_table_section() {
957 let mut doc = parse(
958 "dependencies = 1
959",
960 );
961 assert_eq!(
962 add_dependency_to_document(&mut doc, "workspace-hack", hack_dep()),
963 Err(NotATableError::Section {
964 section: DependencySection::Normal
965 }),
966 );
967 }
968
969 #[test]
970 fn remove_dependency_removes_line() {
971 let mut doc = parse(
972 r#"[dependencies]
973workspace-hack = { path = "../workspace-hack" }
974other = "1"
975"#,
976 );
977 remove_dependency_from_document(&mut doc, "workspace-hack")
978 .expect("[dependencies] is a table");
979 assert_eq!(
980 doc.to_string(),
981 r#"[dependencies]
982other = "1"
983"#,
984 );
985 }
986
987 #[test]
988 fn remove_dependency_removes_from_every_section() {
989 let mut doc = parse(
990 r#"[dependencies]
991workspace-hack = { path = "../workspace-hack" }
992
993[dev-dependencies]
994workspace-hack = { path = "../workspace-hack" }
995other = "1"
996
997[build-dependencies]
998workspace-hack = { path = "../workspace-hack" }
999"#,
1000 );
1001 remove_dependency_from_document(&mut doc, "workspace-hack")
1002 .expect("all sections are tables");
1003 assert_eq!(
1004 doc.to_string(),
1005 r#"[dependencies]
1006
1007[dev-dependencies]
1008other = "1"
1009
1010[build-dependencies]
1011"#,
1012 );
1013 }
1014
1015 #[test]
1016 fn remove_dependency_removes_platform_specific_lines() {
1017 let mut doc = parse(
1018 r#"[dependencies]
1019other = "1"
1020
1021[target.'cfg(windows)'.dependencies]
1022workspace-hack = { path = "../workspace-hack" }
1023
1024[target.'cfg(unix)'.dependencies]
1025workspace-hack = { path = "../workspace-hack" }
1026other = "1"
1027
1028[target.'cfg(unix)'.dev-dependencies]
1029workspace-hack = { path = "../workspace-hack" }
1030
1031[target.'cfg(unix)'.build-dependencies]
1032workspace-hack = { path = "../workspace-hack" }
1033"#,
1034 );
1035 remove_dependency_from_document(&mut doc, "workspace-hack")
1036 .expect("all sections are tables");
1037 assert_eq!(
1038 doc.to_string(),
1039 r#"[dependencies]
1040other = "1"
1041
1042[target.'cfg(windows)'.dependencies]
1043
1044[target.'cfg(unix)'.dependencies]
1045other = "1"
1046
1047[target.'cfg(unix)'.dev-dependencies]
1048
1049[target.'cfg(unix)'.build-dependencies]
1050"#,
1051 );
1052 }
1053
1054 #[test]
1055 fn remove_dependency_rejects_non_table_platform_section() {
1056 let mut doc = parse("target = 1\n");
1057 assert_eq!(
1058 remove_dependency_from_document(&mut doc, "workspace-hack"),
1059 Err(NotATableError::Target),
1060 );
1061
1062 let mut doc = parse("[target]\n'cfg(unix)' = 1\n");
1063 assert_eq!(
1064 remove_dependency_from_document(&mut doc, "workspace-hack"),
1065 Err(NotATableError::Platform {
1066 platform: "cfg(unix)".to_owned()
1067 }),
1068 );
1069
1070 let mut doc = parse("[target.'cfg(unix)']\ndev-dependencies = 1\n");
1071 assert_eq!(
1072 remove_dependency_from_document(&mut doc, "workspace-hack"),
1073 Err(NotATableError::PlatformSection {
1074 platform: "cfg(unix)".to_owned(),
1075 section: DependencySection::Dev,
1076 }),
1077 );
1078 }
1079
1080 #[test]
1081 fn remove_dependency_without_section_is_noop() {
1082 let toml = r#"[package]
1083name = "foo"
1084"#;
1085 let mut doc = parse(toml);
1086 remove_dependency_from_document(&mut doc, "workspace-hack")
1087 .expect("missing sections are fine");
1088 assert_eq!(doc.to_string(), toml, "no sections are added");
1089 }
1090
1091 #[test]
1092 fn remove_dependency_rejects_non_table_section() {
1093 let mut doc = parse(
1094 "build-dependencies = 1
1095",
1096 );
1097 assert_eq!(
1098 remove_dependency_from_document(&mut doc, "workspace-hack"),
1099 Err(NotATableError::Section {
1100 section: DependencySection::Build
1101 }),
1102 );
1103 }
1104
1105 #[test]
1106 fn test_inline_table_for_add() {
1107 let versions = vec![
1108 ("1.2.3", "1", "1"),
1109 ("1.2.3-a.1+g456", "1.2.3-a.1+g456", "1.2.3-a.1"),
1110 ];
1111
1112 for (version, version_str, version_str_v3) in versions {
1113 let version: Version = version.parse().unwrap();
1114 let itable = WorkspaceOp::inline_table_for_add(
1115 &version,
1116 DepFormatVersion::V1,
1117 WorkspaceHackLineStyle::Full,
1118 "../../path".into(),
1119 );
1120 assert_eq!(
1121 itable.to_string(),
1122 "{ path = \"../../path\"}",
1123 "dep format v1 matches"
1124 );
1125
1126 let itable = WorkspaceOp::inline_table_for_add(
1127 &version,
1128 DepFormatVersion::V2,
1129 WorkspaceHackLineStyle::Full,
1130 "../../path".into(),
1131 );
1132 assert_eq!(
1133 itable.to_string(),
1134 format!("{{ version = \"{version_str}\", path = \"../../path\" }}"),
1135 "dep format v2 matches"
1136 );
1137
1138 let itable = WorkspaceOp::inline_table_for_add(
1139 &version,
1140 DepFormatVersion::V3,
1141 WorkspaceHackLineStyle::Full,
1142 "../../path".into(),
1143 );
1144 assert_eq!(
1145 itable.to_string(),
1146 format!("{{ version = \"{version_str_v3}\", path = \"../../path\" }}"),
1147 "dep format v3 matches"
1148 );
1149
1150 let itable = WorkspaceOp::inline_table_for_add(
1151 &version,
1152 DepFormatVersion::V4,
1153 WorkspaceHackLineStyle::VersionOnly,
1154 "../../path".into(),
1155 );
1156 assert_eq!(
1157 itable.to_string(),
1158 format!("{{ version = \"{version_str_v3}\" }}"),
1159 "version only matches"
1160 );
1161
1162 let itable = WorkspaceOp::inline_table_for_add(
1163 &version,
1164 DepFormatVersion::V4,
1165 WorkspaceHackLineStyle::WorkspaceDotted,
1166 "../../path".into(),
1167 );
1168 let mut document = DocumentMut::new();
1169 document
1170 .as_table_mut()
1171 .insert("workspace-hack", Item::Value(Value::InlineTable(itable)));
1172 assert_eq!(
1173 document.to_string(),
1174 "workspace-hack.workspace = true\n",
1175 "workspace dep matches"
1176 );
1177 }
1178 }
1179}