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::core::SourceId;
14use crate::core::Summary;
15use crate::core::{Features, GitReference};
16use crate::util::toml::lookup_path_base;
17use crate::CargoResult;
18use crate::GlobalContext;
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!("Use of `default_features` in `{key}` is unsupported, please switch to `default-features`");
347            }
348
349            let features = if let Some(value) = table.get("features") {
350                Some(
351                    value
352                        .as_array()
353                        .ok_or_else(|| invalid_type(key, "features", value.type_name(), "array"))?
354                        .iter()
355                        .map(|v| {
356                            v.as_str().map(|s| s.to_owned()).ok_or_else(|| {
357                                invalid_type(key, "features", v.type_name(), "string")
358                            })
359                        })
360                        .collect::<CargoResult<IndexSet<String>>>()?,
361                )
362            } else {
363                None
364            };
365
366            let optional = table.get("optional").and_then(|v| v.as_bool());
367            let public = table.get("public").and_then(|v| v.as_bool());
368
369            let dep = Self {
370                name,
371                optional,
372                public,
373                features,
374                default_features,
375                inherited_features: None,
376                source: Some(source),
377                registry,
378                rename,
379            };
380            Ok(dep)
381        } else {
382            anyhow::bail!("Unrecognized` dependency entry format for `{key}");
383        }
384    }
385
386    /// Get the dependency name as defined in the manifest,
387    /// that is, either the alias (rename field if Some),
388    /// or the official package name (name field).
389    pub fn toml_key(&self) -> &str {
390        self.rename().unwrap_or(&self.name)
391    }
392
393    /// Convert dependency to TOML.
394    ///
395    /// Returns a tuple with the dependency's name and either the version as a
396    /// `String` or the path/git repository as an `InlineTable`.
397    /// (If the dependency is set as `optional` or `default-features` is set to
398    /// `false`, an `InlineTable` is returned in any case.)
399    ///
400    /// # Panic
401    ///
402    /// Panics if the path is relative
403    pub fn to_toml<'a>(
404        &self,
405        gctx: &GlobalContext,
406        workspace_root: &Path,
407        crate_root: &Path,
408        unstable_features: &Features,
409    ) -> CargoResult<toml_edit::Item> {
410        assert!(
411            crate_root.is_absolute(),
412            "Absolute path needed, got: {}",
413            crate_root.display()
414        );
415        let table: toml_edit::Item = match (
416            self.public.unwrap_or(false),
417            self.optional.unwrap_or(false),
418            self.features.as_ref(),
419            self.default_features.unwrap_or(true),
420            self.source.as_ref(),
421            self.registry.as_ref(),
422            self.rename.as_ref(),
423        ) {
424            // Extra short when version flag only
425            (
426                false,
427                false,
428                None,
429                true,
430                Some(Source::Registry(RegistrySource { version: v })),
431                None,
432                None,
433            ) => toml_edit::value(v),
434            (false, false, None, true, Some(Source::Workspace(WorkspaceSource {})), None, None) => {
435                let mut table = toml_edit::InlineTable::default();
436                table.set_dotted(true);
437                table.insert("workspace", true.into());
438                toml_edit::value(toml_edit::Value::InlineTable(table))
439            }
440            // Other cases are represented as an inline table
441            (_, _, _, _, _, _, _) => {
442                let mut table = toml_edit::InlineTable::default();
443
444                match &self.source {
445                    Some(Source::Registry(src)) => {
446                        table.insert("version", src.version.as_str().into());
447                    }
448                    Some(Source::Path(src)) => {
449                        let relpath =
450                            path_field(&src, gctx, workspace_root, crate_root, unstable_features)?;
451                        if let Some(r) = src.version.as_deref() {
452                            table.insert("version", r.into());
453                        }
454                        if let Some(base) = &src.base {
455                            table.insert("base", base.into());
456                        }
457                        table.insert("path", relpath.into());
458                    }
459                    Some(Source::Git(src)) => {
460                        table.insert("git", src.git.as_str().into());
461                        if let Some(branch) = src.branch.as_deref() {
462                            table.insert("branch", branch.into());
463                        }
464                        if let Some(tag) = src.tag.as_deref() {
465                            table.insert("tag", tag.into());
466                        }
467                        if let Some(rev) = src.rev.as_deref() {
468                            table.insert("rev", rev.into());
469                        }
470                        if let Some(r) = src.version.as_deref() {
471                            table.insert("version", r.into());
472                        }
473                    }
474                    Some(Source::Workspace(_)) => {
475                        table.insert("workspace", true.into());
476                    }
477                    None => {}
478                }
479                if table.contains_key("version") {
480                    if let Some(r) = self.registry.as_deref() {
481                        table.insert("registry", r.into());
482                    }
483                }
484
485                if self.rename.is_some() {
486                    table.insert("package", self.name.as_str().into());
487                }
488                if let Some(v) = self.default_features {
489                    table.insert("default-features", v.into());
490                }
491                if let Some(features) = self.features.as_ref() {
492                    let features: toml_edit::Value = features.iter().cloned().collect();
493                    table.insert("features", features);
494                }
495                if let Some(v) = self.optional {
496                    table.insert("optional", v.into());
497                }
498                if let Some(v) = self.public {
499                    table.insert("public", v.into());
500                }
501
502                toml_edit::value(toml_edit::Value::InlineTable(table))
503            }
504        };
505
506        Ok(table)
507    }
508
509    /// Modify existing entry to match this dependency.
510    pub fn update_toml<'k, 'a>(
511        &self,
512        gctx: &GlobalContext,
513        workspace_root: &Path,
514        crate_root: &Path,
515        unstable_features: &Features,
516        key: &mut KeyMut<'k>,
517        item: &mut toml_edit::Item,
518    ) -> CargoResult<()> {
519        if str_or_1_len_table(item) {
520            // Little to preserve
521            let mut new_item = self.to_toml(gctx, workspace_root, crate_root, unstable_features)?;
522            match (&item, &mut new_item) {
523                (toml_edit::Item::Value(old), toml_edit::Item::Value(new)) => {
524                    *new.decor_mut() = old.decor().clone();
525                }
526                (toml_edit::Item::Table(old), toml_edit::Item::Table(new)) => {
527                    *new.decor_mut() = old.decor().clone();
528                }
529                (_, _) => {}
530            }
531            *item = new_item;
532        } else if let Some(table) = item.as_table_like_mut() {
533            match &self.source {
534                Some(Source::Registry(src)) => {
535                    overwrite_value(table, "version", src.version.as_str());
536
537                    for key in ["path", "git", "branch", "tag", "rev", "workspace", "base"] {
538                        table.remove(key);
539                    }
540                }
541                Some(Source::Path(src)) => {
542                    if let Some(base) = &src.base {
543                        overwrite_value(table, "base", base);
544                    } else {
545                        table.remove("base");
546                    }
547                    let relpath =
548                        path_field(&src, gctx, workspace_root, crate_root, unstable_features)?;
549                    overwrite_value(table, "path", relpath);
550                    if let Some(r) = src.version.as_deref() {
551                        overwrite_value(table, "version", r);
552                    } else {
553                        table.remove("version");
554                    }
555
556                    for key in ["git", "branch", "tag", "rev", "workspace"] {
557                        table.remove(key);
558                    }
559                }
560                Some(Source::Git(src)) => {
561                    overwrite_value(table, "git", src.git.as_str());
562                    if let Some(branch) = src.branch.as_deref() {
563                        overwrite_value(table, "branch", branch);
564                    } else {
565                        table.remove("branch");
566                    }
567                    if let Some(tag) = src.tag.as_deref() {
568                        overwrite_value(table, "tag", tag);
569                    } else {
570                        table.remove("tag");
571                    }
572                    if let Some(rev) = src.rev.as_deref() {
573                        overwrite_value(table, "rev", rev);
574                    } else {
575                        table.remove("rev");
576                    }
577                    if let Some(r) = src.version.as_deref() {
578                        overwrite_value(table, "version", r);
579                    } else {
580                        table.remove("version");
581                    }
582
583                    for key in ["path", "workspace", "base"] {
584                        table.remove(key);
585                    }
586                }
587                Some(Source::Workspace(_)) => {
588                    overwrite_value(table, "workspace", true);
589                    table.set_dotted(true);
590                    key.fmt();
591                    for key in [
592                        "version",
593                        "registry",
594                        "registry-index",
595                        "path",
596                        "git",
597                        "branch",
598                        "tag",
599                        "rev",
600                        "package",
601                        "default-features",
602                        "base",
603                    ] {
604                        table.remove(key);
605                    }
606                }
607                None => {}
608            }
609            if table.contains_key("version") {
610                if let Some(r) = self.registry.as_deref() {
611                    overwrite_value(table, "registry", r);
612                } else {
613                    table.remove("registry");
614                }
615            } else {
616                table.remove("registry");
617            }
618
619            if self.rename.is_some() {
620                overwrite_value(table, "package", self.name.as_str());
621            }
622            match self.default_features {
623                Some(v) => {
624                    overwrite_value(table, "default-features", v);
625                }
626                None => {
627                    table.remove("default-features");
628                }
629            }
630            if let Some(new_features) = self.features.as_ref() {
631                let mut features = table
632                    .get("features")
633                    .and_then(|i| i.as_value())
634                    .and_then(|v| v.as_array())
635                    .and_then(|a| {
636                        a.iter()
637                            .map(|v| v.as_str())
638                            .collect::<Option<IndexSet<_>>>()
639                    })
640                    .unwrap_or_default();
641                let is_already_sorted = features.iter().is_sorted();
642                features.extend(new_features.iter().map(|s| s.as_str()));
643                let features = if is_already_sorted {
644                    features.into_iter().sorted().collect::<toml_edit::Value>()
645                } else {
646                    features.into_iter().collect::<toml_edit::Value>()
647                };
648                table.set_dotted(false);
649                overwrite_value(table, "features", features);
650            } else {
651                table.remove("features");
652            }
653            match self.optional {
654                Some(v) => {
655                    table.set_dotted(false);
656                    overwrite_value(table, "optional", v);
657                }
658                None => {
659                    table.remove("optional");
660                }
661            }
662            match self.public {
663                Some(v) => {
664                    table.set_dotted(false);
665                    overwrite_value(table, "public", v);
666                }
667                None => {
668                    table.remove("public");
669                }
670            }
671        } else {
672            unreachable!("Invalid dependency type: {}", item.type_name());
673        }
674        Ok(())
675    }
676}
677
678fn overwrite_value(
679    table: &mut dyn toml_edit::TableLike,
680    key: &str,
681    value: impl Into<toml_edit::Value>,
682) {
683    let mut value = value.into();
684    let existing = table.entry(key).or_insert_with(|| Default::default());
685    if let Some(existing_value) = existing.as_value() {
686        *value.decor_mut() = existing_value.decor().clone();
687    }
688    *existing = toml_edit::Item::Value(value);
689}
690
691fn invalid_type(dep: &str, key: &str, actual: &str, expected: &str) -> anyhow::Error {
692    anyhow::format_err!("Found {actual} for {key} when {expected} was expected for {dep}")
693}
694
695impl std::fmt::Display for Dependency {
696    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
697        if let Some(source) = self.source() {
698            write!(f, "{}@{}", self.name, source)
699        } else {
700            self.toml_key().fmt(f)
701        }
702    }
703}
704
705impl<'s> From<&'s Summary> for Dependency {
706    fn from(other: &'s Summary) -> Self {
707        let source: Source = if let Some(path) = other.source_id().local_path() {
708            PathSource::new(path)
709                .set_version(other.version().to_string())
710                .into()
711        } else if let Some(git_ref) = other.source_id().git_reference() {
712            let mut src = GitSource::new(other.source_id().url().to_string())
713                .set_version(other.version().to_string());
714            match git_ref {
715                GitReference::Branch(branch) => src = src.set_branch(branch),
716                GitReference::Tag(tag) => src = src.set_tag(tag),
717                GitReference::Rev(rev) => src = src.set_rev(rev),
718                GitReference::DefaultBranch => {}
719            }
720            src.into()
721        } else {
722            RegistrySource::new(other.version().to_string()).into()
723        };
724        Dependency::new(other.name().as_str()).set_source(source)
725    }
726}
727
728impl From<Summary> for Dependency {
729    fn from(other: Summary) -> Self {
730        (&other).into()
731    }
732}
733
734fn path_field<'a>(
735    source: &PathSource,
736    gctx: &GlobalContext,
737    workspace_root: &Path,
738    crate_root: &Path,
739    unstable_features: &Features,
740) -> CargoResult<String> {
741    let relative_to = if let Some(base) = &source.base {
742        Cow::Owned(lookup_path_base(
743            &PathBaseName::new(base.clone())?,
744            gctx,
745            &|| Ok(workspace_root),
746            unstable_features,
747        )?)
748    } else {
749        Cow::Borrowed(crate_root)
750    };
751    let relpath = pathdiff::diff_paths(&source.path, relative_to)
752        .expect("PathSource::path and workspace path must be absolute");
753    let relpath = relpath.to_str().unwrap().replace('\\', "/");
754    Ok(relpath)
755}
756
757/// Primary location of a dependency.
758#[derive(Debug, Hash, PartialEq, Eq, Clone)]
759pub enum Source {
760    /// Dependency from a registry.
761    Registry(RegistrySource),
762    /// Dependency from a local path.
763    Path(PathSource),
764    /// Dependency from a git repo.
765    Git(GitSource),
766    /// Dependency from a workspace.
767    Workspace(WorkspaceSource),
768}
769
770impl Source {
771    /// Access the registry source, if present.
772    pub fn as_registry(&self) -> Option<&RegistrySource> {
773        match self {
774            Self::Registry(src) => Some(src),
775            _ => None,
776        }
777    }
778
779    /// Access the path source, if present.
780    #[allow(dead_code)]
781    pub fn as_path(&self) -> Option<&PathSource> {
782        match self {
783            Self::Path(src) => Some(src),
784            _ => None,
785        }
786    }
787
788    /// Access the git source, if present.
789    #[allow(dead_code)]
790    pub fn as_git(&self) -> Option<&GitSource> {
791        match self {
792            Self::Git(src) => Some(src),
793            _ => None,
794        }
795    }
796
797    /// Access the workspace source, if present.
798    #[allow(dead_code)]
799    pub fn as_workspace(&self) -> Option<&WorkspaceSource> {
800        match self {
801            Self::Workspace(src) => Some(src),
802            _ => None,
803        }
804    }
805}
806
807impl std::fmt::Display for Source {
808    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
809        match self {
810            Self::Registry(src) => src.fmt(f),
811            Self::Path(src) => src.fmt(f),
812            Self::Git(src) => src.fmt(f),
813            Self::Workspace(src) => src.fmt(f),
814        }
815    }
816}
817
818impl<'s> From<&'s Source> for Source {
819    fn from(inner: &'s Source) -> Self {
820        inner.clone()
821    }
822}
823
824impl From<RegistrySource> for Source {
825    fn from(inner: RegistrySource) -> Self {
826        Self::Registry(inner)
827    }
828}
829
830impl From<PathSource> for Source {
831    fn from(inner: PathSource) -> Self {
832        Self::Path(inner)
833    }
834}
835
836impl From<GitSource> for Source {
837    fn from(inner: GitSource) -> Self {
838        Self::Git(inner)
839    }
840}
841
842impl From<WorkspaceSource> for Source {
843    fn from(inner: WorkspaceSource) -> Self {
844        Self::Workspace(inner)
845    }
846}
847
848/// Dependency from a registry.
849#[derive(Debug, Hash, PartialEq, Eq, Clone)]
850#[non_exhaustive]
851pub struct RegistrySource {
852    /// Version requirement.
853    pub version: String,
854}
855
856impl RegistrySource {
857    /// Specify dependency by version requirement.
858    pub fn new(version: impl AsRef<str>) -> Self {
859        // versions might have semver metadata appended which we do not want to
860        // store in the cargo toml files.  This would cause a warning upon compilation
861        // ("version requirement […] includes semver metadata which will be ignored")
862        let version = version.as_ref().split('+').next().unwrap();
863        Self {
864            version: version.to_owned(),
865        }
866    }
867}
868
869impl std::fmt::Display for RegistrySource {
870    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
871        self.version.fmt(f)
872    }
873}
874
875/// Dependency from a local path.
876#[derive(Debug, Hash, PartialEq, Eq, Clone)]
877#[non_exhaustive]
878pub struct PathSource {
879    /// Local, absolute path.
880    pub path: PathBuf,
881    /// The path base, if using one.
882    pub base: Option<String>,
883    /// Version requirement for when published.
884    pub version: Option<String>,
885}
886
887impl PathSource {
888    /// Specify dependency from a path.
889    pub fn new(path: impl Into<PathBuf>) -> Self {
890        Self {
891            path: path.into(),
892            base: None,
893            version: None,
894        }
895    }
896
897    /// Set an optional version requirement.
898    pub fn set_version(mut self, version: impl AsRef<str>) -> Self {
899        // versions might have semver metadata appended which we do not want to
900        // store in the cargo toml files.  This would cause a warning upon compilation
901        // ("version requirement […] includes semver metadata which will be ignored")
902        let version = version.as_ref().split('+').next().unwrap();
903        self.version = Some(version.to_owned());
904        self
905    }
906
907    /// Get the `SourceID` for this dependency.
908    pub fn source_id(&self) -> CargoResult<SourceId> {
909        SourceId::for_path(&self.path)
910    }
911}
912
913impl std::fmt::Display for PathSource {
914    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
915        self.path.display().fmt(f)
916    }
917}
918
919/// Dependency from a git repo.
920#[derive(Debug, Hash, PartialEq, Eq, Clone)]
921#[non_exhaustive]
922pub struct GitSource {
923    /// Repository URL.
924    pub git: String,
925    /// Select specific branch.
926    pub branch: Option<String>,
927    /// Select specific tag.
928    pub tag: Option<String>,
929    /// Select specific rev.
930    pub rev: Option<String>,
931    /// Version requirement for when published.
932    pub version: Option<String>,
933}
934
935impl GitSource {
936    /// Specify dependency from a git repo.
937    pub fn new(git: impl Into<String>) -> Self {
938        Self {
939            git: git.into(),
940            branch: None,
941            tag: None,
942            rev: None,
943            version: None,
944        }
945    }
946
947    /// Specify an optional branch.
948    pub fn set_branch(mut self, branch: impl Into<String>) -> Self {
949        self.branch = Some(branch.into());
950        self.tag = None;
951        self.rev = None;
952        self
953    }
954
955    /// Specify an optional tag.
956    pub fn set_tag(mut self, tag: impl Into<String>) -> Self {
957        self.branch = None;
958        self.tag = Some(tag.into());
959        self.rev = None;
960        self
961    }
962
963    /// Specify an optional rev.
964    pub fn set_rev(mut self, rev: impl Into<String>) -> Self {
965        self.branch = None;
966        self.tag = None;
967        self.rev = Some(rev.into());
968        self
969    }
970
971    /// Get the `SourceID` for this dependency.
972    pub fn source_id(&self) -> CargoResult<SourceId> {
973        let git_url = self.git.parse::<url::Url>()?;
974        let git_ref = self.git_ref();
975        SourceId::for_git(&git_url, git_ref)
976    }
977
978    fn git_ref(&self) -> GitReference {
979        match (
980            self.branch.as_deref(),
981            self.tag.as_deref(),
982            self.rev.as_deref(),
983        ) {
984            (Some(branch), _, _) => GitReference::Branch(branch.to_owned()),
985            (_, Some(tag), _) => GitReference::Tag(tag.to_owned()),
986            (_, _, Some(rev)) => GitReference::Rev(rev.to_owned()),
987            _ => GitReference::DefaultBranch,
988        }
989    }
990
991    /// Set an optional version requirement.
992    pub fn set_version(mut self, version: impl AsRef<str>) -> Self {
993        // versions might have semver metadata appended which we do not want to
994        // store in the cargo toml files.  This would cause a warning upon compilation
995        // ("version requirement […] includes semver metadata which will be ignored")
996        let version = version.as_ref().split('+').next().unwrap();
997        self.version = Some(version.to_owned());
998        self
999    }
1000}
1001
1002impl std::fmt::Display for GitSource {
1003    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1004        let git_ref = self.git_ref();
1005        if let Some(pretty_ref) = git_ref.pretty_ref(true) {
1006            write!(f, "{}?{}", self.git, pretty_ref)
1007        } else {
1008            write!(f, "{}", self.git)
1009        }
1010    }
1011}
1012
1013/// Dependency from a workspace.
1014#[derive(Debug, Hash, PartialEq, Eq, Clone)]
1015#[non_exhaustive]
1016pub struct WorkspaceSource;
1017
1018impl WorkspaceSource {
1019    pub fn new() -> Self {
1020        Self
1021    }
1022}
1023
1024impl Display for WorkspaceSource {
1025    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1026        "workspace".fmt(f)
1027    }
1028}
1029
1030#[cfg(test)]
1031mod tests {
1032    use crate::util::toml_mut::manifest::LocalManifest;
1033    use cargo_util::paths;
1034
1035    use super::*;
1036
1037    #[test]
1038    fn to_toml_simple_dep() {
1039        let crate_root =
1040            paths::normalize_path(&std::env::current_dir().unwrap().join(Path::new("/")));
1041        let dep = Dependency::new("dep").set_source(RegistrySource::new("1.0"));
1042        let key = dep.toml_key();
1043        let gctx = GlobalContext::default().unwrap();
1044        let item = dep
1045            .to_toml(&gctx, &crate_root, &crate_root, &Features::default())
1046            .unwrap();
1047
1048        assert_eq!(key, "dep".to_owned());
1049
1050        verify_roundtrip(&crate_root, &gctx, key, &item);
1051    }
1052
1053    #[test]
1054    fn to_toml_simple_dep_with_version() {
1055        let crate_root =
1056            paths::normalize_path(&std::env::current_dir().unwrap().join(Path::new("/")));
1057        let dep = Dependency::new("dep").set_source(RegistrySource::new("1.0"));
1058        let key = dep.toml_key();
1059        let gctx = GlobalContext::default().unwrap();
1060        let item = dep
1061            .to_toml(&gctx, &crate_root, &crate_root, &Features::default())
1062            .unwrap();
1063
1064        assert_eq!(key, "dep".to_owned());
1065        assert_eq!(item.as_str(), Some("1.0"));
1066
1067        verify_roundtrip(&crate_root, &gctx, key, &item);
1068    }
1069
1070    #[test]
1071    fn to_toml_optional_dep() {
1072        let crate_root =
1073            paths::normalize_path(&std::env::current_dir().unwrap().join(Path::new("/")));
1074        let dep = Dependency::new("dep")
1075            .set_source(RegistrySource::new("1.0"))
1076            .set_optional(true);
1077        let key = dep.toml_key();
1078        let gctx = GlobalContext::default().unwrap();
1079        let item = dep
1080            .to_toml(&gctx, &crate_root, &crate_root, &Features::default())
1081            .unwrap();
1082
1083        assert_eq!(key, "dep".to_owned());
1084        assert!(item.is_inline_table());
1085
1086        let dep = item.as_inline_table().unwrap();
1087        assert_eq!(dep.get("optional").unwrap().as_bool(), Some(true));
1088
1089        verify_roundtrip(&crate_root, &gctx, key, &item);
1090    }
1091
1092    #[test]
1093    fn to_toml_dep_without_default_features() {
1094        let crate_root =
1095            paths::normalize_path(&std::env::current_dir().unwrap().join(Path::new("/")));
1096        let dep = Dependency::new("dep")
1097            .set_source(RegistrySource::new("1.0"))
1098            .set_default_features(false);
1099        let key = dep.toml_key();
1100        let gctx = GlobalContext::default().unwrap();
1101        let item = dep
1102            .to_toml(&gctx, &crate_root, &crate_root, &Features::default())
1103            .unwrap();
1104
1105        assert_eq!(key, "dep".to_owned());
1106        assert!(item.is_inline_table());
1107
1108        let dep = item.as_inline_table().unwrap();
1109        assert_eq!(dep.get("default-features").unwrap().as_bool(), Some(false));
1110
1111        verify_roundtrip(&crate_root, &gctx, key, &item);
1112    }
1113
1114    #[test]
1115    fn to_toml_dep_with_path_source() {
1116        let root = paths::normalize_path(&std::env::current_dir().unwrap().join(Path::new("/")));
1117        let crate_root = root.join("foo");
1118        let dep = Dependency::new("dep").set_source(PathSource::new(root.join("bar")));
1119        let key = dep.toml_key();
1120        let gctx = GlobalContext::default().unwrap();
1121        let item = dep
1122            .to_toml(&gctx, &crate_root, &crate_root, &Features::default())
1123            .unwrap();
1124
1125        assert_eq!(key, "dep".to_owned());
1126        assert!(item.is_inline_table());
1127
1128        let dep = item.as_inline_table().unwrap();
1129        assert_eq!(dep.get("path").unwrap().as_str(), Some("../bar"));
1130
1131        verify_roundtrip(&crate_root, &gctx, key, &item);
1132    }
1133
1134    #[test]
1135    fn to_toml_dep_with_git_source() {
1136        let crate_root =
1137            paths::normalize_path(&std::env::current_dir().unwrap().join(Path::new("/")));
1138        let dep = Dependency::new("dep").set_source(GitSource::new("https://foor/bar.git"));
1139        let key = dep.toml_key();
1140        let gctx = GlobalContext::default().unwrap();
1141        let item = dep
1142            .to_toml(&gctx, &crate_root, &crate_root, &Features::default())
1143            .unwrap();
1144
1145        assert_eq!(key, "dep".to_owned());
1146        assert!(item.is_inline_table());
1147
1148        let dep = item.as_inline_table().unwrap();
1149        assert_eq!(
1150            dep.get("git").unwrap().as_str(),
1151            Some("https://foor/bar.git")
1152        );
1153
1154        verify_roundtrip(&crate_root, &gctx, key, &item);
1155    }
1156
1157    #[test]
1158    fn to_toml_renamed_dep() {
1159        let crate_root =
1160            paths::normalize_path(&std::env::current_dir().unwrap().join(Path::new("/")));
1161        let dep = Dependency::new("dep")
1162            .set_source(RegistrySource::new("1.0"))
1163            .set_rename("d");
1164        let key = dep.toml_key();
1165        let gctx = GlobalContext::default().unwrap();
1166        let item = dep
1167            .to_toml(&gctx, &crate_root, &crate_root, &Features::default())
1168            .unwrap();
1169
1170        assert_eq!(key, "d".to_owned());
1171        assert!(item.is_inline_table());
1172
1173        let dep = item.as_inline_table().unwrap();
1174        assert_eq!(dep.get("package").unwrap().as_str(), Some("dep"));
1175
1176        verify_roundtrip(&crate_root, &gctx, key, &item);
1177    }
1178
1179    #[test]
1180    fn to_toml_dep_from_alt_registry() {
1181        let crate_root =
1182            paths::normalize_path(&std::env::current_dir().unwrap().join(Path::new("/")));
1183        let dep = Dependency::new("dep")
1184            .set_source(RegistrySource::new("1.0"))
1185            .set_registry("alternative");
1186        let key = dep.toml_key();
1187        let gctx = GlobalContext::default().unwrap();
1188        let item = dep
1189            .to_toml(&gctx, &crate_root, &crate_root, &Features::default())
1190            .unwrap();
1191
1192        assert_eq!(key, "dep".to_owned());
1193        assert!(item.is_inline_table());
1194
1195        let dep = item.as_inline_table().unwrap();
1196        assert_eq!(dep.get("registry").unwrap().as_str(), Some("alternative"));
1197
1198        verify_roundtrip(&crate_root, &gctx, key, &item);
1199    }
1200
1201    #[test]
1202    fn to_toml_complex_dep() {
1203        let crate_root =
1204            paths::normalize_path(&std::env::current_dir().unwrap().join(Path::new("/")));
1205        let dep = Dependency::new("dep")
1206            .set_source(RegistrySource::new("1.0"))
1207            .set_default_features(false)
1208            .set_rename("d");
1209        let key = dep.toml_key();
1210        let gctx = GlobalContext::default().unwrap();
1211        let item = dep
1212            .to_toml(&gctx, &crate_root, &crate_root, &Features::default())
1213            .unwrap();
1214
1215        assert_eq!(key, "d".to_owned());
1216        assert!(item.is_inline_table());
1217
1218        let dep = item.as_inline_table().unwrap();
1219        assert_eq!(dep.get("package").unwrap().as_str(), Some("dep"));
1220        assert_eq!(dep.get("version").unwrap().as_str(), Some("1.0"));
1221        assert_eq!(dep.get("default-features").unwrap().as_bool(), Some(false));
1222
1223        verify_roundtrip(&crate_root, &gctx, key, &item);
1224    }
1225
1226    #[test]
1227    fn paths_with_forward_slashes_are_left_as_is() {
1228        let crate_root =
1229            paths::normalize_path(&std::env::current_dir().unwrap().join(Path::new("/")));
1230        let path = crate_root.join("sibling/crate");
1231        let relpath = "sibling/crate";
1232        let dep = Dependency::new("dep").set_source(PathSource::new(path));
1233        let key = dep.toml_key();
1234        let gctx = GlobalContext::default().unwrap();
1235        let item = dep
1236            .to_toml(&gctx, &crate_root, &crate_root, &Features::default())
1237            .unwrap();
1238
1239        let table = item.as_inline_table().unwrap();
1240        let got = table.get("path").unwrap().as_str().unwrap();
1241        assert_eq!(got, relpath);
1242
1243        verify_roundtrip(&crate_root, &gctx, key, &item);
1244    }
1245
1246    #[test]
1247    fn overwrite_with_workspace_source_fmt_key() {
1248        let crate_root =
1249            paths::normalize_path(&std::env::current_dir().unwrap().join(Path::new("./")));
1250        let toml = "dep = \"1.0\"\n";
1251        let manifest = toml.parse().unwrap();
1252        let mut local = LocalManifest {
1253            path: crate_root.clone(),
1254            manifest,
1255            embedded: None,
1256            raw: toml.to_owned(),
1257        };
1258        assert_eq!(local.manifest.to_string(), toml);
1259        let gctx = GlobalContext::default().unwrap();
1260        for (key, item) in local.data.clone().iter() {
1261            let dep = Dependency::from_toml(
1262                &gctx,
1263                &crate_root,
1264                &crate_root,
1265                &Features::default(),
1266                key,
1267                item,
1268            )
1269            .unwrap();
1270            let dep = dep.set_source(WorkspaceSource::new());
1271            local
1272                .insert_into_table(&vec![], &dep, &gctx, &crate_root, &Features::default())
1273                .unwrap();
1274            assert_eq!(local.data.to_string(), "dep.workspace = true\n");
1275        }
1276    }
1277
1278    #[test]
1279    #[cfg(windows)]
1280    fn normalise_windows_style_paths() {
1281        let crate_root =
1282            paths::normalize_path(&std::env::current_dir().unwrap().join(Path::new("/")));
1283        let original = crate_root.join(r"sibling\crate");
1284        let should_be = "sibling/crate";
1285        let dep = Dependency::new("dep").set_source(PathSource::new(original));
1286        let key = dep.toml_key();
1287        let gctx = GlobalContext::default().unwrap();
1288        let item = dep
1289            .to_toml(&gctx, &crate_root, &crate_root, &Features::default())
1290            .unwrap();
1291
1292        let table = item.as_inline_table().unwrap();
1293        let got = table.get("path").unwrap().as_str().unwrap();
1294        assert_eq!(got, should_be);
1295
1296        verify_roundtrip(&crate_root, &gctx, key, &item);
1297    }
1298
1299    #[track_caller]
1300    fn verify_roundtrip(
1301        crate_root: &Path,
1302        gctx: &GlobalContext,
1303        key: &str,
1304        item: &toml_edit::Item,
1305    ) {
1306        let roundtrip = Dependency::from_toml(
1307            gctx,
1308            crate_root,
1309            crate_root,
1310            &Features::default(),
1311            key,
1312            item,
1313        )
1314        .unwrap();
1315        let round_key = roundtrip.toml_key();
1316        let round_item = roundtrip
1317            .to_toml(gctx, crate_root, crate_root, &Features::default())
1318            .unwrap();
1319        assert_eq!(key, round_key);
1320        assert_eq!(item.to_string(), round_item.to_string());
1321    }
1322}