runmat_package/identity/
registry.rs1use crate::policy::{validate_canonical_segment, REGISTRY_SEGMENT_MAX_LEN};
2use crate::IdentityError;
3use serde::{Deserialize, Serialize};
4use std::fmt::{Display, Formatter};
5use std::str::FromStr;
6use url::Url;
7
8#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
9#[serde(try_from = "String", into = "String")]
10pub struct RegistryId(String);
11
12impl RegistryId {
13 pub fn new(value: impl Into<String>) -> Result<Self, IdentityError> {
14 let value = value.into();
15 validate_canonical_segment(&value, "registry name", REGISTRY_SEGMENT_MAX_LEN)?;
16 Ok(Self(value))
17 }
18
19 pub fn as_str(&self) -> &str {
20 &self.0
21 }
22}
23
24impl Default for RegistryId {
25 fn default() -> Self {
26 Self("default".to_string())
27 }
28}
29
30impl Display for RegistryId {
31 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
32 formatter.write_str(&self.0)
33 }
34}
35
36impl FromStr for RegistryId {
37 type Err = IdentityError;
38
39 fn from_str(value: &str) -> Result<Self, Self::Err> {
40 Self::new(value)
41 }
42}
43
44impl TryFrom<String> for RegistryId {
45 type Error = IdentityError;
46
47 fn try_from(value: String) -> Result<Self, Self::Error> {
48 Self::new(value)
49 }
50}
51
52impl From<RegistryId> for String {
53 fn from(value: RegistryId) -> Self {
54 value.0
55 }
56}
57
58#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
59#[serde(try_from = "String", into = "String")]
60pub struct RegistryOrigin(String);
61
62impl RegistryOrigin {
63 pub fn new(value: &str) -> Result<Self, IdentityError> {
64 let mut url =
65 Url::parse(value).map_err(|_| invalid_source(value, "must be an HTTPS URL"))?;
66 if url.scheme() != "https"
67 || !url.username().is_empty()
68 || url.password().is_some()
69 || url.query().is_some()
70 || url.fragment().is_some()
71 || !matches!(url.path(), "" | "/")
72 {
73 return Err(invalid_source(
74 value,
75 "must be a credential-free HTTPS origin without a path, query, or fragment",
76 ));
77 }
78 let host = url
79 .host_str()
80 .ok_or_else(|| invalid_source(value, "must include a host"))?
81 .to_ascii_lowercase();
82 url.set_host(Some(&host))
83 .map_err(|_| invalid_source(value, "contains an invalid host"))?;
84 if url.port_or_known_default() == Some(443) {
85 url.set_port(None)
86 .map_err(|_| invalid_source(value, "contains an invalid port"))?;
87 }
88 url.set_path("");
89 Ok(Self(url.to_string().trim_end_matches('/').to_string()))
90 }
91
92 pub fn as_str(&self) -> &str {
93 &self.0
94 }
95}
96
97impl Display for RegistryOrigin {
98 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
99 formatter.write_str(&self.0)
100 }
101}
102
103impl FromStr for RegistryOrigin {
104 type Err = IdentityError;
105
106 fn from_str(value: &str) -> Result<Self, Self::Err> {
107 Self::new(value)
108 }
109}
110
111impl TryFrom<String> for RegistryOrigin {
112 type Error = IdentityError;
113
114 fn try_from(value: String) -> Result<Self, Self::Error> {
115 Self::new(&value)
116 }
117}
118
119impl From<RegistryOrigin> for String {
120 fn from(value: RegistryOrigin) -> Self {
121 value.0
122 }
123}
124
125#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
126#[serde(try_from = "String", into = "String")]
127pub struct RegistryReleaseId(String);
128
129impl RegistryReleaseId {
130 pub fn new(value: impl Into<String>) -> Result<Self, IdentityError> {
131 let value = value.into();
132 let suffix = value
133 .strip_prefix("rel_")
134 .ok_or_else(|| invalid_source(&value, "release ID must start with `rel_`"))?;
135 if suffix.len() != 32
136 || suffix
137 .bytes()
138 .any(|byte| !byte.is_ascii_hexdigit() || byte.is_ascii_uppercase())
139 {
140 return Err(invalid_source(
141 &value,
142 "release ID must contain 32 lowercase hexadecimal digits",
143 ));
144 }
145 Ok(Self(value))
146 }
147
148 pub fn as_str(&self) -> &str {
149 &self.0
150 }
151}
152
153impl Display for RegistryReleaseId {
154 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
155 formatter.write_str(&self.0)
156 }
157}
158
159impl FromStr for RegistryReleaseId {
160 type Err = IdentityError;
161
162 fn from_str(value: &str) -> Result<Self, Self::Err> {
163 Self::new(value)
164 }
165}
166
167impl TryFrom<String> for RegistryReleaseId {
168 type Error = IdentityError;
169
170 fn try_from(value: String) -> Result<Self, Self::Error> {
171 Self::new(value)
172 }
173}
174
175impl From<RegistryReleaseId> for String {
176 fn from(value: RegistryReleaseId) -> Self {
177 value.0
178 }
179}
180
181fn invalid_source(value: &str, reason: &'static str) -> IdentityError {
182 IdentityError::InvalidRegistrySource {
183 value: value.to_string(),
184 reason,
185 }
186}