cargo_edit/
dependency.rs

1use std::collections::BTreeMap;
2use std::path::{Path, PathBuf};
3
4use indexmap::IndexSet;
5use toml_edit::KeyMut;
6
7use super::manifest::str_or_1_len_table;
8use crate::CargoResult;
9
10/// A dependency handled by Cargo
11///
12/// `None` means the field will be blank in TOML
13#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
14#[non_exhaustive]
15pub struct Dependency {
16    /// The name of the dependency (as it is set in its `Cargo.toml` and known to crates.io)
17    pub name: String,
18    /// Whether the dependency is opted-in with a feature flag
19    pub optional: Option<bool>,
20
21    /// List of features to add (or None to keep features unchanged).
22    pub features: Option<Vec<String>>,
23    /// Whether default features are enabled
24    pub default_features: Option<bool>,
25    /// List of features inherited from a workspace dependency
26    pub inherited_features: Option<Vec<String>>,
27
28    /// Where the dependency comes from
29    pub source: Option<Source>,
30    /// Non-default registry
31    pub registry: Option<String>,
32
33    /// If the dependency is renamed, this is the new name for the dependency
34    /// as a string.  None if it is not renamed.
35    pub rename: Option<String>,
36
37    /// Features that are exposed by the dependency
38    pub available_features: BTreeMap<String, Vec<String>>,
39}
40
41impl Dependency {
42    /// Create a new dependency with a name
43    pub fn new(name: &str) -> Self {
44        Self {
45            name: name.into(),
46            optional: None,
47            features: None,
48            default_features: None,
49            inherited_features: None,
50            source: None,
51            registry: None,
52            rename: None,
53            available_features: Default::default(),
54        }
55    }
56
57    /// Set dependency to a given version
58    pub fn set_source(mut self, source: impl Into<Source>) -> Self {
59        self.source = Some(source.into());
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    ) -> Self {
68        self.available_features = available_features;
69        self
70    }
71
72    /// Set whether the dependency is optional
73    #[allow(dead_code)]
74    pub fn set_optional(mut self, opt: bool) -> Self {
75        self.optional = Some(opt);
76        self
77    }
78
79    /// Set features as an array of string (does some basic parsing)
80    #[allow(dead_code)]
81    pub fn set_features(mut self, features: Vec<String>) -> Self {
82        self.features = Some(features);
83        self
84    }
85    /// Set features as an array of string (does some basic parsing)
86    pub fn extend_features(mut self, features: impl IntoIterator<Item = String>) -> Self {
87        self.features
88            .get_or_insert_with(Default::default)
89            .extend(features);
90        self
91    }
92
93    /// Set the value of default-features for the dependency
94    #[allow(dead_code)]
95    pub fn set_default_features(mut self, default_features: bool) -> Self {
96        self.default_features = Some(default_features);
97        self
98    }
99
100    /// Set the alias for the dependency
101    pub fn set_rename(mut self, rename: &str) -> Self {
102        self.rename = Some(rename.into());
103        self
104    }
105
106    /// Set the value of registry for the dependency
107    pub fn set_registry(mut self, registry: impl Into<String>) -> Self {
108        self.registry = Some(registry.into());
109        self
110    }
111
112    /// Set features as an array of string (does some basic parsing)
113    pub fn set_inherited_features(mut self, features: Vec<String>) -> Self {
114        self.inherited_features = Some(features);
115        self
116    }
117
118    /// Get the dependency source
119    pub fn source(&self) -> Option<&Source> {
120        self.source.as_ref()
121    }
122
123    /// Get version of dependency
124    pub fn version(&self) -> Option<&str> {
125        match self.source()? {
126            Source::Registry(src) => Some(src.version.as_str()),
127            Source::Path(src) => src.version.as_deref(),
128            Source::Git(src) => src.version.as_deref(),
129            Source::Workspace(_) => None,
130        }
131    }
132
133    /// Get registry of the dependency
134    pub fn registry(&self) -> Option<&str> {
135        self.registry.as_deref()
136    }
137
138    /// Get the alias for the dependency (if any)
139    pub fn rename(&self) -> Option<&str> {
140        self.rename.as_deref()
141    }
142
143    /// Whether default features are activated
144    pub fn default_features(&self) -> Option<bool> {
145        self.default_features
146    }
147
148    /// Get whether the dep is optional
149    pub fn optional(&self) -> Option<bool> {
150        self.optional
151    }
152}
153
154impl Dependency {
155    /// Create a dependency from a TOML table entry
156    pub fn from_toml(crate_root: &Path, key: &str, item: &toml_edit::Item) -> CargoResult<Self> {
157        if let Some(version) = item.as_str() {
158            let dep = Self::new(key).set_source(RegistrySource::new(version));
159            Ok(dep)
160        } else if let Some(table) = item.as_table_like() {
161            let (name, rename) = if let Some(value) = table.get("package") {
162                (
163                    value
164                        .as_str()
165                        .ok_or_else(|| invalid_type(key, "package", value.type_name(), "string"))?
166                        .to_owned(),
167                    Some(key.to_owned()),
168                )
169            } else {
170                (key.to_owned(), None)
171            };
172
173            let source: Source =
174                if let Some(git) = table.get("git") {
175                    let mut src = GitSource::new(
176                        git.as_str()
177                            .ok_or_else(|| invalid_type(key, "git", git.type_name(), "string"))?,
178                    );
179                    if let Some(value) = table.get("branch") {
180                        src = src.set_branch(value.as_str().ok_or_else(|| {
181                            invalid_type(key, "branch", value.type_name(), "string")
182                        })?);
183                    }
184                    if let Some(value) = table.get("tag") {
185                        src = src.set_tag(value.as_str().ok_or_else(|| {
186                            invalid_type(key, "tag", value.type_name(), "string")
187                        })?);
188                    }
189                    if let Some(value) = table.get("rev") {
190                        src = src.set_rev(value.as_str().ok_or_else(|| {
191                            invalid_type(key, "rev", value.type_name(), "string")
192                        })?);
193                    }
194                    if let Some(value) = table.get("version") {
195                        src = src.set_version(value.as_str().ok_or_else(|| {
196                            invalid_type(key, "version", value.type_name(), "string")
197                        })?);
198                    }
199                    src.into()
200                } else if let Some(path) = table.get("path") {
201                    let path = crate_root
202                        .join(path.as_str().ok_or_else(|| {
203                            invalid_type(key, "path", path.type_name(), "string")
204                        })?);
205                    let mut src = PathSource::new(path);
206                    if let Some(value) = table.get("version") {
207                        src = src.set_version(value.as_str().ok_or_else(|| {
208                            invalid_type(key, "version", value.type_name(), "string")
209                        })?);
210                    }
211                    src.into()
212                } else if let Some(version) = table.get("version") {
213                    let src = RegistrySource::new(version.as_str().ok_or_else(|| {
214                        invalid_type(key, "version", version.type_name(), "string")
215                    })?);
216                    src.into()
217                } else if let Some(workspace) = table.get("workspace") {
218                    let workspace_bool = workspace.as_bool().ok_or_else(|| {
219                        invalid_type(key, "workspace", workspace.type_name(), "bool")
220                    })?;
221                    if !workspace_bool {
222                        anyhow::bail!("`{key}.workspace = false` is unsupported")
223                    }
224                    let src = WorkspaceSource::new();
225                    src.into()
226                } else {
227                    anyhow::bail!("Unrecognized dependency source for `{key}`");
228                };
229            let registry = if let Some(value) = table.get("registry") {
230                Some(
231                    value
232                        .as_str()
233                        .ok_or_else(|| invalid_type(key, "registry", value.type_name(), "string"))?
234                        .to_owned(),
235                )
236            } else {
237                None
238            };
239
240            let default_features = table.get("default-features").and_then(|v| v.as_bool());
241            if table.contains_key("default_features") {
242                anyhow::bail!(
243                    "Use of `default_features` in `{key}` is unsupported, please switch to `default-features`"
244                );
245            }
246
247            let features = if let Some(value) = table.get("features") {
248                Some(
249                    value
250                        .as_array()
251                        .ok_or_else(|| invalid_type(key, "features", value.type_name(), "array"))?
252                        .iter()
253                        .map(|v| {
254                            v.as_str().map(|s| s.to_owned()).ok_or_else(|| {
255                                invalid_type(key, "features", v.type_name(), "string")
256                            })
257                        })
258                        .collect::<CargoResult<Vec<String>>>()?,
259                )
260            } else {
261                None
262            };
263
264            let available_features = BTreeMap::default();
265
266            let optional = table.get("optional").and_then(|v| v.as_bool());
267
268            let dep = Self {
269                name,
270                rename,
271                source: Some(source),
272                registry,
273                default_features,
274                features,
275                available_features,
276                optional,
277                inherited_features: None,
278            };
279            Ok(dep)
280        } else {
281            anyhow::bail!("Unrecognized` dependency entry format for `{key}");
282        }
283    }
284
285    /// Get the dependency name as defined in the manifest,
286    /// that is, either the alias (rename field if Some),
287    /// or the official package name (name field).
288    pub fn toml_key(&self) -> &str {
289        self.rename().unwrap_or(&self.name)
290    }
291
292    /// Convert dependency to TOML
293    ///
294    /// Returns a tuple with the dependency's name and either the version as a `String`
295    /// or the path/git repository as an `InlineTable`.
296    /// (If the dependency is set as `optional` or `default-features` is set to `false`,
297    /// an `InlineTable` is returned in any case.)
298    ///
299    /// # Panic
300    ///
301    /// Panics if the path is relative
302    pub fn to_toml(&self, crate_root: &Path) -> toml_edit::Item {
303        assert!(
304            crate_root.is_absolute(),
305            "Absolute path needed, got: {}",
306            crate_root.display()
307        );
308        let table: toml_edit::Item = match (
309            self.optional.unwrap_or(false),
310            self.features.as_ref(),
311            self.default_features.unwrap_or(true),
312            self.source.as_ref(),
313            self.registry.as_ref(),
314            self.rename.as_ref(),
315        ) {
316            // Extra short when version flag only
317            (
318                false,
319                None,
320                true,
321                Some(Source::Registry(RegistrySource { version: v })),
322                None,
323                None,
324            ) => toml_edit::value(v),
325            (false, None, true, Some(Source::Workspace(WorkspaceSource {})), None, None) => {
326                let mut table = toml_edit::InlineTable::default();
327                table.set_dotted(true);
328                table.insert("workspace", true.into());
329                toml_edit::value(toml_edit::Value::InlineTable(table))
330            }
331            // Other cases are represented as an inline table
332            (_, _, _, _, _, _) => {
333                let mut table = toml_edit::InlineTable::default();
334
335                match &self.source {
336                    Some(Source::Registry(src)) => {
337                        table.insert("version", src.version.as_str().into());
338                    }
339                    Some(Source::Path(src)) => {
340                        let relpath = path_field(crate_root, &src.path);
341                        if let Some(r) = src.version.as_deref() {
342                            table.insert("version", r.into());
343                        }
344                        table.insert("path", relpath.into());
345                    }
346                    Some(Source::Git(src)) => {
347                        table.insert("git", src.git.as_str().into());
348                        if let Some(branch) = src.branch.as_deref() {
349                            table.insert("branch", branch.into());
350                        }
351                        if let Some(tag) = src.tag.as_deref() {
352                            table.insert("tag", tag.into());
353                        }
354                        if let Some(rev) = src.rev.as_deref() {
355                            table.insert("rev", rev.into());
356                        }
357                        if let Some(r) = src.version.as_deref() {
358                            table.insert("version", r.into());
359                        }
360                    }
361                    Some(Source::Workspace(_)) => {
362                        table.insert("workspace", true.into());
363                    }
364                    None => {}
365                }
366                if table.contains_key("version") {
367                    if let Some(r) = self.registry.as_deref() {
368                        table.insert("registry", r.into());
369                    }
370                }
371
372                if self.rename.is_some() {
373                    table.insert("package", self.name.as_str().into());
374                }
375                if let Some(v) = self.default_features {
376                    table.insert("default-features", v.into());
377                }
378                if let Some(features) = self.features.as_ref() {
379                    let features: toml_edit::Value = features.iter().cloned().collect();
380                    table.insert("features", features);
381                }
382                if let Some(v) = self.optional {
383                    table.insert("optional", v.into());
384                }
385
386                toml_edit::value(toml_edit::Value::InlineTable(table))
387            }
388        };
389
390        table
391    }
392
393    /// Modify existing entry to match this dependency
394    pub fn update_toml(&self, crate_root: &Path, key: &mut KeyMut, item: &mut toml_edit::Item) {
395        if str_or_1_len_table(item) {
396            // Nothing to preserve
397            *item = self.to_toml(crate_root);
398            key.fmt();
399        } else if let Some(table) = item.as_table_like_mut() {
400            match &self.source {
401                Some(Source::Registry(src)) => {
402                    overwrite_value(table, "version", src.version.as_str());
403
404                    for key in ["path", "git", "branch", "tag", "rev", "workspace"] {
405                        table.remove(key);
406                    }
407                }
408                Some(Source::Path(src)) => {
409                    let relpath = path_field(crate_root, &src.path);
410                    overwrite_value(table, "path", relpath);
411                    if let Some(r) = src.version.as_deref() {
412                        overwrite_value(table, "version", r);
413                    } else {
414                        table.remove("version");
415                    }
416
417                    for key in ["git", "branch", "tag", "rev", "workspace"] {
418                        table.remove(key);
419                    }
420                }
421                Some(Source::Git(src)) => {
422                    overwrite_value(table, "git", src.git.as_str());
423                    if let Some(branch) = src.branch.as_deref() {
424                        overwrite_value(table, "branch", branch);
425                    } else {
426                        table.remove("branch");
427                    }
428                    if let Some(tag) = src.tag.as_deref() {
429                        overwrite_value(table, "tag", tag);
430                    } else {
431                        table.remove("tag");
432                    }
433                    if let Some(rev) = src.rev.as_deref() {
434                        overwrite_value(table, "rev", rev);
435                    } else {
436                        table.remove("rev");
437                    }
438                    if let Some(r) = src.version.as_deref() {
439                        overwrite_value(table, "version", r);
440                    } else {
441                        table.remove("version");
442                    }
443
444                    for key in ["path", "workspace"] {
445                        table.remove(key);
446                    }
447                }
448                Some(Source::Workspace(_)) => {
449                    overwrite_value(table, "workspace", true);
450                    table.set_dotted(true);
451                    key.fmt();
452                    for key in [
453                        "version",
454                        "registry",
455                        "registry-index",
456                        "path",
457                        "git",
458                        "branch",
459                        "tag",
460                        "rev",
461                        "package",
462                        "default-features",
463                    ] {
464                        table.remove(key);
465                    }
466                }
467                None => {}
468            }
469            if table.contains_key("version") {
470                if let Some(r) = self.registry.as_deref() {
471                    overwrite_value(table, "registry", r);
472                } else {
473                    table.remove("registry");
474                }
475            } else {
476                table.remove("registry");
477            }
478
479            if self.rename.is_some() {
480                overwrite_value(table, "package", self.name.as_str());
481            }
482            match self.default_features {
483                Some(v) => {
484                    overwrite_value(table, "default-features", v);
485                }
486                None => {
487                    table.remove("default-features");
488                }
489            }
490            if let Some(new_features) = self.features.as_ref() {
491                let mut features = table
492                    .get("features")
493                    .and_then(|i| i.as_value())
494                    .and_then(|v| v.as_array())
495                    .and_then(|a| {
496                        a.iter()
497                            .map(|v| v.as_str())
498                            .collect::<Option<IndexSet<_>>>()
499                    })
500                    .unwrap_or_default();
501                features.extend(new_features.iter().map(|s| s.as_str()));
502                let features = features.into_iter().collect::<toml_edit::Value>();
503                table.set_dotted(false);
504                overwrite_value(table, "features", features);
505            } else {
506                table.remove("features");
507            }
508            match self.optional {
509                Some(v) => {
510                    table.set_dotted(false);
511                    overwrite_value(table, "optional", v);
512                }
513                None => {
514                    table.remove("optional");
515                }
516            }
517        } else {
518            unreachable!("Invalid dependency type: {}", item.type_name());
519        }
520    }
521}
522
523impl Default for WorkspaceSource {
524    fn default() -> Self {
525        Self::new()
526    }
527}
528
529/// Overwrite a value while preserving the original formatting
530fn overwrite_value(
531    table: &mut dyn toml_edit::TableLike,
532    key: &str,
533    value: impl Into<toml_edit::Value>,
534) {
535    let mut value = value.into();
536
537    let existing = table.entry(key).or_insert(toml_edit::Item::None);
538    let existing_decor = existing
539        .as_value()
540        .map(|v| v.decor().clone())
541        .unwrap_or_default();
542
543    *value.decor_mut() = existing_decor;
544
545    *existing = toml_edit::Item::Value(value);
546}
547
548fn invalid_type(dep: &str, key: &str, actual: &str, expected: &str) -> anyhow::Error {
549    anyhow::format_err!("Found {actual} for {key} when {expected} was expected for {dep}")
550}
551
552impl std::fmt::Display for Dependency {
553    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
554        if let Some(source) = self.source() {
555            write!(f, "{}@{}", self.name, source)
556        } else {
557            self.toml_key().fmt(f)
558        }
559    }
560}
561
562fn path_field(crate_root: &Path, abs_path: &Path) -> String {
563    let relpath = pathdiff::diff_paths(abs_path, crate_root).expect("both paths are absolute");
564    let relpath = relpath.to_str().unwrap().replace('\\', "/");
565    relpath
566}
567
568/// Primary location of a dependency
569#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
570pub enum Source {
571    /// Dependency from a registry
572    Registry(RegistrySource),
573    /// Dependency from a local path
574    Path(PathSource),
575    /// Dependency from a git repo
576    Git(GitSource),
577    /// Dependency from a workspace
578    Workspace(WorkspaceSource),
579}
580
581impl Source {
582    /// Access the registry source, if present
583    pub fn as_registry(&self) -> Option<&RegistrySource> {
584        match self {
585            Self::Registry(src) => Some(src),
586            _ => None,
587        }
588    }
589
590    /// Access the path source, if present
591    #[allow(dead_code)]
592    pub fn as_path(&self) -> Option<&PathSource> {
593        match self {
594            Self::Path(src) => Some(src),
595            _ => None,
596        }
597    }
598
599    /// Access the git source, if present
600    #[allow(dead_code)]
601    pub fn as_git(&self) -> Option<&GitSource> {
602        match self {
603            Self::Git(src) => Some(src),
604            _ => None,
605        }
606    }
607
608    /// Access the workspace source, if present
609    #[allow(dead_code)]
610    pub fn as_workspace(&self) -> Option<&WorkspaceSource> {
611        match self {
612            Self::Workspace(src) => Some(src),
613            _ => None,
614        }
615    }
616}
617
618impl std::fmt::Display for Source {
619    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
620        match self {
621            Self::Registry(src) => src.fmt(f),
622            Self::Path(src) => src.fmt(f),
623            Self::Git(src) => src.fmt(f),
624            Self::Workspace(src) => src.fmt(f),
625        }
626    }
627}
628
629impl<'s> From<&'s Source> for Source {
630    fn from(inner: &'s Source) -> Self {
631        inner.clone()
632    }
633}
634
635impl From<RegistrySource> for Source {
636    fn from(inner: RegistrySource) -> Self {
637        Self::Registry(inner)
638    }
639}
640
641impl From<PathSource> for Source {
642    fn from(inner: PathSource) -> Self {
643        Self::Path(inner)
644    }
645}
646
647impl From<GitSource> for Source {
648    fn from(inner: GitSource) -> Self {
649        Self::Git(inner)
650    }
651}
652
653impl From<WorkspaceSource> for Source {
654    fn from(inner: WorkspaceSource) -> Self {
655        Self::Workspace(inner)
656    }
657}
658
659/// Dependency from a registry
660#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
661#[non_exhaustive]
662pub struct RegistrySource {
663    /// Version requirement
664    pub version: String,
665}
666
667impl RegistrySource {
668    /// Specify dependency by version requirement
669    pub fn new(version: impl AsRef<str>) -> Self {
670        // versions might have semver metadata appended which we do not want to
671        // store in the cargo toml files.  This would cause a warning upon compilation
672        // ("version requirement […] includes semver metadata which will be ignored")
673        let version = version.as_ref().split('+').next().unwrap();
674        Self {
675            version: version.to_owned(),
676        }
677    }
678}
679
680impl std::fmt::Display for RegistrySource {
681    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
682        self.version.fmt(f)
683    }
684}
685
686/// Dependency from a local path
687#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
688#[non_exhaustive]
689pub struct PathSource {
690    /// Local, absolute path
691    pub path: PathBuf,
692    /// Version requirement for when published
693    pub version: Option<String>,
694}
695
696impl PathSource {
697    /// Specify dependency from a path
698    pub fn new(path: impl Into<PathBuf>) -> Self {
699        Self {
700            path: path.into(),
701            version: None,
702        }
703    }
704
705    /// Set an optional version requirement
706    pub fn set_version(mut self, version: impl AsRef<str>) -> Self {
707        // versions might have semver metadata appended which we do not want to
708        // store in the cargo toml files.  This would cause a warning upon compilation
709        // ("version requirement […] includes semver metadata which will be ignored")
710        let version = version.as_ref().split('+').next().unwrap();
711        self.version = Some(version.to_owned());
712        self
713    }
714}
715
716impl std::fmt::Display for PathSource {
717    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
718        self.path.display().fmt(f)
719    }
720}
721
722/// Dependency from a git repo
723#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
724#[non_exhaustive]
725pub struct GitSource {
726    /// Repo URL
727    pub git: String,
728    /// Select specific branch
729    pub branch: Option<String>,
730    /// Select specific tag
731    pub tag: Option<String>,
732    /// Select specific rev
733    pub rev: Option<String>,
734    /// Version requirement for when published
735    pub version: Option<String>,
736}
737
738impl GitSource {
739    /// Specify dependency from a git repo
740    pub fn new(git: impl Into<String>) -> Self {
741        Self {
742            git: git.into(),
743            branch: None,
744            tag: None,
745            rev: None,
746            version: None,
747        }
748    }
749
750    /// Specify an optional branch
751    pub fn set_branch(mut self, branch: impl Into<String>) -> Self {
752        self.branch = Some(branch.into());
753        self.tag = None;
754        self.rev = None;
755        self
756    }
757
758    /// Specify an optional tag
759    pub fn set_tag(mut self, tag: impl Into<String>) -> Self {
760        self.branch = None;
761        self.tag = Some(tag.into());
762        self.rev = None;
763        self
764    }
765
766    /// Specify an optional rev
767    pub fn set_rev(mut self, rev: impl Into<String>) -> Self {
768        self.branch = None;
769        self.tag = None;
770        self.rev = Some(rev.into());
771        self
772    }
773
774    /// Set an optional version requirement
775    pub fn set_version(mut self, version: impl AsRef<str>) -> Self {
776        // versions might have semver metadata appended which we do not want to
777        // store in the cargo toml files.  This would cause a warning upon compilation
778        // ("version requirement […] includes semver metadata which will be ignored")
779        let version = version.as_ref().split('+').next().unwrap();
780        self.version = Some(version.to_owned());
781        self
782    }
783}
784
785impl std::fmt::Display for GitSource {
786    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
787        write!(f, "{}", self.git)?;
788        if let Some(branch) = &self.branch {
789            write!(f, "?branch={branch}")?;
790        } else if let Some(tag) = &self.tag {
791            write!(f, "?tag={tag}")?;
792        } else if let Some(rev) = &self.rev {
793            write!(f, "?rev={rev}")?;
794        }
795        Ok(())
796    }
797}
798
799/// Dependency from a workspace
800#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Copy, Clone)]
801#[non_exhaustive]
802pub struct WorkspaceSource;
803
804impl WorkspaceSource {
805    pub fn new() -> Self {
806        Self
807    }
808}
809
810impl std::fmt::Display for WorkspaceSource {
811    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
812        "workspace".fmt(f)
813    }
814}
815
816#[cfg(test)]
817mod tests {
818    use std::path::Path;
819
820    use super::*;
821
822    #[test]
823    fn to_toml_simple_dep() {
824        let crate_root = dunce::canonicalize(std::env::current_dir().unwrap().join(Path::new("/")))
825            .expect("root exists");
826        let dep = Dependency::new("dep").set_source(RegistrySource::new("1.0"));
827        let key = dep.toml_key();
828        let item = dep.to_toml(&crate_root);
829
830        assert_eq!(key, "dep".to_owned());
831
832        verify_roundtrip(&crate_root, key, &item);
833    }
834
835    #[test]
836    fn to_toml_simple_dep_with_version() {
837        let crate_root = dunce::canonicalize(std::env::current_dir().unwrap().join(Path::new("/")))
838            .expect("root exists");
839        let dep = Dependency::new("dep").set_source(RegistrySource::new("1.0"));
840        let key = dep.toml_key();
841        let item = dep.to_toml(&crate_root);
842
843        assert_eq!(key, "dep".to_owned());
844        assert_eq!(item.as_str(), Some("1.0"));
845
846        verify_roundtrip(&crate_root, key, &item);
847    }
848
849    #[test]
850    fn to_toml_optional_dep() {
851        let crate_root = dunce::canonicalize(std::env::current_dir().unwrap().join(Path::new("/")))
852            .expect("root exists");
853        let dep = Dependency::new("dep")
854            .set_source(RegistrySource::new("1.0"))
855            .set_optional(true);
856        let key = dep.toml_key();
857        let item = dep.to_toml(&crate_root);
858
859        assert_eq!(key, "dep".to_owned());
860        assert!(item.is_inline_table());
861
862        let dep = item.as_inline_table().unwrap();
863        assert_eq!(dep.get("optional").unwrap().as_bool(), Some(true));
864
865        verify_roundtrip(&crate_root, key, &item);
866    }
867
868    #[test]
869    fn to_toml_dep_without_default_features() {
870        let crate_root = dunce::canonicalize(std::env::current_dir().unwrap().join(Path::new("/")))
871            .expect("root exists");
872        let dep = Dependency::new("dep")
873            .set_source(RegistrySource::new("1.0"))
874            .set_default_features(false);
875        let key = dep.toml_key();
876        let item = dep.to_toml(&crate_root);
877
878        assert_eq!(key, "dep".to_owned());
879        assert!(item.is_inline_table());
880
881        let dep = item.as_inline_table().unwrap();
882        assert_eq!(dep.get("default-features").unwrap().as_bool(), Some(false));
883
884        verify_roundtrip(&crate_root, key, &item);
885    }
886
887    #[test]
888    fn to_toml_dep_with_path_source() {
889        let root = dunce::canonicalize(std::env::current_dir().unwrap().join(Path::new("/")))
890            .expect("root exists");
891        let crate_root = root.join("foo");
892        let dep = Dependency::new("dep").set_source(PathSource::new(root.join("bar")));
893        let key = dep.toml_key();
894        let item = dep.to_toml(&crate_root);
895
896        assert_eq!(key, "dep".to_owned());
897        assert!(item.is_inline_table());
898
899        let dep = item.as_inline_table().unwrap();
900        assert_eq!(dep.get("path").unwrap().as_str(), Some("../bar"));
901
902        verify_roundtrip(&crate_root, key, &item);
903    }
904
905    #[test]
906    fn to_toml_dep_with_git_source() {
907        let crate_root = dunce::canonicalize(std::env::current_dir().unwrap().join(Path::new("/")))
908            .expect("root exists");
909        let dep = Dependency::new("dep").set_source(GitSource::new("https://foor/bar.git"));
910        let key = dep.toml_key();
911        let item = dep.to_toml(&crate_root);
912
913        assert_eq!(key, "dep".to_owned());
914        assert!(item.is_inline_table());
915
916        let dep = item.as_inline_table().unwrap();
917        assert_eq!(
918            dep.get("git").unwrap().as_str(),
919            Some("https://foor/bar.git")
920        );
921
922        verify_roundtrip(&crate_root, key, &item);
923    }
924
925    #[test]
926    fn to_toml_renamed_dep() {
927        let crate_root = dunce::canonicalize(std::env::current_dir().unwrap().join(Path::new("/")))
928            .expect("root exists");
929        let dep = Dependency::new("dep")
930            .set_source(RegistrySource::new("1.0"))
931            .set_rename("d");
932        let key = dep.toml_key();
933        let item = dep.to_toml(&crate_root);
934
935        assert_eq!(key, "d".to_owned());
936        assert!(item.is_inline_table());
937
938        let dep = item.as_inline_table().unwrap();
939        assert_eq!(dep.get("package").unwrap().as_str(), Some("dep"));
940
941        verify_roundtrip(&crate_root, key, &item);
942    }
943
944    #[test]
945    fn to_toml_dep_from_alt_registry() {
946        let crate_root = dunce::canonicalize(std::env::current_dir().unwrap().join(Path::new("/")))
947            .expect("root exists");
948        let dep = Dependency::new("dep")
949            .set_source(RegistrySource::new("1.0"))
950            .set_registry("alternative");
951        let key = dep.toml_key();
952        let item = dep.to_toml(&crate_root);
953
954        assert_eq!(key, "dep".to_owned());
955        assert!(item.is_inline_table());
956
957        let dep = item.as_inline_table().unwrap();
958        assert_eq!(dep.get("registry").unwrap().as_str(), Some("alternative"));
959
960        verify_roundtrip(&crate_root, key, &item);
961    }
962
963    #[test]
964    fn to_toml_complex_dep() {
965        let crate_root = dunce::canonicalize(std::env::current_dir().unwrap().join(Path::new("/")))
966            .expect("root exists");
967        let dep = Dependency::new("dep")
968            .set_source(RegistrySource::new("1.0"))
969            .set_default_features(false)
970            .set_rename("d");
971        let key = dep.toml_key();
972        let item = dep.to_toml(&crate_root);
973
974        assert_eq!(key, "d".to_owned());
975        assert!(item.is_inline_table());
976
977        let dep = item.as_inline_table().unwrap();
978        assert_eq!(dep.get("package").unwrap().as_str(), Some("dep"));
979        assert_eq!(dep.get("version").unwrap().as_str(), Some("1.0"));
980        assert_eq!(dep.get("default-features").unwrap().as_bool(), Some(false));
981
982        verify_roundtrip(&crate_root, key, &item);
983    }
984
985    #[test]
986    fn paths_with_forward_slashes_are_left_as_is() {
987        let crate_root = dunce::canonicalize(std::env::current_dir().unwrap().join(Path::new("/")))
988            .expect("root exists");
989        let path = crate_root.join("sibling/crate");
990        let relpath = "sibling/crate";
991        let dep = Dependency::new("dep").set_source(PathSource::new(path));
992        let key = dep.toml_key();
993        let item = dep.to_toml(&crate_root);
994
995        let table = item.as_inline_table().unwrap();
996        let got = table.get("path").unwrap().as_str().unwrap();
997        assert_eq!(got, relpath);
998
999        verify_roundtrip(&crate_root, key, &item);
1000    }
1001
1002    #[test]
1003    #[cfg(windows)]
1004    fn normalise_windows_style_paths() {
1005        let crate_root =
1006            dunce::canonicalize(&std::env::current_dir().unwrap().join(Path::new("/")))
1007                .expect("root exists");
1008        let original = crate_root.join(r"sibling\crate");
1009        let should_be = "sibling/crate";
1010        let dep = Dependency::new("dep").set_source(PathSource::new(original));
1011        let key = dep.toml_key();
1012        let item = dep.to_toml(&crate_root);
1013
1014        let table = item.as_inline_table().unwrap();
1015        let got = table.get("path").unwrap().as_str().unwrap();
1016        assert_eq!(got, should_be);
1017
1018        verify_roundtrip(&crate_root, key, &item);
1019    }
1020
1021    #[track_caller]
1022    fn verify_roundtrip(crate_root: &Path, key: &str, item: &toml_edit::Item) {
1023        let roundtrip = Dependency::from_toml(crate_root, key, item).unwrap();
1024        let round_key = roundtrip.toml_key();
1025        let round_item = roundtrip.to_toml(crate_root);
1026        assert_eq!(key, round_key);
1027        assert_eq!(item.to_string(), round_item.to_string());
1028    }
1029}