pkgsrc/
pkgname.rs

1/*
2 * Copyright (c) 2024 Jonathan Perkin <jonathan@perkin.org.uk>
3 *
4 * Permission to use, copy, modify, and distribute this software for any
5 * purpose with or without fee is hereby granted, provided that the above
6 * copyright notice and this permission notice appear in all copies.
7 *
8 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
9 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
11 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
14 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
15 */
16
17#[cfg(feature = "serde")]
18use serde_with::{DeserializeFromStr, SerializeDisplay};
19
20/**
21 * Parse a `PKGNAME` into its constituent parts.
22 *
23 * In pkgsrc terminology a `PKGNAME` is made up of three parts:
24 *
25 * * `PKGBASE` contains the name of the package
26 * * `PKGVERSION` contains the full version string
27 * * `PKGREVISION` is an optional package revision denoted by `nb` followed by
28 *   a number.
29 *
30 * The name and version are split at the last `-`, and the revision, if
31 * specified, should be located at the end of the version.
32 *
33 * This module does not enforce strict formatting.  If a `PKGNAME` is not well
34 * formed then values may be empty or [`None`].
35 *
36 * # Examples
37 *
38 * ```
39 * use pkgsrc::PkgName;
40 *
41 * // A well formed package name.
42 * let pkg = PkgName::new("mktool-1.3.2nb2");
43 * assert_eq!(pkg.pkgname(), "mktool-1.3.2nb2");
44 * assert_eq!(pkg.pkgbase(), "mktool");
45 * assert_eq!(pkg.pkgversion(), "1.3.2nb2");
46 * assert_eq!(pkg.pkgrevision(), Some(2));
47 *
48 * // An invalid PKGREVISION that can likely only be created by accident.
49 * let pkg = PkgName::new("mktool-1.3.2nb");
50 * assert_eq!(pkg.pkgbase(), "mktool");
51 * assert_eq!(pkg.pkgversion(), "1.3.2nb");
52 * assert_eq!(pkg.pkgrevision(), Some(0));
53 *
54 * // A "-" in the version causes an incorrect split.
55 * let pkg = PkgName::new("mktool-1.3-2");
56 * assert_eq!(pkg.pkgbase(), "mktool-1.3");
57 * assert_eq!(pkg.pkgversion(), "2");
58 * assert_eq!(pkg.pkgrevision(), None);
59 *
60 * // Not well formed, but still accepted.
61 * let pkg = PkgName::new("mktool");
62 * assert_eq!(pkg.pkgbase(), "mktool");
63 * assert_eq!(pkg.pkgversion(), "");
64 * assert_eq!(pkg.pkgrevision(), None);
65 *
66 * // Doesn't make any sense, but whatever!
67 * let pkg = PkgName::new("1.0nb2");
68 * assert_eq!(pkg.pkgbase(), "1.0nb2");
69 * assert_eq!(pkg.pkgversion(), "");
70 * assert_eq!(pkg.pkgrevision(), None);
71 * ```
72 */
73#[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
74#[cfg_attr(feature = "serde", derive(SerializeDisplay, DeserializeFromStr))]
75pub struct PkgName {
76    pkgname: String,
77    pkgbase: String,
78    pkgversion: String,
79    pkgrevision: Option<i64>,
80}
81
82impl PkgName {
83    /**
84     * Create a new [`PkgName`] from a [`str`] reference.
85     */
86    #[must_use]
87    pub fn new(pkgname: &str) -> Self {
88        let (pkgbase, pkgversion) = match pkgname.rsplit_once('-') {
89            Some((b, v)) => (String::from(b), String::from(v)),
90            None => (String::from(pkgname), String::new()),
91        };
92        let pkgrevision = match pkgversion.rsplit_once("nb") {
93            Some((_, v)) => v.parse::<i64>().ok().or(Some(0)),
94            None => None,
95        };
96        Self {
97            pkgname: pkgname.to_string(),
98            pkgbase,
99            pkgversion,
100            pkgrevision,
101        }
102    }
103
104    /**
105     * Return a [`str`] reference containing the original `PKGNAME` used to
106     * create this instance.
107     */
108    #[must_use]
109    pub fn pkgname(&self) -> &str {
110        &self.pkgname
111    }
112
113    /**
114     * Return a [`str`] reference containing the `PKGBASE` portion of the
115     * package name, i.e.  everything up to the final `-` and the version
116     * number.
117     */
118    #[must_use]
119    pub fn pkgbase(&self) -> &str {
120        &self.pkgbase
121    }
122
123    /**
124     * Return a [`str`] reference containing the full `PKGVERSION` of the
125     * package name, i.e. everything after the final `-`.  If no `-` was found
126     * in the [`str`] used to create this [`PkgName`] then this will be an
127     * empty string.
128     */
129    #[must_use]
130    pub fn pkgversion(&self) -> &str {
131        &self.pkgversion
132    }
133
134    /**
135     * Return an optional `PKGREVISION`, i.e. the `nb<x>` suffix that denotes
136     * a pkgsrc revision.  If any characters after the `nb` cannot be parsed
137     * as an [`i64`] then [`None`] is returned.  If there are no characters at
138     * all after the `nb` then `Some(0)` is returned.
139     */
140    #[must_use]
141    pub const fn pkgrevision(&self) -> Option<i64> {
142        self.pkgrevision
143    }
144}
145
146impl From<&str> for PkgName {
147    fn from(s: &str) -> Self {
148        Self::new(s)
149    }
150}
151
152impl From<String> for PkgName {
153    fn from(s: String) -> Self {
154        Self::new(&s)
155    }
156}
157
158impl From<&String> for PkgName {
159    fn from(s: &String) -> Self {
160        Self::new(s)
161    }
162}
163
164impl std::fmt::Display for PkgName {
165    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
166        write!(f, "{}", self.pkgname)
167    }
168}
169
170impl PartialEq<str> for PkgName {
171    fn eq(&self, other: &str) -> bool {
172        self.pkgname == other
173    }
174}
175
176impl PartialEq<&str> for PkgName {
177    fn eq(&self, other: &&str) -> bool {
178        &self.pkgname == other
179    }
180}
181
182impl PartialEq<String> for PkgName {
183    fn eq(&self, other: &String) -> bool {
184        &self.pkgname == other
185    }
186}
187
188impl std::str::FromStr for PkgName {
189    type Err = std::convert::Infallible;
190
191    fn from_str(s: &str) -> Result<Self, Self::Err> {
192        Ok(Self::new(s))
193    }
194}
195
196impl crate::kv::FromKv for PkgName {
197    fn from_kv(value: &str, _span: crate::kv::Span) -> crate::kv::Result<Self> {
198        Ok(Self::new(value))
199    }
200}
201
202#[cfg(test)]
203mod tests {
204    use super::*;
205
206    #[test]
207    fn pkgname_full() {
208        let pkg = PkgName::new("mktool-1.3.2nb2");
209        assert_eq!(format!("{pkg}"), "mktool-1.3.2nb2");
210        assert_eq!(pkg.pkgname(), "mktool-1.3.2nb2");
211        assert_eq!(pkg.pkgbase(), "mktool");
212        assert_eq!(pkg.pkgversion(), "1.3.2nb2");
213        assert_eq!(pkg.pkgrevision(), Some(2));
214    }
215
216    #[test]
217    fn pkgname_broken_pkgrevision() {
218        let pkg = PkgName::new("mktool-1nb3alpha2nb");
219        assert_eq!(pkg.pkgbase(), "mktool");
220        assert_eq!(pkg.pkgversion(), "1nb3alpha2nb");
221        assert_eq!(pkg.pkgrevision(), Some(0));
222    }
223
224    #[test]
225    fn pkgname_no_version() {
226        let pkg = PkgName::new("mktool");
227        assert_eq!(pkg.pkgbase(), "mktool");
228        assert_eq!(pkg.pkgversion(), "");
229        assert_eq!(pkg.pkgrevision(), None);
230    }
231
232    #[test]
233    fn pkgname_from() {
234        let pkg = PkgName::from("mktool-1.3.2nb2");
235        assert_eq!(pkg.pkgname(), "mktool-1.3.2nb2");
236        let pkg = PkgName::from(String::from("mktool-1.3.2nb2"));
237        assert_eq!(pkg.pkgname(), "mktool-1.3.2nb2");
238        let s = String::from("mktool-1.3.2nb2");
239        let pkg = PkgName::from(&s);
240        assert_eq!(pkg.pkgname(), "mktool-1.3.2nb2");
241    }
242
243    #[test]
244    fn pkgname_from_str() {
245        use std::str::FromStr;
246
247        let pkg = PkgName::from_str("mktool-1.3.2nb2").unwrap();
248        assert_eq!(pkg.pkgname(), "mktool-1.3.2nb2");
249
250        let pkg: PkgName = "foo-2.0".parse().unwrap();
251        assert_eq!(pkg.pkgbase(), "foo");
252    }
253
254    #[test]
255    fn pkgname_partial_eq() {
256        let pkg = PkgName::new("mktool-1.3.2nb2");
257        assert_eq!(pkg, *"mktool-1.3.2nb2");
258        assert_eq!(pkg, "mktool-1.3.2nb2");
259        assert_eq!(pkg, "mktool-1.3.2nb2".to_string());
260        assert_ne!(pkg, "notmktool-1.0");
261    }
262
263    #[test]
264    #[cfg(feature = "serde")]
265    fn pkgname_serde() {
266        let pkg = PkgName::new("mktool-1.3.2nb2");
267        let se = serde_json::to_string(&pkg).unwrap();
268        let de: PkgName = serde_json::from_str(&se).unwrap();
269        assert_eq!(se, "\"mktool-1.3.2nb2\"");
270        assert_eq!(pkg, de);
271        assert_eq!(de.pkgname(), "mktool-1.3.2nb2");
272        assert_eq!(de.pkgbase(), "mktool");
273        assert_eq!(de.pkgversion(), "1.3.2nb2");
274        assert_eq!(de.pkgrevision(), Some(2));
275    }
276}