Skip to main content

deps_core/
package.rs

1//! Newtypes distinguishing package names, version requirement strings, and
2//! concrete resolved versions.
3//!
4//! [`PackageName`], [`VersionReq`], and [`ConcreteVersion`] wrap `String` to
5//! give these kinds of manifest and registry data their own types, so a
6//! function that expects one cannot accidentally be called with another.
7//! None of these types validate, trim, or normalize their contents:
8//! ecosystem-specific normalization (case folding, separator rewriting,
9//! etc.) belongs to `EcosystemFormatter`, not to these types. See each
10//! type's documentation for details.
11
12use std::borrow::Borrow;
13use std::fmt;
14
15/// A package/crate name as it appears in a manifest file.
16///
17/// This is deliberately permissive: it stores whatever bytes the manifest
18/// contained, including the empty string, leading/trailing whitespace, or
19/// non-ASCII characters. No validation, trimming, or normalization is
20/// performed by this type.
21///
22/// For several ecosystems this is not a "package name" in the narrow sense
23/// but a registry lookup key: Maven and Gradle store `"group:artifact"`, Swift
24/// stores `"owner/repo"`, and Go stores a URL-like module path. All of these
25/// are valid `PackageName` values. Ecosystem-specific normalization (case
26/// folding, separator rewriting, etc.) is the responsibility of
27/// `EcosystemFormatter`, not this type — do not add validation rules here,
28/// as it would silently break those ecosystems.
29///
30/// # Examples
31///
32/// ```
33/// use deps_core::PackageName;
34///
35/// let name = PackageName::new("serde");
36/// assert_eq!(name.as_str(), "serde");
37/// assert_eq!(name, "serde");
38/// ```
39#[derive(Debug, Clone, PartialEq, Eq, Hash)]
40pub struct PackageName(String);
41
42impl PackageName {
43    /// Wraps `value` as a `PackageName`, unchanged.
44    ///
45    /// This never fails and never modifies its input: an empty string is a
46    /// valid `PackageName`, as is a string with surrounding whitespace.
47    ///
48    /// # Examples
49    ///
50    /// ```
51    /// use deps_core::PackageName;
52    ///
53    /// let name = PackageName::new(String::from("tokio"));
54    /// assert_eq!(name.as_str(), "tokio");
55    /// ```
56    pub fn new(value: impl Into<String>) -> Self {
57        Self(value.into())
58    }
59
60    /// Returns the package name as a string slice.
61    ///
62    /// # Examples
63    ///
64    /// ```
65    /// use deps_core::PackageName;
66    ///
67    /// let name = PackageName::new("axum");
68    /// assert_eq!(name.as_str(), "axum");
69    /// ```
70    #[must_use]
71    pub fn as_str(&self) -> &str {
72        &self.0
73    }
74
75    /// Consumes the `PackageName`, returning the wrapped `String`.
76    ///
77    /// # Examples
78    ///
79    /// ```
80    /// use deps_core::PackageName;
81    ///
82    /// let name = PackageName::new("axum");
83    /// let owned: String = name.into_string();
84    /// assert_eq!(owned, "axum");
85    /// ```
86    #[must_use]
87    pub fn into_string(self) -> String {
88        self.0
89    }
90}
91
92impl fmt::Display for PackageName {
93    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94        f.write_str(&self.0)
95    }
96}
97
98impl From<String> for PackageName {
99    fn from(value: String) -> Self {
100        Self(value)
101    }
102}
103
104impl From<&str> for PackageName {
105    fn from(value: &str) -> Self {
106        Self(value.to_string())
107    }
108}
109
110impl AsRef<str> for PackageName {
111    fn as_ref(&self) -> &str {
112        &self.0
113    }
114}
115
116/// Enables `&str` lookups into `HashMap<PackageName, _>`/`HashSet<PackageName>`.
117///
118/// Sound because derived `Hash`/`Eq` on `PackageName(String)` delegate to
119/// `String`'s implementations, which in turn are defined to match `str`'s
120/// exactly (`String: Borrow<str>` in `std` rests on the same guarantee) — so
121/// `PackageName` and the `str` it borrows always hash and compare equal.
122impl Borrow<str> for PackageName {
123    fn borrow(&self) -> &str {
124        &self.0
125    }
126}
127
128impl PartialEq<str> for PackageName {
129    fn eq(&self, other: &str) -> bool {
130        self.0 == other
131    }
132}
133
134impl PartialEq<&str> for PackageName {
135    fn eq(&self, other: &&str) -> bool {
136        self.0 == *other
137    }
138}
139
140/// A package name that failed a [`PackageNaming::validate_package_name`] lint.
141///
142/// This is not a construction-time gate — [`PackageName::new`] stays infallible — it
143/// only carries *why* a name looks wrong so an LSP diagnostic can say something more
144/// specific than "invalid name".
145///
146/// [`PackageNaming::validate_package_name`]: crate::lsp_helpers::PackageNaming::validate_package_name
147///
148/// # Examples
149///
150/// ```
151/// use deps_core::InvalidPackageName;
152///
153/// let err = InvalidPackageName::new("name is longer than 214 characters");
154/// assert_eq!(err.reason(), "name is longer than 214 characters");
155/// assert_eq!(err.to_string(), "name is longer than 214 characters");
156/// ```
157#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
158#[error("{0}")]
159pub struct InvalidPackageName(std::borrow::Cow<'static, str>);
160
161impl InvalidPackageName {
162    /// Creates an `InvalidPackageName` carrying `reason` as the explanation.
163    pub fn new(reason: impl Into<std::borrow::Cow<'static, str>>) -> Self {
164        Self(reason.into())
165    }
166
167    /// Returns why the name was rejected.
168    #[must_use]
169    pub fn reason(&self) -> &str {
170        &self.0
171    }
172}
173
174/// A version requirement string as it appears in a manifest file.
175///
176/// This is deliberately permissive: it stores whatever bytes the manifest
177/// contained (e.g. `"^1.0"`, `">=2.0,<3.0"`, `"*"`), including the empty
178/// string. No parsing, validation, trimming, or normalization is performed
179/// by this type — ecosystems that need to parse a requirement (via `semver`,
180/// `node-semver`, `pep440_rs`, etc.) do so from [`VersionReq::as_str`], not
181/// from this type. Note that `deps-go`'s `GoDependency.version` also uses this
182/// type even though it holds an exact pinned version (e.g. `"v1.9.1"`), not a
183/// range or constraint — Go modules don't have a separate "requirement"
184/// concept, so the exact version doubles as the requirement.
185///
186/// # Examples
187///
188/// ```
189/// use deps_core::VersionReq;
190///
191/// let req = VersionReq::new("^1.0");
192/// assert_eq!(req.as_str(), "^1.0");
193/// assert_eq!(req, "^1.0");
194/// ```
195#[derive(Debug, Clone, PartialEq, Eq, Hash)]
196pub struct VersionReq(String);
197
198impl VersionReq {
199    /// Wraps `value` as a `VersionReq`, unchanged.
200    ///
201    /// This never fails and never modifies its input: an empty string is a
202    /// valid `VersionReq`, as is a string with surrounding whitespace.
203    ///
204    /// # Examples
205    ///
206    /// ```
207    /// use deps_core::VersionReq;
208    ///
209    /// let req = VersionReq::new(String::from(">=1.0"));
210    /// assert_eq!(req.as_str(), ">=1.0");
211    /// ```
212    pub fn new(value: impl Into<String>) -> Self {
213        Self(value.into())
214    }
215
216    /// Returns the version requirement as a string slice.
217    ///
218    /// # Examples
219    ///
220    /// ```
221    /// use deps_core::VersionReq;
222    ///
223    /// let req = VersionReq::new("~1.2");
224    /// assert_eq!(req.as_str(), "~1.2");
225    /// ```
226    #[must_use]
227    pub fn as_str(&self) -> &str {
228        &self.0
229    }
230
231    /// Consumes the `VersionReq`, returning the wrapped `String`.
232    ///
233    /// # Examples
234    ///
235    /// ```
236    /// use deps_core::VersionReq;
237    ///
238    /// let req = VersionReq::new("~1.2");
239    /// let owned: String = req.into_string();
240    /// assert_eq!(owned, "~1.2");
241    /// ```
242    #[must_use]
243    pub fn into_string(self) -> String {
244        self.0
245    }
246}
247
248impl fmt::Display for VersionReq {
249    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
250        f.write_str(&self.0)
251    }
252}
253
254impl From<String> for VersionReq {
255    fn from(value: String) -> Self {
256        Self(value)
257    }
258}
259
260impl From<&str> for VersionReq {
261    fn from(value: &str) -> Self {
262        Self(value.to_string())
263    }
264}
265
266impl AsRef<str> for VersionReq {
267    fn as_ref(&self) -> &str {
268        &self.0
269    }
270}
271
272impl PartialEq<str> for VersionReq {
273    fn eq(&self, other: &str) -> bool {
274        self.0 == other
275    }
276}
277
278impl PartialEq<&str> for VersionReq {
279    fn eq(&self, other: &&str) -> bool {
280        self.0 == *other
281    }
282}
283
284/// A concrete, resolved version string as returned by a registry.
285///
286/// This is deliberately permissive: it stores whatever bytes the registry
287/// returned (e.g. `"1.2.3"`, `"2.0.0-beta.1"`), including the empty string.
288/// No parsing, validation, trimming, or normalization is performed by this
289/// type — ecosystems that need to parse a version (via `semver`,
290/// `node-semver`, `pep440_rs`, etc.) do so from [`ConcreteVersion::as_str`],
291/// not from this type. It is distinct from [`VersionReq`]: a `VersionReq` is
292/// a constraint written in a manifest (`"^1.0"`), while a `ConcreteVersion`
293/// is a single resolved version (`"1.0.4"`).
294///
295/// # Examples
296///
297/// ```
298/// use deps_core::ConcreteVersion;
299///
300/// let version = ConcreteVersion::new("1.2.3");
301/// assert_eq!(version.as_str(), "1.2.3");
302/// assert_eq!(version, "1.2.3");
303/// ```
304#[derive(Debug, Clone, PartialEq, Eq, Hash)]
305pub struct ConcreteVersion(String);
306
307impl ConcreteVersion {
308    /// Wraps `value` as a `ConcreteVersion`, unchanged.
309    ///
310    /// This never fails and never modifies its input: an empty string is a
311    /// valid `ConcreteVersion`, as is a string with surrounding whitespace.
312    ///
313    /// # Examples
314    ///
315    /// ```
316    /// use deps_core::ConcreteVersion;
317    ///
318    /// let version = ConcreteVersion::new(String::from("1.0.0"));
319    /// assert_eq!(version.as_str(), "1.0.0");
320    /// ```
321    pub fn new(value: impl Into<String>) -> Self {
322        Self(value.into())
323    }
324
325    /// Returns the concrete version as a string slice.
326    ///
327    /// # Examples
328    ///
329    /// ```
330    /// use deps_core::ConcreteVersion;
331    ///
332    /// let version = ConcreteVersion::new("4.5.6");
333    /// assert_eq!(version.as_str(), "4.5.6");
334    /// ```
335    #[must_use]
336    pub fn as_str(&self) -> &str {
337        &self.0
338    }
339
340    /// Consumes the `ConcreteVersion`, returning the wrapped `String`.
341    ///
342    /// # Examples
343    ///
344    /// ```
345    /// use deps_core::ConcreteVersion;
346    ///
347    /// let version = ConcreteVersion::new("4.5.6");
348    /// let owned: String = version.into_string();
349    /// assert_eq!(owned, "4.5.6");
350    /// ```
351    #[must_use]
352    pub fn into_string(self) -> String {
353        self.0
354    }
355}
356
357impl fmt::Display for ConcreteVersion {
358    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
359        f.write_str(&self.0)
360    }
361}
362
363impl From<String> for ConcreteVersion {
364    fn from(value: String) -> Self {
365        Self(value)
366    }
367}
368
369impl From<&str> for ConcreteVersion {
370    fn from(value: &str) -> Self {
371        Self(value.to_string())
372    }
373}
374
375impl AsRef<str> for ConcreteVersion {
376    fn as_ref(&self) -> &str {
377        &self.0
378    }
379}
380
381impl PartialEq<str> for ConcreteVersion {
382    fn eq(&self, other: &str) -> bool {
383        self.0 == other
384    }
385}
386
387impl PartialEq<&str> for ConcreteVersion {
388    fn eq(&self, other: &&str) -> bool {
389        self.0 == *other
390    }
391}
392
393#[cfg(test)]
394mod tests {
395    use super::{ConcreteVersion, PackageName, VersionReq};
396
397    #[test]
398    fn package_name_round_trips_empty_string() {
399        let name = PackageName::new("");
400        assert_eq!(name.as_str(), "");
401        assert_eq!(name, "");
402    }
403
404    #[test]
405    fn package_name_round_trips_surrounding_whitespace() {
406        let name = PackageName::new("  serde  ");
407        assert_eq!(name.as_str(), "  serde  ");
408        assert_eq!(name, "  serde  ");
409    }
410
411    #[test]
412    fn package_name_round_trips_non_ascii() {
413        let name = PackageName::new("パッケージ");
414        assert_eq!(name.as_str(), "パッケージ");
415        assert_eq!(name, "パッケージ");
416    }
417
418    #[test]
419    fn package_name_compares_byte_wise_like_string() {
420        assert_ne!(PackageName::new("Foo"), PackageName::new("foo"));
421        assert_eq!(
422            PackageName::new("Foo") == PackageName::new("foo"),
423            "Foo" == "foo"
424        );
425    }
426
427    #[test]
428    fn package_name_into_string_roundtrip() {
429        let original = String::from("tokio");
430        let name = PackageName::new(original.clone());
431        assert_eq!(name.into_string(), original);
432    }
433
434    #[test]
435    fn package_name_hashmap_reachable_by_str_and_by_package_name() {
436        use std::collections::HashMap;
437
438        let mut map: HashMap<PackageName, u32> = HashMap::new();
439        map.insert(PackageName::new("serde"), 1);
440
441        assert_eq!(map.get("serde"), Some(&1));
442        assert_eq!(map.get(&PackageName::new("serde")), Some(&1));
443    }
444
445    #[test]
446    fn version_req_round_trips_empty_string() {
447        let req = VersionReq::new("");
448        assert_eq!(req.as_str(), "");
449        assert_eq!(req, "");
450    }
451
452    #[test]
453    fn version_req_round_trips_surrounding_whitespace() {
454        let req = VersionReq::new("  ^1.0  ");
455        assert_eq!(req.as_str(), "  ^1.0  ");
456        assert_eq!(req, "  ^1.0  ");
457    }
458
459    #[test]
460    fn version_req_round_trips_non_ascii() {
461        let req = VersionReq::new("非対応");
462        assert_eq!(req.as_str(), "非対応");
463        assert_eq!(req, "非対応");
464    }
465
466    #[test]
467    fn version_req_compares_byte_wise_like_string() {
468        assert_ne!(VersionReq::new("^1.0"), VersionReq::new("^1.0 "));
469    }
470
471    #[test]
472    fn version_req_into_string_roundtrip() {
473        let original = String::from("~2.3.4");
474        let req = VersionReq::new(original.clone());
475        assert_eq!(req.into_string(), original);
476    }
477
478    #[test]
479    fn concrete_version_round_trips_empty_string() {
480        let version = ConcreteVersion::new("");
481        assert_eq!(version.as_str(), "");
482        assert_eq!(version, "");
483    }
484
485    #[test]
486    fn concrete_version_round_trips_surrounding_whitespace() {
487        let version = ConcreteVersion::new("  1.0.0  ");
488        assert_eq!(version.as_str(), "  1.0.0  ");
489        assert_eq!(version, "  1.0.0  ");
490    }
491
492    #[test]
493    fn concrete_version_round_trips_non_ascii() {
494        let version = ConcreteVersion::new("バージョン");
495        assert_eq!(version.as_str(), "バージョン");
496        assert_eq!(version, "バージョン");
497    }
498
499    #[test]
500    fn concrete_version_compares_byte_wise_like_string() {
501        assert_ne!(
502            ConcreteVersion::new("1.0.0"),
503            ConcreteVersion::new("1.0.0 ")
504        );
505    }
506
507    #[test]
508    fn concrete_version_into_string_roundtrip() {
509        let original = String::from("2.3.4");
510        let version = ConcreteVersion::new(original.clone());
511        assert_eq!(version.into_string(), original);
512    }
513}