cargo/util/toml_mut/
dependency.rs

1//! Information about dependencies in a manifest.
2
3use std::borrow::Cow;
4use std::fmt::{Display, Formatter};
5use std::path::{Path, PathBuf};
6
7use cargo_util_schemas::manifest::PathBaseName;
8use indexmap::IndexSet;
9use itertools::Itertools;
10use toml_edit::KeyMut;
11
12use super::manifest::str_or_1_len_table;
13use crate::CargoResult;
14use crate::GlobalContext;
15use crate::core::SourceId;
16use crate::core::Summary;
17use crate::core::{Features, GitReference};
18use crate::util::toml::lookup_path_base;
19
20/// A dependency handled by Cargo.
21///
22/// `None` means the field will be blank in TOML.
23#[derive(Debug, PartialEq, Eq, Clone)]
24#[non_exhaustive]
25pub struct Dependency {
26    /// The name of the dependency (as it is set in its `Cargo.toml` and known
27    /// to crates.io).
28    pub name: String,
29    /// Whether the dependency is opted-in with a feature flag.
30    pub optional: Option<bool>,
31
32    /// Whether the dependency is marked as public.
33    pub public: Option<bool>,
34
35    /// List of features to add (or None to keep features unchanged).
36    pub features: Option<IndexSet<String>>,
37    /// Whether default features are enabled.
38    pub default_features: Option<bool>,
39    /// List of features inherited from a workspace dependency.
40    pub inherited_features: Option<IndexSet<String>>,
41
42    /// Where the dependency comes from.
43    pub source: Option<Source>,
44    /// Non-default registry.
45    pub registry: Option<String>,
46
47    /// If the dependency is renamed, this is the new name for the dependency
48    /// as a string.  None if it is not renamed.
49    pub rename: Option<String>,
50}
51
52impl Dependency {
53    /// Create a new dependency with a name.
54    pub fn new(name: &str) -> Self {
55        Self {
56            name: name.into(),
57            optional: None,
58            public: None,
59            features: None,
60            default_features: None,
61            inherited_features: None,
62            source: None,
63            registry: None,
64            rename: None,
65        }
66    }
67
68    /// Set dependency to a given version.
69    pub fn set_source(mut self, source: impl Into<Source>) -> Self {
70        self.source = Some(source.into());
71        self
72    }
73
74    /// Remove the existing version requirement.
75    pub fn clear_version(mut self) -> Self {
76        match &mut self.source {
77            Some(Source::Registry(_)) => {
78                self.source = None;
79            }
80            Some(Source::Path(path)) => {
81                path.version = None;
82            }
83            Some(Source::Git(git)) => {
84                git.version = None;
85            }
86            Some(Source::Workspace(_workspace)) => {}
87            None => {}
88        }
89        self
90    }
91
92    /// Set whether the dependency is optional.
93    #[allow(dead_code)]
94    pub fn set_optional(mut self, opt: bool) -> Self {
95        self.optional = Some(opt);
96        self
97    }
98
99    /// Set features as an array of string (does some basic parsing).
100    #[allow(dead_code)]
101    pub fn set_features(mut self, features: IndexSet<String>) -> Self {
102        self.features = Some(features);
103        self
104    }
105
106    /// Set features as an array of string (does some basic parsing).
107    pub fn extend_features(mut self, features: impl IntoIterator<Item = String>) -> Self {
108        self.features
109            .get_or_insert_with(Default::default)
110            .extend(features);
111        self
112    }
113
114    /// Set the value of default-features for the dependency.
115    #[allow(dead_code)]
116    pub fn set_default_features(mut self, default_features: bool) -> Self {
117        self.default_features = Some(default_features);
118        self
119    }
120
121    /// Set the alias for the dependency.
122    pub fn set_rename(mut self, rename: &str) -> Self {
123        self.rename = Some(rename.into());
124        self
125    }
126
127    /// Set the value of registry for the dependency.
128    pub fn set_registry(mut self, registry: impl Into<String>) -> Self {
129        self.registry = Some(registry.into());
130        self
131    }
132
133    /// Set features as an array of string (does some basic parsing).
134    pub fn set_inherited_features(mut self, features: IndexSet<String>) -> Self {
135        self.inherited_features = Some(features);
136        self
137    }
138
139    /// Get the dependency source.
140    pub fn source(&self) -> Option<&Source> {
141        self.source.as_ref()
142    }
143
144    /// Get version of dependency.
145    pub fn version(&self) -> Option<&str> {
146        match self.source()? {
147            Source::Registry(src) => Some(src.version.as_str()),
148            Source::Path(src) => src.version.as_deref(),
149            Source::Git(src) => src.version.as_deref(),
150            Source::Workspace(_) => None,
151        }
152    }
153
154    /// Get registry of the dependency.
155    pub fn registry(&self) -> Option<&str> {
156        self.registry.as_deref()
157    }
158
159    /// Get the alias for the dependency (if any).
160    pub fn rename(&self) -> Option<&str> {
161        self.rename.as_deref()
162    }
163
164    /// Whether default features are activated.
165    pub fn default_features(&self) -> Option<bool> {
166        self.default_features
167    }
168
169    /// Get whether the dep is optional.
170    pub fn optional(&self) -> Option<bool> {
171        self.optional
172    }
173
174    /// Get whether the dep is public.
175    pub fn public(&self) -> Option<bool> {
176        self.public
177    }
178
179    /// Get the `SourceID` for this dependency.
180    pub fn source_id(&self, gctx: &GlobalContext) -> CargoResult<MaybeWorkspace<SourceId>> {
181        match &self.source.as_ref() {
182            Some(Source::Registry(_)) | None => {
183                if let Some(r) = self.registry() {
184                    let source_id = SourceId::alt_registry(gctx, r)?;
185                    Ok(MaybeWorkspace::Other(source_id))
186                } else {
187                    let source_id = SourceId::crates_io(gctx)?;
188                    Ok(MaybeWorkspace::Other(source_id))
189                }
190            }
191            Some(Source::Path(source)) => Ok(MaybeWorkspace::Other(source.source_id()?)),
192            Some(Source::Git(source)) => Ok(MaybeWorkspace::Other(source.source_id()?)),
193            Some(Source::Workspace(workspace)) => Ok(MaybeWorkspace::Workspace(workspace.clone())),
194        }
195    }
196
197    /// Query to find this dependency.
198    pub fn query(
199        &self,
200        gctx: &GlobalContext,
201    ) -> CargoResult<MaybeWorkspace<crate::core::dependency::Dependency>> {
202        let source_id = self.source_id(gctx)?;
203        match source_id {
204            MaybeWorkspace::Workspace(workspace) => Ok(MaybeWorkspace::Workspace(workspace)),
205            MaybeWorkspace::Other(source_id) => Ok(MaybeWorkspace::Other(
206                crate::core::dependency::Dependency::parse(
207                    self.name.as_str(),
208                    self.version(),
209                    source_id,
210                )?,
211            )),
212        }
213    }
214}
215
216/// Either a workspace or another type.
217pub enum MaybeWorkspace<T> {
218    Workspace(WorkspaceSource),
219    Other(T),
220}
221
222impl Dependency {
223    /// Create a dependency from a TOML table entry.
224    pub fn from_toml(
225        gctx: &GlobalContext,
226        workspace_root: &Path,
227        crate_root: &Path,
228        unstable_features: &Features,
229        key: &str,
230        item: &toml_edit::Item,
231    ) -> CargoResult<Self> {
232        if let Some(version) = item.as_str() {
233            let dep = Self::new(key).set_source(RegistrySource::new(version));
234            Ok(dep)
235        } else if let Some(table) = item.as_table_like() {
236            let (name, rename) = if let Some(value) = table.get("package") {
237                (
238                    value
239                        .as_str()
240                        .ok_or_else(|| invalid_type(key, "package", value.type_name(), "string"))?
241                        .to_owned(),
242                    Some(key.to_owned()),
243                )
244            } else {
245                (key.to_owned(), None)
246            };
247
248            let source: Source = if let Some(git) = table.get("git") {
249                let mut src = GitSource::new(
250                    git.as_str()
251                        .ok_or_else(|| invalid_type(key, "git", git.type_name(), "string"))?,
252                );
253                if let Some(value) = table.get("branch") {
254                    src =
255                        src.set_branch(value.as_str().ok_or_else(|| {
256                            invalid_type(key, "branch", value.type_name(), "string")
257                        })?);
258                }
259                if let Some(value) = table.get("tag") {
260                    src =
261                        src.set_tag(value.as_str().ok_or_else(|| {
262                            invalid_type(key, "tag", value.type_name(), "string")
263                        })?);
264                }
265                if let Some(value) = table.get("rev") {
266                    src =
267                        src.set_rev(value.as_str().ok_or_else(|| {
268                            invalid_type(key, "rev", value.type_name(), "string")
269                        })?);
270                }
271                if let Some(value) = table.get("version") {
272                    src = src.set_version(value.as_str().ok_or_else(|| {
273                        invalid_type(key, "version", value.type_name(), "string")
274                    })?);
275                }
276                src.into()
277            } else if let Some(path) = table.get("path") {
278                let base = table
279                    .get("base")
280                    .map(|base| {
281                        base.as_str()
282                            .ok_or_else(|| invalid_type(key, "base", base.type_name(), "string"))
283                            .map(|s| s.to_owned())
284                    })
285                    .transpose()?;
286                let relative_to = if let Some(base) = &base {
287                    Cow::Owned(lookup_path_base(
288                        &PathBaseName::new(base.clone())?,
289                        gctx,
290                        &|| Ok(workspace_root),
291                        unstable_features,
292                    )?)
293                } else {
294                    Cow::Borrowed(crate_root)
295                };
296                let path =
297                    relative_to
298                        .join(path.as_str().ok_or_else(|| {
299                            invalid_type(key, "path", path.type_name(), "string")
300                        })?);
301                let mut src = PathSource::new(path);
302                src.base = base;
303                if let Some(value) = table.get("version") {
304                    src = src.set_version(value.as_str().ok_or_else(|| {
305                        invalid_type(key, "version", value.type_name(), "string")
306                    })?);
307                }
308                src.into()
309            } else if let Some(version) = table.get("version") {
310                let src =
311                    RegistrySource::new(version.as_str().ok_or_else(|| {
312                        invalid_type(key, "version", version.type_name(), "string")
313                    })?);
314                src.into()
315            } else if let Some(workspace) = table.get("workspace") {
316                let workspace_bool = workspace
317                    .as_bool()
318                    .ok_or_else(|| invalid_type(key, "workspace", workspace.type_name(), "bool"))?;
319                if !workspace_bool {
320                    anyhow::bail!("`{key}.workspace = false` is unsupported")
321                }
322                let src = WorkspaceSource::new();
323                src.into()
324            } else {
325                let mut msg = format!("unrecognized dependency source for `{key}`");
326                if table.is_empty() {
327                    msg.push_str(
328                        ", expected a local path, Git repository, version, or workspace dependency to be specified",
329                    );
330                }
331                anyhow::bail!(msg);
332            };
333            let registry = if let Some(value) = table.get("registry") {
334                Some(
335                    value
336                        .as_str()
337                        .ok_or_else(|| invalid_type(key, "registry", value.type_name(), "string"))?
338                        .to_owned(),
339                )
340            } else {
341                None
342            };
343
344            let default_features = table.get("default-features").and_then(|v| v.as_bool());
345            if table.contains_key("default_features") {
346                anyhow::bail!(
347                    "Use of `default_features` in `{key}` is unsupported, please switch to `default-features`"
348                );
349            }
350
351            let features = if let Some(value) = table.get("features") {
352                Some(
353                    value
354                        .as_array()
355                        .ok_or_else(|| invalid_type(key, "features", value.type_name(), "array"))?
356                        .iter()
357                        .map(|v| {
358                            v.as_str().map(|s| s.to_owned()).ok_or_else(|| {
359                                invalid_type(key, "features", v.type_name(), "string")
360                            })
361                        })
362                        .collect::<CargoResult<IndexSet<String>>>()?,
363                )
364            } else {
365                None
366            };
367
368            let optional = table.get("optional").and_then(|v| v.as_bool());
369            let public = table.get("public").and_then(|v| v.as_bool());
370
371            let dep = Self {
372                name,
373                optional,
374                public,
375                features,
376                default_features,
377                inherited_features: None,
378                source: Some(source),
379                registry,
380                rename,
381            };
382            Ok(dep)
383        } else {
384            anyhow::bail!("Unrecognized` dependency entry format for `{key}");
385        }
386    }
387
388    /// Get the dependency name as defined in the manifest,
389    /// that is, either the alias (rename field if Some),
390    /// or the official package name (name field).
391    pub fn toml_key(&self) -> &str {
392        self.rename().unwrap_or(&self.name)
393    }
394
395    /// Convert dependency to TOML.
396    ///
397    /// Returns a tuple with the dependency's name and either the version as a
398    /// `String` or the path/git repository as an `InlineTable`.
399    /// (If the dependency is set as `optional` or `default-features` is set to
400    /// `false`, an `InlineTable` is returned in any case.)
401    ///
402    /// # Panic
403    ///
404    /// Panics if the path is relative
405    pub fn to_toml<'a>(
406        &self,
407        gctx: &GlobalContext,
408        workspace_root: &Path,
409        crate_root: &Path,
410        unstable_features: &Features,
411    ) -> CargoResult<toml_edit::Item> {
412        assert!(
413            crate_root.is_absolute(),
414            "Absolute path needed, got: {}",
415            crate_root.display()
416        );
417        let table: toml_edit::Item = match (
418            self.public.unwrap_or(false),
419            self.optional.unwrap_or(false),
420            self.features.as_ref(),
421            self.default_features.unwrap_or(true),
422            self.source.as_ref(),
423            self.registry.as_ref(),
424            self.rename.as_ref(),
425        ) {
426            // Extra short when version flag only
427            (
428                false,
429                false,
430                None,
431                true,
432                Some(Source::Registry(RegistrySource { version: v })),
433                None,
434                None,
435            ) => toml_edit::value(v),
436            (false, false, None, true, Some(Source::Workspace(WorkspaceSource {})), None, None) => {
437                let mut table = toml_edit::InlineTable::default();
438                table.set_dotted(true);
439                table.insert("workspace", true.into());
440                toml_edit::value(toml_edit::Value::InlineTable(table))
441            }
442            // Other cases are represented as an inline table
443            (_, _, _, _, _, _, _) => {
444                let mut table = toml_edit::InlineTable::default();
445
446                match &self.source {
447                    Some(Source::Registry(src)) => {
448                        table.insert("version", src.version.as_str().into());
449                    }
450                    Some(Source::Path(src)) => {
451                        let relpath =
452                            path_field(&src, gctx, workspace_root, crate_root, unstable_features)?;
453                        if let Some(r) = src.version.as_deref() {
454                            table.insert("version", r.into());
455                        }
456                        if let Some(base) = &src.base {
457                            table.insert("base", base.into());
458                        }
459                        table.insert("path", relpath.into());
460                    }
461                    Some(Source::Git(src)) => {
462                        table.insert("git", src.git.as_str().into());
463                        if let Some(branch) = src.branch.as_deref() {
464                            table.insert("branch", branch.into());
465                        }
466                        if let Some(tag) = src.tag.as_deref() {
467                            table.insert("tag", tag.into());
468                        }
469                        if let Some(rev) = src.rev.as_deref() {
470                            table.insert("rev", rev.into());
471                        }
472                        if let Some(r) = src.version.as_deref() {
473                            table.insert("version", r.into());
474                        }
475                    }
476                    Some(Source::Workspace(_)) => {
477                        table.insert("workspace", true.into());
478                    }
479                    None => {}
480                }
481                if table.contains_key("version") {
482                    if let Some(r) = self.registry.as_deref() {
483                        table.insert("registry", r.into());
484                    }
485                }
486
487                if self.rename.is_some() {
488                    table.insert("package", self.name.as_str().into());
489                }
490                if let Some(v) = self.default_features {
491                    table.insert("default-features", v.into());
492                }
493                if let Some(features) = self.features.as_ref() {
494                    let features: toml_edit::Value = features.iter().cloned().collect();
495                    table.insert("features", features);
496                }
497                if let Some(v) = self.optional {
498                    table.insert("optional", v.into());
499                }
500                if let Some(v) = self.public {
501                    table.insert("public", v.into());
502                }
503
504                toml_edit::value(toml_edit::Value::InlineTable(table))
505            }
506        };
507
508        Ok(table)
509    }
510
511    /// Modify existing entry to match this dependency.
512    pub fn update_toml<'k, 'a>(
513        &self,
514        gctx: &GlobalContext,
515        workspace_root: &Path,
516        crate_root: &Path,
517        unstable_features: &Features,
518        key: &mut KeyMut<'k>,
519        item: &mut toml_edit::Item,
520    ) -> CargoResult<()> {
521        if str_or_1_len_table(item) {
522            // Little to preserve
523            let mut new_item = self.to_toml(gctx, workspace_root, crate_root, unstable_features)?;
524            match (&item, &mut new_item) {
525                (toml_edit::Item::Value(old), toml_edit::Item::Value(new)) => {
526                    *new.decor_mut() = old.decor().clone();
527                }
528                (toml_edit::Item::Table(old), toml_edit::Item::Table(new)) => {
529                    *new.decor_mut() = old.decor().clone();
530                }
531                (_, _) => {}
532            }
533            *item = new_item;
534        } else if let Some(table) = item.as_table_like_mut() {
535            match &self.source {
536                Some(Source::Registry(src)) => {
537                    overwrite_value(table, "version", src.version.as_str());
538
539                    for key in ["path", "git", "branch", "tag", "rev", "workspace", "base"] {
540                        table.remove(key);
541                    }
542                }
543                Some(Source::Path(src)) => {
544                    if let Some(base) = &src.base {
545                        overwrite_value(table, "base", base);
546                    } else {
547                        table.remove("base");
548                    }
549                    let relpath =
550                        path_field(&src, gctx, workspace_root, crate_root, unstable_features)?;
551                    overwrite_value(table, "path", relpath);
552                    if let Some(r) = src.version.as_deref() {
553                        overwrite_value(table, "version", r);
554                    } else {
555                        table.remove("version");
556                    }
557
558                    for key in ["git", "branch", "tag", "rev", "workspace"] {
559                        table.remove(key);
560                    }
561                }
562                Some(Source::Git(src)) => {
563                    overwrite_value(table, "git", src.git.as_str());
564                    if let Some(branch) = src.branch.as_deref() {
565                        overwrite_value(table, "branch", branch);
566                    } else {
567                        table.remove("branch");
568                    }
569                    if let Some(tag) = src.tag.as_deref() {
570                        overwrite_value(table, "tag", tag);
571                    } else {
572                        table.remove("tag");
573                    }
574                    if let Some(rev) = src.rev.as_deref() {
575                        overwrite_value(table, "rev", rev);
576                    } else {
577                        table.remove("rev");
578                    }
579                    if let Some(r) = src.version.as_deref() {
580                        overwrite_value(table, "version", r);
581                    } else {
582                        table.remove("version");
583                    }
584
585                    for key in ["path", "workspace", "base"] {
586                        table.remove(key);
587                    }
588                }
589                Some(Source::Workspace(_)) => {
590                    overwrite_value(table, "workspace", true);
591                    table.set_dotted(true);
592                    key.fmt();
593                    for key in [
594                        "version",
595                        "registry",
596                        "registry-index",
597                        "path",
598                        "git",
599                        "branch",
600                        "tag",
601                        "rev",
602                        "package",
603                        "default-features",
604                        "base",
605                    ] {
606                        table.remove(key);
607                    }
608                }
609                None => {}
610            }
611            if table.contains_key("version") {
612                if let Some(r) = self.registry.as_deref() {
613                    overwrite_value(table, "registry", r);
614                } else {
615                    table.remove("registry");
616                }
617            } else {
618                table.remove("registry");
619            }
620
621            if self.rename.is_some() {
622                overwrite_value(table, "package", self.name.as_str());
623            }
624            match self.default_features {
625                Some(v) => {
626                    overwrite_value(table, "default-features", v);
627                }
628                None => {
629                    table.remove("default-features");
630                }
631            }
632            if let Some(new_features) = self.features.as_ref() {
633                let mut features = table
634                    .get("features")
635                    .and_then(|i| i.as_value())
636                    .and_then(|v| v.as_array())
637                    .and_then(|a| {
638                        a.iter()
639                            .map(|v| v.as_str())
640                            .collect::<Option<IndexSet<_>>>()
641                    })
642                    .unwrap_or_default();
643                let is_already_sorted = features.iter().is_sorted();
644                features.extend(new_features.iter().map(|s| s.as_str()));
645                let features = if is_already_sorted {
646                    features.into_iter().sorted().collect::<toml_edit::Value>()
647                } else {
648                    features.into_iter().collect::<toml_edit::Value>()
649                };
650                table.set_dotted(false);
651                overwrite_value(table, "features", features);
652            } else {
653                table.remove("features");
654            }
655            match self.optional {
656                Some(v) => {
657                    table.set_dotted(false);
658                    overwrite_value(table, "optional", v);
659                }
660                None => {
661                    table.remove("optional");
662                }
663            }
664            match self.public {
665                Some(v) => {
666                    table.set_dotted(false);
667                    overwrite_value(table, "public", v);
668                }
669                None => {
670                    table.remove("public");
671                }
672            }
673        } else {
674            unreachable!("Invalid dependency type: {}", item.type_name());
675        }
676        Ok(())
677    }
678}
679
680fn overwrite_value(
681    table: &mut dyn toml_edit::TableLike,
682    key: &str,
683    value: impl Into<toml_edit::Value>,
684) {
685    let mut value = value.into();
686    let existing = table.entry(key).or_insert_with(|| Default::default());
687    if let Some(existing_value) = existing.as_value() {
688        *value.decor_mut() = existing_value.decor().clone();
689    }
690    *existing = toml_edit::Item::Value(value);
691}
692
693fn invalid_type(dep: &str, key: &str, actual: &str, expected: &str) -> anyhow::Error {
694    anyhow::format_err!("Found {actual} for {key} when {expected} was expected for {dep}")
695}
696
697impl std::fmt::Display for Dependency {
698    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
699        if let Some(source) = self.source() {
700            write!(f, "{}@{}", self.name, source)
701        } else {
702            self.toml_key().fmt(f)
703        }
704    }
705}
706
707impl<'s> From<&'s Summary> for Dependency {
708    fn from(other: &'s Summary) -> Self {
709        let source: Source = if let Some(path) = other.source_id().local_path() {
710            PathSource::new(path)
711                .set_version(other.version().to_string())
712                .into()
713        } else if let Some(git_ref) = other.source_id().git_reference() {
714            let mut src = GitSource::new(other.source_id().url().to_string())
715                .set_version(other.version().to_string());
716            match git_ref {
717                GitReference::Branch(branch) => src = src.set_branch(branch),
718                GitReference::Tag(tag) => src = src.set_tag(tag),
719                GitReference::Rev(rev) => src = src.set_rev(rev),
720                GitReference::DefaultBranch => {}
721            }
722            src.into()
723        } else {
724            RegistrySource::new(other.version().to_string()).into()
725        };
726        Dependency::new(other.name().as_str()).set_source(source)
727    }
728}
729
730impl From<Summary> for Dependency {
731    fn from(other: Summary) -> Self {
732        (&other).into()
733    }
734}
735
736fn path_field<'a>(
737    source: &PathSource,
738    gctx: &GlobalContext,
739    workspace_root: &Path,
740    crate_root: &Path,
741    unstable_features: &Features,
742) -> CargoResult<String> {
743    let relative_to = if let Some(base) = &source.base {
744        Cow::Owned(lookup_path_base(
745            &PathBaseName::new(base.clone())?,
746            gctx,
747            &|| Ok(workspace_root),
748            unstable_features,
749        )?)
750    } else {
751        Cow::Borrowed(crate_root)
752    };
753    let relpath = pathdiff::diff_paths(&source.path, relative_to)
754        .expect("PathSource::path and workspace path must be absolute");
755    let relpath = relpath.to_str().unwrap().replace('\\', "/");
756    Ok(relpath)
757}
758
759/// Primary location of a dependency.
760#[derive(Debug, Hash, PartialEq, Eq, Clone)]
761pub enum Source {
762    /// Dependency from a registry.
763    Registry(RegistrySource),
764    /// Dependency from a local path.
765    Path(PathSource),
766    /// Dependency from a git repo.
767    Git(GitSource),
768    /// Dependency from a workspace.
769    Workspace(WorkspaceSource),
770}
771
772impl Source {
773    /// Access the registry source, if present.
774    pub fn as_registry(&self) -> Option<&RegistrySource> {
775        match self {
776            Self::Registry(src) => Some(src),
777            _ => None,
778        }
779    }
780
781    /// Access the path source, if present.
782    #[allow(dead_code)]
783    pub fn as_path(&self) -> Option<&PathSource> {
784        match self {
785            Self::Path(src) => Some(src),
786            _ => None,
787        }
788    }
789
790    /// Access the git source, if present.
791    #[allow(dead_code)]
792    pub fn as_git(&self) -> Option<&GitSource> {
793        match self {
794            Self::Git(src) => Some(src),
795            _ => None,
796        }
797    }
798
799    /// Access the workspace source, if present.
800    #[allow(dead_code)]
801    pub fn as_workspace(&self) -> Option<&WorkspaceSource> {
802        match self {
803            Self::Workspace(src) => Some(src),
804            _ => None,
805        }
806    }
807}
808
809impl std::fmt::Display for Source {
810    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
811        match self {
812            Self::Registry(src) => src.fmt(f),
813            Self::Path(src) => src.fmt(f),
814            Self::Git(src) => src.fmt(f),
815            Self::Workspace(src) => src.fmt(f),
816        }
817    }
818}
819
820impl<'s> From<&'s Source> for Source {
821    fn from(inner: &'s Source) -> Self {
822        inner.clone()
823    }
824}
825
826impl From<RegistrySource> for Source {
827    fn from(inner: RegistrySource) -> Self {
828        Self::Registry(inner)
829    }
830}
831
832impl From<PathSource> for Source {
833    fn from(inner: PathSource) -> Self {
834        Self::Path(inner)
835    }
836}
837
838impl From<GitSource> for Source {
839    fn from(inner: GitSource) -> Self {
840        Self::Git(inner)
841    }
842}
843
844impl From<WorkspaceSource> for Source {
845    fn from(inner: WorkspaceSource) -> Self {
846        Self::Workspace(inner)
847    }
848}
849
850/// Dependency from a registry.
851#[derive(Debug, Hash, PartialEq, Eq, Clone)]
852#[non_exhaustive]
853pub struct RegistrySource {
854    /// Version requirement.
855    pub version: String,
856}
857
858impl RegistrySource {
859    /// Specify dependency by version requirement.
860    pub fn new(version: impl AsRef<str>) -> Self {
861        // versions might have semver metadata appended which we do not want to
862        // store in the cargo toml files.  This would cause a warning upon compilation
863        // ("version requirement […] includes semver metadata which will be ignored")
864        let version = version.as_ref().split('+').next().unwrap();
865        Self {
866            version: version.to_owned(),
867        }
868    }
869}
870
871impl std::fmt::Display for RegistrySource {
872    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
873        self.version.fmt(f)
874    }
875}
876
877/// Dependency from a local path.
878#[derive(Debug, Hash, PartialEq, Eq, Clone)]
879#[non_exhaustive]
880pub struct PathSource {
881    /// Local, absolute path.
882    pub path: PathBuf,
883    /// The path base, if using one.
884    pub base: Option<String>,
885    /// Version requirement for when published.
886    pub version: Option<String>,
887}
888
889impl PathSource {
890    /// Specify dependency from a path.
891    pub fn new(path: impl Into<PathBuf>) -> Self {
892        Self {
893            path: path.into(),
894            base: None,
895            version: None,
896        }
897    }
898
899    /// Set an optional version requirement.
900    pub fn set_version(mut self, version: impl AsRef<str>) -> Self {
901        // versions might have semver metadata appended which we do not want to
902        // store in the cargo toml files.  This would cause a warning upon compilation
903        // ("version requirement […] includes semver metadata which will be ignored")
904        let version = version.as_ref().split('+').next().unwrap();
905        self.version = Some(version.to_owned());
906        self
907    }
908
909    /// Get the `SourceID` for this dependency.
910    pub fn source_id(&self) -> CargoResult<SourceId> {
911        SourceId::for_path(&self.path)
912    }
913}
914
915impl std::fmt::Display for PathSource {
916    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
917        self.path.display().fmt(f)
918    }
919}
920
921/// Dependency from a git repo.
922#[derive(Debug, Hash, PartialEq, Eq, Clone)]
923#[non_exhaustive]
924pub struct GitSource {
925    /// Repository URL.
926    pub git: String,
927    /// Select specific branch.
928    pub branch: Option<String>,
929    /// Select specific tag.
930    pub tag: Option<String>,
931    /// Select specific rev.
932    pub rev: Option<String>,
933    /// Version requirement for when published.
934    pub version: Option<String>,
935}
936
937impl GitSource {
938    /// Specify dependency from a git repo.
939    pub fn new(git: impl Into<String>) -> Self {
940        Self {
941            git: git.into(),
942            branch: None,
943            tag: None,
944            rev: None,
945            version: None,
946        }
947    }
948
949    /// Specify an optional branch.
950    pub fn set_branch(mut self, branch: impl Into<String>) -> Self {
951        self.branch = Some(branch.into());
952        self.tag = None;
953        self.rev = None;
954        self
955    }
956
957    /// Specify an optional tag.
958    pub fn set_tag(mut self, tag: impl Into<String>) -> Self {
959        self.branch = None;
960        self.tag = Some(tag.into());
961        self.rev = None;
962        self
963    }
964
965    /// Specify an optional rev.
966    pub fn set_rev(mut self, rev: impl Into<String>) -> Self {
967        self.branch = None;
968        self.tag = None;
969        self.rev = Some(rev.into());
970        self
971    }
972
973    /// Get the `SourceID` for this dependency.
974    pub fn source_id(&self) -> CargoResult<SourceId> {
975        let git_url = self.git.parse::<url::Url>()?;
976        let git_ref = self.git_ref();
977        SourceId::for_git(&git_url, git_ref)
978    }
979
980    fn git_ref(&self) -> GitReference {
981        match (
982            self.branch.as_deref(),
983            self.tag.as_deref(),
984            self.rev.as_deref(),
985        ) {
986            (Some(branch), _, _) => GitReference::Branch(branch.to_owned()),
987            (_, Some(tag), _) => GitReference::Tag(tag.to_owned()),
988            (_, _, Some(rev)) => GitReference::Rev(rev.to_owned()),
989            _ => GitReference::DefaultBranch,
990        }
991    }
992
993    /// Set an optional version requirement.
994    pub fn set_version(mut self, version: impl AsRef<str>) -> Self {
995        // versions might have semver metadata appended which we do not want to
996        // store in the cargo toml files.  This would cause a warning upon compilation
997        // ("version requirement […] includes semver metadata which will be ignored")
998        let version = version.as_ref().split('+').next().unwrap();
999        self.version = Some(version.to_owned());
1000        self
1001    }
1002}
1003
1004impl std::fmt::Display for GitSource {
1005    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1006        let git_ref = self.git_ref();
1007        if let Some(pretty_ref) = git_ref.pretty_ref(true) {
1008            write!(f, "{}?{}", self.git, pretty_ref)
1009        } else {
1010            write!(f, "{}", self.git)
1011        }
1012    }
1013}
1014
1015/// Dependency from a workspace.
1016#[derive(Debug, Hash, PartialEq, Eq, Clone)]
1017#[non_exhaustive]
1018pub struct WorkspaceSource;
1019
1020impl WorkspaceSource {
1021    pub fn new() -> Self {
1022        Self
1023    }
1024}
1025
1026impl Display for WorkspaceSource {
1027    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1028        "workspace".fmt(f)
1029    }
1030}
1031
1032#[cfg(test)]
1033mod tests {
1034    use crate::util::toml_mut::manifest::LocalManifest;
1035    use cargo_util::paths;
1036
1037    use super::*;
1038
1039    #[test]
1040    fn to_toml_simple_dep() {
1041        let crate_root =
1042            paths::normalize_path(&std::env::current_dir().unwrap().join(Path::new("/")));
1043        let dep = Dependency::new("dep").set_source(RegistrySource::new("1.0"));
1044        let key = dep.toml_key();
1045        let gctx = GlobalContext::default().unwrap();
1046        let item = dep
1047            .to_toml(&gctx, &crate_root, &crate_root, &Features::default())
1048            .unwrap();
1049
1050        assert_eq!(key, "dep".to_owned());
1051
1052        verify_roundtrip(&crate_root, &gctx, key, &item);
1053    }
1054
1055    #[test]
1056    fn to_toml_simple_dep_with_version() {
1057        let crate_root =
1058            paths::normalize_path(&std::env::current_dir().unwrap().join(Path::new("/")));
1059        let dep = Dependency::new("dep").set_source(RegistrySource::new("1.0"));
1060        let key = dep.toml_key();
1061        let gctx = GlobalContext::default().unwrap();
1062        let item = dep
1063            .to_toml(&gctx, &crate_root, &crate_root, &Features::default())
1064            .unwrap();
1065
1066        assert_eq!(key, "dep".to_owned());
1067        assert_eq!(item.as_str(), Some("1.0"));
1068
1069        verify_roundtrip(&crate_root, &gctx, key, &item);
1070    }
1071
1072    #[test]
1073    fn to_toml_optional_dep() {
1074        let crate_root =
1075            paths::normalize_path(&std::env::current_dir().unwrap().join(Path::new("/")));
1076        let dep = Dependency::new("dep")
1077            .set_source(RegistrySource::new("1.0"))
1078            .set_optional(true);
1079        let key = dep.toml_key();
1080        let gctx = GlobalContext::default().unwrap();
1081        let item = dep
1082            .to_toml(&gctx, &crate_root, &crate_root, &Features::default())
1083            .unwrap();
1084
1085        assert_eq!(key, "dep".to_owned());
1086        assert!(item.is_inline_table());
1087
1088        let dep = item.as_inline_table().unwrap();
1089        assert_eq!(dep.get("optional").unwrap().as_bool(), Some(true));
1090
1091        verify_roundtrip(&crate_root, &gctx, key, &item);
1092    }
1093
1094    #[test]
1095    fn to_toml_dep_without_default_features() {
1096        let crate_root =
1097            paths::normalize_path(&std::env::current_dir().unwrap().join(Path::new("/")));
1098        let dep = Dependency::new("dep")
1099            .set_source(RegistrySource::new("1.0"))
1100            .set_default_features(false);
1101        let key = dep.toml_key();
1102        let gctx = GlobalContext::default().unwrap();
1103        let item = dep
1104            .to_toml(&gctx, &crate_root, &crate_root, &Features::default())
1105            .unwrap();
1106
1107        assert_eq!(key, "dep".to_owned());
1108        assert!(item.is_inline_table());
1109
1110        let dep = item.as_inline_table().unwrap();
1111        assert_eq!(dep.get("default-features").unwrap().as_bool(), Some(false));
1112
1113        verify_roundtrip(&crate_root, &gctx, key, &item);
1114    }
1115
1116    #[test]
1117    fn to_toml_dep_with_path_source() {
1118        let root = paths::normalize_path(&std::env::current_dir().unwrap().join(Path::new("/")));
1119        let crate_root = root.join("foo");
1120        let dep = Dependency::new("dep").set_source(PathSource::new(root.join("bar")));
1121        let key = dep.toml_key();
1122        let gctx = GlobalContext::default().unwrap();
1123        let item = dep
1124            .to_toml(&gctx, &crate_root, &crate_root, &Features::default())
1125            .unwrap();
1126
1127        assert_eq!(key, "dep".to_owned());
1128        assert!(item.is_inline_table());
1129
1130        let dep = item.as_inline_table().unwrap();
1131        assert_eq!(dep.get("path").unwrap().as_str(), Some("../bar"));
1132
1133        verify_roundtrip(&crate_root, &gctx, key, &item);
1134    }
1135
1136    #[test]
1137    fn to_toml_dep_with_git_source() {
1138        let crate_root =
1139            paths::normalize_path(&std::env::current_dir().unwrap().join(Path::new("/")));
1140        let dep = Dependency::new("dep").set_source(GitSource::new("https://foor/bar.git"));
1141        let key = dep.toml_key();
1142        let gctx = GlobalContext::default().unwrap();
1143        let item = dep
1144            .to_toml(&gctx, &crate_root, &crate_root, &Features::default())
1145            .unwrap();
1146
1147        assert_eq!(key, "dep".to_owned());
1148        assert!(item.is_inline_table());
1149
1150        let dep = item.as_inline_table().unwrap();
1151        assert_eq!(
1152            dep.get("git").unwrap().as_str(),
1153            Some("https://foor/bar.git")
1154        );
1155
1156        verify_roundtrip(&crate_root, &gctx, key, &item);
1157    }
1158
1159    #[test]
1160    fn to_toml_renamed_dep() {
1161        let crate_root =
1162            paths::normalize_path(&std::env::current_dir().unwrap().join(Path::new("/")));
1163        let dep = Dependency::new("dep")
1164            .set_source(RegistrySource::new("1.0"))
1165            .set_rename("d");
1166        let key = dep.toml_key();
1167        let gctx = GlobalContext::default().unwrap();
1168        let item = dep
1169            .to_toml(&gctx, &crate_root, &crate_root, &Features::default())
1170            .unwrap();
1171
1172        assert_eq!(key, "d".to_owned());
1173        assert!(item.is_inline_table());
1174
1175        let dep = item.as_inline_table().unwrap();
1176        assert_eq!(dep.get("package").unwrap().as_str(), Some("dep"));
1177
1178        verify_roundtrip(&crate_root, &gctx, key, &item);
1179    }
1180
1181    #[test]
1182    fn to_toml_dep_from_alt_registry() {
1183        let crate_root =
1184            paths::normalize_path(&std::env::current_dir().unwrap().join(Path::new("/")));
1185        let dep = Dependency::new("dep")
1186            .set_source(RegistrySource::new("1.0"))
1187            .set_registry("alternative");
1188        let key = dep.toml_key();
1189        let gctx = GlobalContext::default().unwrap();
1190        let item = dep
1191            .to_toml(&gctx, &crate_root, &crate_root, &Features::default())
1192            .unwrap();
1193
1194        assert_eq!(key, "dep".to_owned());
1195        assert!(item.is_inline_table());
1196
1197        let dep = item.as_inline_table().unwrap();
1198        assert_eq!(dep.get("registry").unwrap().as_str(), Some("alternative"));
1199
1200        verify_roundtrip(&crate_root, &gctx, key, &item);
1201    }
1202
1203    #[test]
1204    fn to_toml_complex_dep() {
1205        let crate_root =
1206            paths::normalize_path(&std::env::current_dir().unwrap().join(Path::new("/")));
1207        let dep = Dependency::new("dep")
1208            .set_source(RegistrySource::new("1.0"))
1209            .set_default_features(false)
1210            .set_rename("d");
1211        let key = dep.toml_key();
1212        let gctx = GlobalContext::default().unwrap();
1213        let item = dep
1214            .to_toml(&gctx, &crate_root, &crate_root, &Features::default())
1215            .unwrap();
1216
1217        assert_eq!(key, "d".to_owned());
1218        assert!(item.is_inline_table());
1219
1220        let dep = item.as_inline_table().unwrap();
1221        assert_eq!(dep.get("package").unwrap().as_str(), Some("dep"));
1222        assert_eq!(dep.get("version").unwrap().as_str(), Some("1.0"));
1223        assert_eq!(dep.get("default-features").unwrap().as_bool(), Some(false));
1224
1225        verify_roundtrip(&crate_root, &gctx, key, &item);
1226    }
1227
1228    #[test]
1229    fn paths_with_forward_slashes_are_left_as_is() {
1230        let crate_root =
1231            paths::normalize_path(&std::env::current_dir().unwrap().join(Path::new("/")));
1232        let path = crate_root.join("sibling/crate");
1233        let relpath = "sibling/crate";
1234        let dep = Dependency::new("dep").set_source(PathSource::new(path));
1235        let key = dep.toml_key();
1236        let gctx = GlobalContext::default().unwrap();
1237        let item = dep
1238            .to_toml(&gctx, &crate_root, &crate_root, &Features::default())
1239            .unwrap();
1240
1241        let table = item.as_inline_table().unwrap();
1242        let got = table.get("path").unwrap().as_str().unwrap();
1243        assert_eq!(got, relpath);
1244
1245        verify_roundtrip(&crate_root, &gctx, key, &item);
1246    }
1247
1248    #[test]
1249    fn overwrite_with_workspace_source_fmt_key() {
1250        let crate_root =
1251            paths::normalize_path(&std::env::current_dir().unwrap().join(Path::new("./")));
1252        let toml = "dep = \"1.0\"\n";
1253        let manifest = toml.parse().unwrap();
1254        let mut local = LocalManifest {
1255            path: crate_root.clone(),
1256            manifest,
1257            embedded: None,
1258            raw: toml.to_owned(),
1259        };
1260        assert_eq!(local.manifest.to_string(), toml);
1261        let gctx = GlobalContext::default().unwrap();
1262        for (key, item) in local.data.clone().iter() {
1263            let dep = Dependency::from_toml(
1264                &gctx,
1265                &crate_root,
1266                &crate_root,
1267                &Features::default(),
1268                key,
1269                item,
1270            )
1271            .unwrap();
1272            let dep = dep.set_source(WorkspaceSource::new());
1273            local
1274                .insert_into_table(&[], &dep, &gctx, &crate_root, &Features::default())
1275                .unwrap();
1276            assert_eq!(local.data.to_string(), "dep.workspace = true\n");
1277        }
1278    }
1279
1280    #[test]
1281    #[cfg(windows)]
1282    fn normalise_windows_style_paths() {
1283        let crate_root =
1284            paths::normalize_path(&std::env::current_dir().unwrap().join(Path::new("/")));
1285        let original = crate_root.join(r"sibling\crate");
1286        let should_be = "sibling/crate";
1287        let dep = Dependency::new("dep").set_source(PathSource::new(original));
1288        let key = dep.toml_key();
1289        let gctx = GlobalContext::default().unwrap();
1290        let item = dep
1291            .to_toml(&gctx, &crate_root, &crate_root, &Features::default())
1292            .unwrap();
1293
1294        let table = item.as_inline_table().unwrap();
1295        let got = table.get("path").unwrap().as_str().unwrap();
1296        assert_eq!(got, should_be);
1297
1298        verify_roundtrip(&crate_root, &gctx, key, &item);
1299    }
1300
1301    #[track_caller]
1302    fn verify_roundtrip(
1303        crate_root: &Path,
1304        gctx: &GlobalContext,
1305        key: &str,
1306        item: &toml_edit::Item,
1307    ) {
1308        let roundtrip = Dependency::from_toml(
1309            gctx,
1310            crate_root,
1311            crate_root,
1312            &Features::default(),
1313            key,
1314            item,
1315        )
1316        .unwrap();
1317        let round_key = roundtrip.toml_key();
1318        let round_item = roundtrip
1319            .to_toml(gctx, crate_root, crate_root, &Features::default())
1320            .unwrap();
1321        assert_eq!(key, round_key);
1322        assert_eq!(item.to_string(), round_item.to_string());
1323    }
1324}