Skip to main content

release_kit/depend/
version.rs

1//! The version a dependency is pinned at, and the tag that names it.
2//!
3//! The source tree's declared version is the default and `--pin` the
4//! one override; the tag form follows the shape the source's own tags
5//! show. No registry is asked whether the version is published.
6
7use serde::Serialize;
8
9use super::source::{Source, TagStyle};
10use crate::error::RkError;
11
12/// The resolved pin.
13#[derive(Debug, Clone, Serialize)]
14pub struct Resolved {
15    /// The bare version, `1.2.3`.
16    pub version: String,
17    /// The tag that names it in the source's own shape.
18    pub tag: String,
19    /// `argument` or `source-tree`.
20    pub origin: &'static str,
21}
22
23/// Resolve the pin from the argument, else from the source tree.
24///
25/// # Errors
26///
27/// Returns [`RkError::Usage`] for an argument that is not a version and
28/// for a source that declares none while no argument was given.
29pub fn resolve(source: &Source, argument: Option<&str>) -> Result<Resolved, RkError> {
30    if let Some(raw) = argument {
31        let Some(tag) = crate::devshell::normalize_tag(raw) else {
32            return Err(RkError::Usage(format!(
33                "--pin {raw} is not a version: pass 1.2.3, v1.2.3, or the release URL"
34            )));
35        };
36        let version = tag.trim_start_matches('v').to_owned();
37        return Ok(Resolved {
38            tag: tag_for(&version, source.tag_style),
39            version,
40            origin: "argument",
41        });
42    }
43    let Some(version) = source.version.clone() else {
44        return Err(RkError::Usage(
45            "the source declares no version; pass --pin".into(),
46        ));
47    };
48    Ok(Resolved {
49        tag: tag_for(&version, source.tag_style),
50        version,
51        origin: "source-tree",
52    })
53}
54
55/// The tag for a version in the source's shape; the prefixed form where
56/// the tags say nothing.
57#[must_use]
58pub fn tag_for(version: &str, style: TagStyle) -> String {
59    match style {
60        TagStyle::Bare => version.to_owned(),
61        TagStyle::Prefixed | TagStyle::Unknown => format!("v{version}"),
62    }
63}
64
65#[cfg(test)]
66mod tests {
67    #![allow(clippy::expect_used)]
68
69    use camino::Utf8PathBuf;
70
71    use super::{TagStyle, resolve, tag_for};
72    use crate::depend::source::Source;
73    use crate::error::RkError;
74
75    fn source(version: Option<&str>, style: TagStyle) -> Source {
76        Source {
77            path: Utf8PathBuf::from("/srv/sample"),
78            tech: Some("rust"),
79            name: Some("sample-tool".into()),
80            version: version.map(str::to_owned),
81            bins: Vec::new(),
82            owner_repo: None,
83            host: None,
84            flake_package: false,
85            dist_github: false,
86            binstall_github: false,
87            tag_style: style,
88            channels: Vec::new(),
89        }
90    }
91
92    /// SATISFIES dependencies:the-version-comes-from-the-source-tree
93    #[test]
94    fn the_tag_follows_the_source_tag_style() {
95        assert_eq!(tag_for("1.4.0", TagStyle::Prefixed), "v1.4.0");
96        assert_eq!(tag_for("1.4.0", TagStyle::Bare), "1.4.0");
97        assert_eq!(tag_for("1.4.0", TagStyle::Unknown), "v1.4.0");
98        let resolved = resolve(&source(Some("1.4.0"), TagStyle::Bare), None).expect("resolves");
99        assert_eq!(resolved.version, "1.4.0");
100        assert_eq!(resolved.tag, "1.4.0");
101        assert_eq!(resolved.origin, "source-tree");
102    }
103
104    /// SATISFIES dependencies:the-version-comes-from-the-source-tree
105    #[test]
106    fn the_argument_overrides_the_tree() {
107        let tree = source(Some("1.4.0"), TagStyle::Prefixed);
108        for raw in [
109            "2.0.0",
110            "v2.0.0",
111            "https://github.com/acme/sample/releases/tag/v2.0.0",
112        ] {
113            let resolved = resolve(&tree, Some(raw)).expect("resolves");
114            assert_eq!(resolved.version, "2.0.0", "{raw}");
115            assert_eq!(resolved.tag, "v2.0.0", "{raw}");
116            assert_eq!(resolved.origin, "argument");
117        }
118        assert!(matches!(
119            resolve(&tree, Some("latest")),
120            Err(RkError::Usage(_))
121        ));
122    }
123
124    #[test]
125    fn no_version_is_a_usage_error() {
126        assert!(matches!(
127            resolve(&source(None, TagStyle::Unknown), None),
128            Err(RkError::Usage(_))
129        ));
130    }
131}