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}