Skip to main content

provenant/models/
package_uid.rs

1// SPDX-FileCopyrightText: Provenant contributors
2// SPDX-License-Identifier: Apache-2.0
3
4use std::borrow::Borrow;
5use std::fmt;
6use std::ops::Deref;
7
8use serde::{Deserialize, Serialize};
9use uuid::Uuid;
10
11#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
12#[serde(transparent)]
13pub struct PackageUid(String);
14
15impl PackageUid {
16    /// Creates a new `PackageUid` by appending a UUID to the given purl.
17    pub fn new(purl: &str) -> Self {
18        let uuid = Uuid::new_v4();
19        Self::with_uuid_suffix(purl, uuid)
20    }
21
22    /// Creates a new `PackageUid` from a non-purl base string.
23    pub fn new_opaque(base: &str) -> Self {
24        let uuid = Uuid::new_v4();
25        Self::with_uuid_suffix(base, uuid)
26    }
27
28    fn with_uuid_suffix(base: &str, uuid: Uuid) -> Self {
29        if base.contains('?') {
30            PackageUid(format!("{}&uuid={}", base, uuid))
31        } else {
32            PackageUid(format!("{}?uuid={}", base, uuid))
33        }
34    }
35
36    /// Wraps an existing UID string without validation or UUID generation.
37    ///
38    /// Use this for deserialization boundaries and round-trip conversions
39    /// where the UID string is already well-formed.
40    pub fn from_raw(s: String) -> Self {
41        PackageUid(s)
42    }
43
44    /// Returns the empty-string sentinel representing "no purl".
45    pub fn empty() -> Self {
46        PackageUid(String::new())
47    }
48
49    /// Returns the purl portion by stripping the UUID suffix.
50    pub fn stable_key(&self) -> &str {
51        self.0
52            .split_once("?uuid=")
53            .map(|(prefix, _)| prefix)
54            .or_else(|| self.0.split_once("&uuid=").map(|(prefix, _)| prefix))
55            .unwrap_or(&self.0)
56    }
57
58    /// Returns a new `PackageUid` with the purl base replaced, preserving the UUID suffix.
59    pub fn replace_base(&self, new_purl: &str) -> Self {
60        if let Some((_, suffix)) = self.0.split_once("?uuid=") {
61            return PackageUid(format!("{}?uuid={}", new_purl, suffix));
62        }
63        if let Some((_, suffix)) = self.0.split_once("&uuid=") {
64            let separator = if new_purl.contains('?') { '&' } else { '?' };
65            return PackageUid(format!("{}{separator}uuid={suffix}", new_purl));
66        }
67        PackageUid(self.0.clone())
68    }
69
70    /// Returns the inner string slice.
71    pub fn as_str(&self) -> &str {
72        &self.0
73    }
74
75    /// Returns `true` if this is the empty-string sentinel.
76    pub fn is_empty(&self) -> bool {
77        self.0.is_empty()
78    }
79}
80
81impl fmt::Display for PackageUid {
82    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
83        self.0.fmt(f)
84    }
85}
86
87impl AsRef<str> for PackageUid {
88    fn as_ref(&self) -> &str {
89        &self.0
90    }
91}
92
93impl Borrow<str> for PackageUid {
94    fn borrow(&self) -> &str {
95        &self.0
96    }
97}
98
99impl Deref for PackageUid {
100    type Target = str;
101
102    fn deref(&self) -> &Self::Target {
103        &self.0
104    }
105}