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