1use std::collections::BTreeMap;
2use std::path::{Path, PathBuf};
3
4use super::manifest::str_or_1_len_table;
5
6#[derive(Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Clone)]
8pub struct Dependency {
9 pub name: String,
11 optional: Option<bool>,
12 pub features: Option<Vec<String>>,
14 default_features: Option<bool>,
15 source: DependencySource,
16 rename: Option<String>,
19
20 pub available_features: BTreeMap<String, Vec<String>>,
22}
23
24impl Dependency {
25 pub fn new(name: &str) -> Dependency {
27 Dependency {
28 name: name.into(),
29 ..Dependency::default()
30 }
31 }
32
33 pub fn set_version(mut self, version: &str) -> Dependency {
35 let version = version.split('+').next().unwrap();
39 let (old_path, old_registry) = match self.source {
40 DependencySource::Version { path, registry, .. } => (path, registry),
41 _ => (None, None),
42 };
43 self.source = DependencySource::Version {
44 version: Some(version.into()),
45 path: old_path,
46 registry: old_registry,
47 };
48 self
49 }
50
51 pub fn clear_version(mut self) -> Dependency {
53 if let DependencySource::Version {
54 version, registry, ..
55 } = &mut self.source
56 {
57 *version = None;
58 *registry = None;
59 }
60 self
61 }
62
63 pub fn set_available_features(
65 mut self,
66 available_features: BTreeMap<String, Vec<String>>,
67 ) -> Dependency {
68 self.available_features = available_features;
69 self
70 }
71
72 pub fn set_git(
74 mut self,
75 repo: &str,
76 branch: Option<String>,
77 tag: Option<String>,
78 rev: Option<String>,
79 ) -> Dependency {
80 self.source = DependencySource::Git {
81 repo: repo.into(),
82 branch,
83 tag,
84 rev,
85 };
86 self
87 }
88
89 pub fn set_path(mut self, path: PathBuf) -> Dependency {
95 assert!(
96 path.is_absolute(),
97 "Absolute path needed, got: {}",
98 path.display()
99 );
100 let (old_version, old_registry) = match self.source {
101 DependencySource::Version {
102 version, registry, ..
103 } => (version, registry),
104 _ => (None, None),
105 };
106 self.source = DependencySource::Version {
107 version: old_version,
108 path: Some(path),
109 registry: old_registry,
110 };
111 self
112 }
113
114 pub fn set_optional(mut self, opt: Option<bool>) -> Dependency {
116 self.optional = opt;
117 self
118 }
119 pub fn set_features(mut self, features: Option<Vec<String>>) -> Dependency {
121 self.features = features;
122 self
123 }
124
125 pub fn set_default_features(mut self, default_features: Option<bool>) -> Dependency {
127 self.default_features = default_features;
128 self
129 }
130
131 pub fn set_rename(mut self, rename: &str) -> Dependency {
133 self.rename = Some(rename.into());
134 self
135 }
136
137 pub fn set_registry(mut self, registry: &str) -> Dependency {
139 let (old_version, old_path) = match self.source {
140 DependencySource::Version { version, path, .. } => (version, path),
141 _ => (None, None),
142 };
143 self.source = DependencySource::Version {
144 version: old_version,
145 path: old_path,
146 registry: Some(registry.into()),
147 };
148 self
149 }
150
151 pub fn version(&self) -> Option<&str> {
153 if let DependencySource::Version {
154 version: Some(ref version),
155 ..
156 } = self.source
157 {
158 Some(version)
159 } else {
160 None
161 }
162 }
163
164 pub fn path(&self) -> Option<&Path> {
166 if let DependencySource::Version {
167 path: Some(ref path),
168 ..
169 } = self.source
170 {
171 Some(path.as_path())
172 } else {
173 None
174 }
175 }
176
177 pub fn registry(&self) -> Option<&str> {
179 if let DependencySource::Version {
180 registry: Some(ref registry),
181 ..
182 } = self.source
183 {
184 Some(registry)
185 } else {
186 None
187 }
188 }
189
190 pub fn git(&self) -> Option<&str> {
192 if let DependencySource::Git { repo, .. } = &self.source {
193 Some(repo.as_str())
194 } else {
195 None
196 }
197 }
198
199 pub fn rename(&self) -> Option<&str> {
201 self.rename.as_deref()
202 }
203
204 pub fn default_features(&self) -> Option<bool> {
206 self.default_features
207 }
208}
209
210impl Dependency {
211 pub fn from_toml(crate_root: &Path, key: &str, item: &toml_edit::Item) -> Option<Self> {
213 if let Some(version) = item.as_str() {
214 let dep = Dependency::new(key).set_version(version);
215 Some(dep)
216 } else if let Some(table) = item.as_table_like() {
217 let (name, rename) = if let Some(value) = table.get("package") {
218 (value.as_str()?.to_owned(), Some(key.to_owned()))
219 } else {
220 (key.to_owned(), None)
221 };
222
223 let source = if let Some(repo) = table.get("git") {
224 let repo = repo.as_str()?.to_owned();
225 let branch = if let Some(value) = table.get("branch") {
226 Some(value.as_str()?.to_owned())
227 } else {
228 None
229 };
230 let tag = if let Some(value) = table.get("tag") {
231 Some(value.as_str()?.to_owned())
232 } else {
233 None
234 };
235 let rev = if let Some(value) = table.get("rev") {
236 Some(value.as_str()?.to_owned())
237 } else {
238 None
239 };
240 DependencySource::Git {
241 repo,
242 branch,
243 tag,
244 rev,
245 }
246 } else {
247 let version = if let Some(value) = table.get("version") {
248 Some(value.as_str()?.to_owned())
249 } else {
250 None
251 };
252 let path = if let Some(value) = table.get("path") {
253 let path = value.as_str()?;
254 let path = crate_root.join(path);
255 Some(path)
256 } else {
257 None
258 };
259 let registry = if let Some(value) = table.get("registry") {
260 Some(value.as_str()?.to_owned())
261 } else {
262 None
263 };
264 DependencySource::Version {
265 version,
266 path,
267 registry,
268 }
269 };
270
271 let default_features = if let Some(value) = table.get("default-features") {
272 value.as_bool()?
273 } else {
274 true
275 };
276 let default_features = Some(default_features);
277
278 let features = if let Some(value) = table.get("features") {
279 Some(
280 value
281 .as_array()?
282 .iter()
283 .map(|v| v.as_str().map(|s| s.to_owned()))
284 .collect::<Option<Vec<String>>>()?,
285 )
286 } else {
287 None
288 };
289
290 let available_features = BTreeMap::default();
291
292 let optional = if let Some(value) = table.get("optional") {
293 value.as_bool()?
294 } else {
295 false
296 };
297 let optional = Some(optional);
298
299 let dep = Dependency {
300 name,
301 rename,
302 source,
303 default_features,
304 features,
305 available_features,
306 optional,
307 };
308 Some(dep)
309 } else {
310 None
311 }
312 }
313
314 pub fn toml_key(&self) -> &str {
318 self.rename().unwrap_or(&self.name)
319 }
320
321 pub fn to_toml(&self, crate_root: &Path) -> toml_edit::Item {
332 assert!(
333 crate_root.is_absolute(),
334 "Absolute path needed, got: {}",
335 crate_root.display()
336 );
337 let data: toml_edit::Item = match (
338 self.optional.unwrap_or(false),
339 self.features.as_ref(),
340 self.default_features.unwrap_or(true),
341 self.source.clone(),
342 self.rename.as_ref(),
343 ) {
344 (
346 false,
347 None,
348 true,
349 DependencySource::Version {
350 version: Some(v),
351 path: None,
352 registry: None,
353 },
354 None,
355 ) => toml_edit::value(v),
356 (_, _, _, _, _) => {
358 let mut data = toml_edit::InlineTable::default();
359
360 match &self.source {
361 DependencySource::Version {
362 version,
363 path,
364 registry,
365 } => {
366 if let Some(v) = version {
367 data.insert("version", v.into());
368 }
369 if let Some(p) = path {
370 let relpath = path_field(crate_root, p);
371 data.insert("path", relpath.into());
372 }
373 if let Some(r) = registry {
374 data.insert("registry", r.into());
375 }
376 }
377 DependencySource::Git {
378 repo,
379 branch,
380 tag,
381 rev,
382 } => {
383 data.insert("git", repo.into());
384 if let Some(branch) = branch {
385 data.insert("branch", branch.into());
386 }
387 if let Some(tag) = tag {
388 data.insert("tag", tag.into());
389 }
390 if let Some(rev) = rev {
391 data.insert("rev", rev.into());
392 }
393 }
394 }
395 if self.rename.is_some() {
396 data.insert("package", self.name.as_str().into());
397 }
398 match self.default_features {
399 Some(true) | None => {}
400 Some(false) => {
401 data.insert("default-features", false.into());
402 }
403 }
404 if let Some(features) = self.features.as_deref() {
405 let features: toml_edit::Value = features.iter().cloned().collect();
406 data.insert("features", features);
407 }
408 match self.optional {
409 Some(false) | None => {}
410 Some(true) => {
411 data.insert("optional", true.into());
412 }
413 }
414
415 toml_edit::value(toml_edit::Value::InlineTable(data))
416 }
417 };
418
419 data
420 }
421
422 pub fn update_toml(&self, crate_root: &Path, item: &mut toml_edit::Item) {
424 #[allow(clippy::if_same_then_else)]
425 if str_or_1_len_table(item) {
426 *item = self.to_toml(crate_root);
428 } else if !is_package_eq(item, &self.name, self.rename.as_deref()) {
429 *item = self.to_toml(crate_root);
431 } else if let Some(table) = item.as_table_like_mut() {
432 match &self.source {
433 DependencySource::Version {
434 version,
435 path,
436 registry,
437 } => {
438 if let Some(v) = version {
439 table.insert("version", toml_edit::value(v));
440 } else {
441 table.remove("version");
442 }
443 if let Some(p) = path {
444 let relpath = path_field(crate_root, p);
445 table.insert("path", toml_edit::value(relpath));
446 } else {
447 table.remove("path");
448 }
449 if let Some(r) = registry {
450 table.insert("registry", toml_edit::value(r));
451 }
452 for key in ["git", "branch", "tag", "rev"] {
453 table.remove(key);
454 }
455 }
456 DependencySource::Git {
457 repo,
458 branch,
459 tag,
460 rev,
461 } => {
462 table.insert("git", toml_edit::value(repo));
463 if let Some(branch) = branch {
464 table.insert("branch", toml_edit::value(branch));
465 } else {
466 table.remove("branch");
467 }
468 if let Some(tag) = tag {
469 table.insert("tag", toml_edit::value(tag));
470 } else {
471 table.remove("tag");
472 }
473 if let Some(rev) = rev {
474 table.insert("rev", toml_edit::value(rev));
475 } else {
476 table.remove("rev");
477 }
478 for key in ["version", "path", "registry"] {
479 table.remove(key);
480 }
481 }
482 }
483 if self.rename.is_some() {
484 table.insert("package", toml_edit::value(self.name.as_str()));
485 }
486 match self.default_features {
487 Some(true) => {
488 table.remove("default-features");
489 }
490 Some(false) => {
491 table.insert("default-features", toml_edit::value(false));
492 }
493 None => {}
494 }
495 if let Some(new_features) = self.features.as_deref() {
496 let mut features = table
497 .get("features")
498 .and_then(|i| i.as_value())
499 .and_then(|v| v.as_array())
500 .and_then(|a| {
501 a.iter()
502 .map(|v| v.as_str())
503 .collect::<Option<indexmap::IndexSet<_>>>()
504 })
505 .unwrap_or_default();
506 features.extend(new_features.iter().map(|s| s.as_str()));
507 let features = toml_edit::value(features.into_iter().collect::<toml_edit::Value>());
508 table.insert("features", features);
509 }
510 match self.optional {
511 Some(true) => {
512 table.insert("optional", toml_edit::value(true));
513 }
514 Some(false) => {
515 table.remove("optional");
516 }
517 None => {}
518 }
519
520 table.fmt();
521 } else {
522 unreachable!("Invalid dependency type: {}", item.type_name());
523 }
524 }
525}
526
527fn path_field(crate_root: &Path, abs_path: &Path) -> String {
528 let relpath = pathdiff::diff_paths(abs_path, crate_root).expect("both paths are absolute");
529 let relpath = relpath.to_str().unwrap().replace('\\', "/");
530 relpath
531}
532
533fn is_package_eq(item: &mut toml_edit::Item, name: &str, rename: Option<&str>) -> bool {
534 if let Some(table) = item.as_table_like_mut() {
535 let existing_package = table.get("package").and_then(|i| i.as_str());
536 let new_package = rename.map(|_| name);
537 existing_package == new_package
538 } else {
539 false
540 }
541}
542
543impl Default for Dependency {
544 fn default() -> Dependency {
545 Dependency {
546 name: "".into(),
547 rename: None,
548 optional: None,
549 features: None,
550 default_features: None,
551 source: DependencySource::Version {
552 version: None,
553 path: None,
554 registry: None,
555 },
556 available_features: BTreeMap::default(),
557 }
558 }
559}
560
561#[derive(Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Clone)]
562enum DependencySource {
563 Version {
564 version: Option<String>,
565 path: Option<PathBuf>,
566 registry: Option<String>,
567 },
568 Git {
569 repo: String,
570 branch: Option<String>,
571 tag: Option<String>,
572 rev: Option<String>,
573 },
574}
575
576#[cfg(test)]
577mod tests {
578 use super::super::dependency::Dependency;
579 use std::path::Path;
580
581 #[test]
582 fn to_toml_simple_dep() {
583 let crate_root = dunce::canonicalize(Path::new("/")).expect("root exists");
584 let dep = Dependency::new("dep");
585 let key = dep.toml_key();
586 let item = dep.to_toml(&crate_root);
587
588 assert_eq!(key, "dep".to_owned());
589
590 verify_roundtrip(&crate_root, key, &item);
591 }
592
593 #[test]
594 fn to_toml_simple_dep_with_version() {
595 let crate_root = dunce::canonicalize(Path::new("/")).expect("root exists");
596 let dep = Dependency::new("dep").set_version("1.0");
597 let key = dep.toml_key();
598 let item = dep.to_toml(&crate_root);
599
600 assert_eq!(key, "dep".to_owned());
601 assert_eq!(item.as_str(), Some("1.0"));
602
603 verify_roundtrip(&crate_root, key, &item);
604 }
605
606 #[test]
607 fn to_toml_optional_dep() {
608 let crate_root = dunce::canonicalize(Path::new("/")).expect("root exists");
609 let dep = Dependency::new("dep").set_optional(Some(true));
610 let key = dep.toml_key();
611 let item = dep.to_toml(&crate_root);
612
613 assert_eq!(key, "dep".to_owned());
614 assert!(item.is_inline_table());
615
616 let dep = item.as_inline_table().unwrap();
617 assert_eq!(dep.get("optional").unwrap().as_bool(), Some(true));
618
619 verify_roundtrip(&crate_root, key, &item);
620 }
621
622 #[test]
623 fn to_toml_dep_without_default_features() {
624 let crate_root = dunce::canonicalize(Path::new("/")).expect("root exists");
625 let dep = Dependency::new("dep").set_default_features(Some(false));
626 let key = dep.toml_key();
627 let item = dep.to_toml(&crate_root);
628
629 assert_eq!(key, "dep".to_owned());
630 assert!(item.is_inline_table());
631
632 let dep = item.as_inline_table().unwrap();
633 assert_eq!(dep.get("default-features").unwrap().as_bool(), Some(false));
634
635 verify_roundtrip(&crate_root, key, &item);
636 }
637
638 #[test]
639 fn to_toml_dep_with_path_source() {
640 let root = dunce::canonicalize(Path::new("/")).expect("root exists");
641 let crate_root = root.join("foo");
642 let dep = Dependency::new("dep").set_path(root.join("bar"));
643 let key = dep.toml_key();
644 let item = dep.to_toml(&crate_root);
645
646 assert_eq!(key, "dep".to_owned());
647 assert!(item.is_inline_table());
648
649 let dep = item.as_inline_table().unwrap();
650 assert_eq!(dep.get("path").unwrap().as_str(), Some("../bar"));
651
652 verify_roundtrip(&crate_root, key, &item);
653 }
654
655 #[test]
656 fn to_toml_dep_with_git_source() {
657 let crate_root = dunce::canonicalize(Path::new("/")).expect("root exists");
658 let dep = Dependency::new("dep").set_git("https://foor/bar.git", None, None, None);
659 let key = dep.toml_key();
660 let item = dep.to_toml(&crate_root);
661
662 assert_eq!(key, "dep".to_owned());
663 assert!(item.is_inline_table());
664
665 let dep = item.as_inline_table().unwrap();
666 assert_eq!(
667 dep.get("git").unwrap().as_str(),
668 Some("https://foor/bar.git")
669 );
670
671 verify_roundtrip(&crate_root, key, &item);
672 }
673
674 #[test]
675 fn to_toml_renamed_dep() {
676 let crate_root = dunce::canonicalize(Path::new("/")).expect("root exists");
677 let dep = Dependency::new("dep").set_rename("d");
678 let key = dep.toml_key();
679 let item = dep.to_toml(&crate_root);
680
681 assert_eq!(key, "d".to_owned());
682 assert!(item.is_inline_table());
683
684 let dep = item.as_inline_table().unwrap();
685 assert_eq!(dep.get("package").unwrap().as_str(), Some("dep"));
686
687 verify_roundtrip(&crate_root, key, &item);
688 }
689
690 #[test]
691 fn to_toml_dep_from_alt_registry() {
692 let crate_root = dunce::canonicalize(Path::new("/")).expect("root exists");
693 let dep = Dependency::new("dep").set_registry("alternative");
694 let key = dep.toml_key();
695 let item = dep.to_toml(&crate_root);
696
697 assert_eq!(key, "dep".to_owned());
698 assert!(item.is_inline_table());
699
700 let dep = item.as_inline_table().unwrap();
701 assert_eq!(dep.get("registry").unwrap().as_str(), Some("alternative"));
702
703 verify_roundtrip(&crate_root, key, &item);
704 }
705
706 #[test]
707 fn to_toml_complex_dep() {
708 let crate_root = dunce::canonicalize(Path::new("/")).expect("root exists");
709 let dep = Dependency::new("dep")
710 .set_version("1.0")
711 .set_default_features(Some(false))
712 .set_rename("d");
713 let key = dep.toml_key();
714 let item = dep.to_toml(&crate_root);
715
716 assert_eq!(key, "d".to_owned());
717 assert!(item.is_inline_table());
718
719 let dep = item.as_inline_table().unwrap();
720 assert_eq!(dep.get("package").unwrap().as_str(), Some("dep"));
721 assert_eq!(dep.get("version").unwrap().as_str(), Some("1.0"));
722 assert_eq!(dep.get("default-features").unwrap().as_bool(), Some(false));
723
724 verify_roundtrip(&crate_root, key, &item);
725 }
726
727 #[test]
728 fn paths_with_forward_slashes_are_left_as_is() {
729 let crate_root = dunce::canonicalize(Path::new("/")).expect("root exists");
730 let path = crate_root.join("sibling/crate");
731 let relpath = "sibling/crate";
732 let dep = Dependency::new("dep").set_path(path);
733 let key = dep.toml_key();
734 let item = dep.to_toml(&crate_root);
735
736 let table = item.as_inline_table().unwrap();
737 let got = table.get("path").unwrap().as_str().unwrap();
738 assert_eq!(got, relpath);
739
740 verify_roundtrip(&crate_root, key, &item);
741 }
742
743 #[test]
744 #[cfg(windows)]
745 fn normalise_windows_style_paths() {
746 let crate_root = dunce::canonicalize(Path::new("/")).expect("root exists");
747 let original = crate_root.join(r"sibling\crate");
748 let should_be = "sibling/crate";
749 let dep = Dependency::new("dep").set_path(original);
750 let key = dep.toml_key();
751 let item = dep.to_toml(&crate_root);
752
753 let table = item.as_inline_table().unwrap();
754 let got = table.get("path").unwrap().as_str().unwrap();
755 assert_eq!(got, should_be);
756
757 verify_roundtrip(&crate_root, key, &item);
758 }
759
760 fn verify_roundtrip(crate_root: &Path, key: &str, item: &toml_edit::Item) {
761 let roundtrip = Dependency::from_toml(crate_root, key, item).unwrap();
762 let round_key = roundtrip.toml_key();
763 let round_item = roundtrip.to_toml(crate_root);
764 assert_eq!(key, round_key);
765 assert_eq!(item.to_string(), round_item.to_string());
766 }
767}