jacquard_common/types/
nsid.rs1use crate::bos::{Bos, DefaultStr};
2use crate::types::recordkey::RecordKeyType;
3use crate::types::string::{AtStrError, StrParseKind};
4use crate::{CowStr, IntoStatic};
5use alloc::string::{String, ToString};
6use core::fmt;
7use core::ops::Deref;
8use core::str::FromStr;
9#[cfg(all(not(target_arch = "wasm32"), feature = "std"))]
10use regex::Regex;
11#[cfg(all(not(target_arch = "wasm32"), not(feature = "std")))]
12use regex_automata::meta::Regex;
13#[cfg(target_arch = "wasm32")]
14use regex_lite::Regex;
15use serde::{Deserialize, Deserializer, Serialize};
16use smol_str::{SmolStr, ToSmolStr};
17
18use super::Lazy;
19
20#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize)]
24#[serde(transparent)]
25#[repr(transparent)]
26pub struct Nsid<S: Bos<str> = DefaultStr>(pub(crate) S);
27
28pub static NSID_REGEX: Lazy<Regex> = Lazy::new(|| {
30 Regex::new(r"^[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(\.[a-zA-Z][a-zA-Z0-9]{0,62})$").unwrap()
31});
32
33pub fn validate_nsid(nsid: &str) -> Result<(), AtStrError> {
38 if nsid.len() > 317 {
39 Err(AtStrError::too_long("nsid", nsid, 317, nsid.len()))
40 } else if !NSID_REGEX.is_match(nsid) {
41 Err(AtStrError::regex(
42 "nsid",
43 nsid,
44 SmolStr::new_static("invalid"),
45 ))
46 } else {
47 Ok(())
48 }
49}
50
51impl<S: Bos<str> + AsRef<str>> Nsid<S> {
52 pub fn as_str(&self) -> &str {
54 self.0.as_ref()
55 }
56
57 pub fn domain_authority(&self) -> &str {
59 let s = self.as_str();
60 let split = s.rfind('.').expect("enforced by constructor");
61 &s[..split]
62 }
63
64 pub fn name(&self) -> &str {
66 let s = self.as_str();
67 let split = s.rfind('.').expect("enforced by constructor");
68 &s[split + 1..]
69 }
70}
71
72impl<S: Bos<str>> Nsid<S> {
73 pub unsafe fn unchecked(nsid: S) -> Self {
77 Nsid(nsid)
78 }
79
80 pub fn borrow(&self) -> Nsid<&str>
82 where
83 S: AsRef<str>,
84 {
85 unsafe { Nsid::unchecked(self.0.as_ref()) }
87 }
88}
89
90impl<S: Bos<str> + AsRef<str>> Nsid<S> {
91 pub fn new(s: S) -> Result<Self, AtStrError> {
93 validate_nsid(s.as_ref())?;
94 Ok(Self(s))
95 }
96
97 pub fn raw(s: S) -> Self {
99 Self::new(s).expect("invalid NSID")
100 }
101}
102
103impl<S: Bos<str> + FromStr> Nsid<S> {
104 pub fn new_owned(nsid: impl AsRef<str>) -> Result<Self, AtStrError> {
106 let nsid = nsid.as_ref();
107 validate_nsid(nsid)?;
108 let s = S::from_str(nsid)
109 .map_err(|_| AtStrError::new("nsid", nsid.to_string(), StrParseKind::Conversion))?;
110 Ok(Self(s))
111 }
112
113 pub fn new_static(nsid: &'static str) -> Result<Self, AtStrError> {
115 validate_nsid(nsid)?;
116 let s = S::from_str(nsid)
117 .map_err(|_| AtStrError::new("nsid", nsid.to_string(), StrParseKind::Conversion))?;
118 Ok(Self(s))
119 }
120}
121
122impl<'de, S> Deserialize<'de> for Nsid<S>
123where
124 S: Bos<str> + AsRef<str> + Deserialize<'de>,
125{
126 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
127 where
128 D: Deserializer<'de>,
129 {
130 let s = S::deserialize(deserializer)?;
131 validate_nsid(s.as_ref()).map_err(serde::de::Error::custom)?;
132 Ok(Nsid(s))
133 }
134}
135
136impl<S: Bos<str> + IntoStatic> IntoStatic for Nsid<S>
137where
138 S::Output: Bos<str>,
139{
140 type Output = Nsid<S::Output>;
141
142 fn into_static(self) -> Self::Output {
143 Nsid(self.0.into_static())
144 }
145}
146
147impl<S: Bos<str>> Nsid<S> {
148 pub fn convert<B: Bos<str> + From<S>>(self) -> Nsid<B> {
150 Nsid(B::from(self.0))
151 }
152}
153
154impl<S: Bos<str> + FromStr> FromStr for Nsid<S> {
155 type Err = AtStrError;
156
157 fn from_str(s: &str) -> Result<Self, Self::Err> {
158 Self::new_owned(s)
159 }
160}
161
162impl<S: Bos<str> + AsRef<str>> fmt::Display for Nsid<S> {
163 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
164 f.write_str(self.as_str())
165 }
166}
167
168impl<S: Bos<str> + AsRef<str>> fmt::Debug for Nsid<S> {
169 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
170 write!(f, "at://{}", self.as_str())
171 }
172}
173
174impl<S: Bos<str> + AsRef<str>> From<Nsid<S>> for String {
175 fn from(value: Nsid<S>) -> Self {
176 value.as_str().to_string()
177 }
178}
179
180impl<S: Bos<str> + AsRef<str>> From<Nsid<S>> for SmolStr {
181 fn from(value: Nsid<S>) -> Self {
182 value.as_str().to_smolstr()
183 }
184}
185
186impl From<String> for Nsid {
187 fn from(value: String) -> Self {
188 Self::new_owned(value).unwrap()
189 }
190}
191
192impl<'n> From<CowStr<'n>> for Nsid<CowStr<'n>> {
193 fn from(value: CowStr<'n>) -> Self {
194 Self::new(value).unwrap()
195 }
196}
197
198impl From<SmolStr> for Nsid {
199 fn from(value: SmolStr) -> Self {
200 Self::new_owned(value).unwrap()
201 }
202}
203
204impl<S: Bos<str> + AsRef<str>> AsRef<str> for Nsid<S> {
205 fn as_ref(&self) -> &str {
206 self.as_str()
207 }
208}
209
210impl<S: Bos<str> + AsRef<str>> Deref for Nsid<S> {
211 type Target = str;
212
213 fn deref(&self) -> &Self::Target {
214 self.as_str()
215 }
216}
217
218unsafe impl<S: Bos<str> + AsRef<str> + Clone + Serialize> RecordKeyType for Nsid<S> {
219 fn as_str(&self) -> &str {
220 self.as_str()
221 }
222}
223
224#[cfg(test)]
225mod tests {
226 use super::*;
227
228 #[test]
229 fn valid_nsids() {
230 assert!(Nsid::<&str>::new("com.example.foo").is_ok());
231 assert!(Nsid::<&str>::new("com.example.fooBar").is_ok());
232 assert!(Nsid::<&str>::new("com.long-domain.foo").is_ok());
233 assert!(Nsid::<&str>::new("a.b.c").is_ok());
234 assert!(Nsid::<&str>::new("a1.b2.c3").is_ok());
235 }
236
237 #[test]
238 fn minimum_segments() {
239 assert!(Nsid::<&str>::new("a.b.c").is_ok());
240 assert!(Nsid::<&str>::new("a.b").is_err());
241 assert!(Nsid::<&str>::new("a").is_err());
242 }
243
244 #[test]
245 fn domain_and_name_parsing() {
246 let nsid = Nsid::<&str>::new("com.example.fooBar").unwrap();
247 assert_eq!(nsid.domain_authority(), "com.example");
248 assert_eq!(nsid.name(), "fooBar");
249 }
250
251 #[test]
252 fn max_length() {
253 let s1 = format!("a{}a", "b".repeat(61));
254 let s2 = format!("c{}c", "d".repeat(61));
255 let s3 = format!("e{}e", "f".repeat(61));
256 let s4 = format!("g{}g", "h".repeat(61));
257 let s5 = format!("i{}i", "j".repeat(59));
258 let valid_317 = format!("{s1}.{s2}.{s3}.{s4}.{s5}");
259 assert_eq!(valid_317.len(), 317);
260 assert!(Nsid::<&str>::new(&valid_317).is_ok());
261
262 let s5_long = format!("i{}i", "j".repeat(60));
263 let too_long_318 = format!("{s1}.{s2}.{s3}.{s4}.{s5_long}");
264 assert_eq!(too_long_318.len(), 318);
265 assert!(Nsid::<&str>::new(&too_long_318).is_err());
266 }
267
268 #[test]
269 fn segment_length() {
270 let valid_63 = format!("{}.{}.foo", "a".repeat(63), "b".repeat(63));
271 assert!(Nsid::<&str>::new(&valid_63).is_ok());
272
273 let too_long_64 = format!("{}.b.foo", "a".repeat(64));
274 assert!(Nsid::<&str>::new(&too_long_64).is_err());
275 }
276
277 #[test]
278 fn first_segment_cannot_start_with_digit() {
279 assert!(Nsid::<&str>::new("com.example.foo").is_ok());
280 assert!(Nsid::<&str>::new("9com.example.foo").is_err());
281 }
282
283 #[test]
284 fn name_segment_rules() {
285 assert!(Nsid::<&str>::new("com.example.foo").is_ok());
286 assert!(Nsid::<&str>::new("com.example.fooBar123").is_ok());
287 assert!(Nsid::<&str>::new("com.example.9foo").is_err());
288 assert!(Nsid::<&str>::new("com.example.foo-bar").is_err());
289 }
290
291 #[test]
292 fn domain_segment_rules() {
293 assert!(Nsid::<&str>::new("foo-bar.example.baz").is_ok());
294 assert!(Nsid::<&str>::new("foo.bar-baz.qux").is_ok());
295 assert!(Nsid::<&str>::new("-foo.bar.baz").is_err());
296 assert!(Nsid::<&str>::new("foo-.bar.baz").is_err());
297 }
298
299 #[test]
300 fn case_sensitivity() {
301 assert!(Nsid::<&str>::new("com.example.fooBar").is_ok());
302 assert!(Nsid::<&str>::new("com.example.FooBar").is_ok());
303 }
304
305 #[test]
306 fn into_static() {
307 let n = Nsid::<&str>::new("com.example.foo").unwrap();
308 let owned: Nsid<SmolStr> = n.into_static();
309 assert_eq!(owned.as_str(), "com.example.foo");
310 }
311}