cargo_edit_9/
dependency.rs

1use std::collections::BTreeMap;
2use std::path::{Path, PathBuf};
3
4use super::manifest::str_or_1_len_table;
5
6/// A dependency handled by Cargo
7#[derive(Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Clone)]
8pub struct Dependency {
9    /// The name of the dependency (as it is set in its `Cargo.toml` and known to crates.io)
10    pub name: String,
11    optional: Option<bool>,
12    /// List of features to add (or None to keep features unchanged).
13    pub features: Option<Vec<String>>,
14    default_features: Option<bool>,
15    source: DependencySource,
16    /// If the dependency is renamed, this is the new name for the dependency
17    /// as a string.  None if it is not renamed.
18    rename: Option<String>,
19
20    /// Features that are exposed by the dependency
21    pub available_features: BTreeMap<String, Vec<String>>,
22}
23
24impl Dependency {
25    /// Create a new dependency with a name
26    pub fn new(name: &str) -> Dependency {
27        Dependency {
28            name: name.into(),
29            ..Dependency::default()
30        }
31    }
32
33    /// Set dependency to a given version
34    pub fn set_version(mut self, version: &str) -> Dependency {
35        // versions might have semver metadata appended which we do not want to
36        // store in the cargo toml files.  This would cause a warning upon compilation
37        // ("version requirement […] includes semver metadata which will be ignored")
38        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    /// Remove the existing version requirement
52    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    /// Set the available features of the dependency to a given vec
64    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    /// Set dependency to a given repository
73    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    /// Set dependency to a given path
90    ///
91    /// # Panic
92    ///
93    /// Panics if the path is relative
94    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    /// Set whether the dependency is optional
115    pub fn set_optional(mut self, opt: Option<bool>) -> Dependency {
116        self.optional = opt;
117        self
118    }
119    /// Set features as an array of string (does some basic parsing)
120    pub fn set_features(mut self, features: Option<Vec<String>>) -> Dependency {
121        self.features = features;
122        self
123    }
124
125    /// Set the value of default-features for the dependency
126    pub fn set_default_features(mut self, default_features: Option<bool>) -> Dependency {
127        self.default_features = default_features;
128        self
129    }
130
131    /// Set the alias for the dependency
132    pub fn set_rename(mut self, rename: &str) -> Dependency {
133        self.rename = Some(rename.into());
134        self
135    }
136
137    /// Set the value of registry for the dependency
138    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    /// Get version of dependency
152    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    /// Get the path of the dependency
165    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    /// Get registry of the dependency
178    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    /// Get the git repo of the dependency
191    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    /// Get the alias for the dependency (if any)
200    pub fn rename(&self) -> Option<&str> {
201        self.rename.as_deref()
202    }
203
204    /// Whether default features are activated
205    pub fn default_features(&self) -> Option<bool> {
206        self.default_features
207    }
208}
209
210impl Dependency {
211    /// Create a dependency from a TOML table entry
212    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    /// Get the dependency name as defined in the manifest,
315    /// that is, either the alias (rename field if Some),
316    /// or the official package name (name field).
317    pub fn toml_key(&self) -> &str {
318        self.rename().unwrap_or(&self.name)
319    }
320
321    /// Convert dependency to TOML
322    ///
323    /// Returns a tuple with the dependency's name and either the version as a `String`
324    /// or the path/git repository as an `InlineTable`.
325    /// (If the dependency is set as `optional` or `default-features` is set to `false`,
326    /// an `InlineTable` is returned in any case.)
327    ///
328    /// # Panic
329    ///
330    /// Panics if the path is relative
331    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            // Extra short when version flag only
345            (
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            // Other cases are represented as an inline table
357            (_, _, _, _, _) => {
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    /// Modify existing entry to match this dependency
423    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            // Nothing to preserve
427            *item = self.to_toml(crate_root);
428        } else if !is_package_eq(item, &self.name, self.rename.as_deref()) {
429            // No existing keys are relevant when the package changes
430            *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}