Skip to main content

reliakit_primitives/
text.rs

1use crate::{PrimitiveError, PrimitiveResult};
2use alloc::string::String;
3use core::{fmt, ops::Deref, str::FromStr};
4
5// ── Slug ─────────────────────────────────────────────────────────────────────
6
7/// URL-safe slug: lowercase ASCII alphanumeric characters and hyphens.
8///
9/// Rules: non-empty, only `[a-z0-9-]`, does not start or end with `-`,
10/// no consecutive `--`.
11#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
12pub struct Slug(String);
13
14impl Slug {
15    /// Creates a new `Slug`. Returns `Invalid` if the value violates slug rules.
16    pub fn new(value: impl Into<String>) -> PrimitiveResult<Self> {
17        let value = value.into();
18        if value.is_empty() {
19            return Err(PrimitiveError::Empty);
20        }
21        if !is_valid_slug(&value) {
22            return Err(PrimitiveError::Invalid {
23                message: "slug must be lowercase alphanumeric with hyphens, must not start or end with a hyphen, and must not contain consecutive hyphens",
24            });
25        }
26        Ok(Self(value))
27    }
28
29    /// Returns the underlying slug string slice.
30    pub fn as_str(&self) -> &str {
31        &self.0
32    }
33
34    /// Consumes the wrapper and returns the inner string.
35    pub fn into_inner(self) -> String {
36        self.0
37    }
38}
39
40fn is_valid_slug(s: &str) -> bool {
41    if s.starts_with('-') || s.ends_with('-') {
42        return false;
43    }
44    let mut prev = ' ';
45    for c in s.chars() {
46        if !matches!(c, 'a'..='z' | '0'..='9' | '-') {
47            return false;
48        }
49        if c == '-' && prev == '-' {
50            return false;
51        }
52        prev = c;
53    }
54    true
55}
56
57impl fmt::Display for Slug {
58    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
59        f.write_str(&self.0)
60    }
61}
62
63impl AsRef<str> for Slug {
64    fn as_ref(&self) -> &str {
65        self.as_str()
66    }
67}
68
69impl Deref for Slug {
70    type Target = str;
71
72    fn deref(&self) -> &Self::Target {
73        self.as_str()
74    }
75}
76
77impl TryFrom<&str> for Slug {
78    type Error = PrimitiveError;
79
80    fn try_from(value: &str) -> Result<Self, Self::Error> {
81        Self::new(value)
82    }
83}
84
85impl TryFrom<String> for Slug {
86    type Error = PrimitiveError;
87
88    fn try_from(value: String) -> Result<Self, Self::Error> {
89        Self::new(value)
90    }
91}
92
93impl FromStr for Slug {
94    type Err = PrimitiveError;
95
96    fn from_str(s: &str) -> Result<Self, Self::Err> {
97        Self::new(s)
98    }
99}
100
101impl PartialEq<str> for Slug {
102    fn eq(&self, other: &str) -> bool {
103        self.as_str() == other
104    }
105}
106
107impl PartialEq<&str> for Slug {
108    fn eq(&self, other: &&str) -> bool {
109        self.as_str() == *other
110    }
111}
112
113impl PartialEq<String> for Slug {
114    fn eq(&self, other: &String) -> bool {
115        self.as_str() == other.as_str()
116    }
117}
118
119impl PartialEq<&String> for Slug {
120    fn eq(&self, other: &&String) -> bool {
121        self.as_str() == other.as_str()
122    }
123}
124
125impl From<Slug> for String {
126    fn from(value: Slug) -> Self {
127        value.into_inner()
128    }
129}
130
131// ── Email ─────────────────────────────────────────────────────────────────────
132
133/// Email address with basic structural validation.
134///
135/// Checks: exactly one `@`, non-empty local part and domain, domain contains
136/// at least one `.`, domain labels are non-empty, no whitespace. Not a full
137/// RFC 5321 validator.
138#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
139pub struct Email(String);
140
141impl Email {
142    /// Creates a new `Email`. Returns `Invalid` if the value fails structural checks.
143    pub fn new(value: impl Into<String>) -> PrimitiveResult<Self> {
144        let value = value.into();
145        if value.is_empty() {
146            return Err(PrimitiveError::Empty);
147        }
148        if !is_valid_email(&value) {
149            return Err(PrimitiveError::Invalid {
150                message: "invalid email address",
151            });
152        }
153        Ok(Self(value))
154    }
155
156    /// Returns the underlying email string slice.
157    pub fn as_str(&self) -> &str {
158        &self.0
159    }
160
161    /// Consumes the wrapper and returns the inner string.
162    pub fn into_inner(self) -> String {
163        self.0
164    }
165
166    /// Returns the local part (before `@`).
167    pub fn local(&self) -> &str {
168        self.0.split('@').next().unwrap_or("")
169    }
170
171    /// Returns the domain part (after `@`).
172    pub fn domain(&self) -> &str {
173        self.0.split('@').nth(1).unwrap_or("")
174    }
175}
176
177fn is_valid_email(s: &str) -> bool {
178    if s.chars().any(|c| c.is_whitespace()) {
179        return false;
180    }
181    let at_count = s.chars().filter(|&c| c == '@').count();
182    if at_count != 1 {
183        return false;
184    }
185    let mut parts = s.splitn(2, '@');
186    let local = parts.next().unwrap_or("");
187    let domain = parts.next().unwrap_or("");
188    if local.is_empty() || domain.is_empty() {
189        return false;
190    }
191    if !domain.contains('.') || domain.split('.').any(str::is_empty) {
192        return false;
193    }
194    true
195}
196
197impl fmt::Display for Email {
198    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
199        f.write_str(&self.0)
200    }
201}
202
203impl AsRef<str> for Email {
204    fn as_ref(&self) -> &str {
205        self.as_str()
206    }
207}
208
209impl Deref for Email {
210    type Target = str;
211
212    fn deref(&self) -> &Self::Target {
213        self.as_str()
214    }
215}
216
217impl TryFrom<&str> for Email {
218    type Error = PrimitiveError;
219
220    fn try_from(value: &str) -> Result<Self, Self::Error> {
221        Self::new(value)
222    }
223}
224
225impl TryFrom<String> for Email {
226    type Error = PrimitiveError;
227
228    fn try_from(value: String) -> Result<Self, Self::Error> {
229        Self::new(value)
230    }
231}
232
233impl FromStr for Email {
234    type Err = PrimitiveError;
235
236    fn from_str(s: &str) -> Result<Self, Self::Err> {
237        Self::new(s)
238    }
239}
240
241impl PartialEq<str> for Email {
242    fn eq(&self, other: &str) -> bool {
243        self.as_str() == other
244    }
245}
246
247impl PartialEq<&str> for Email {
248    fn eq(&self, other: &&str) -> bool {
249        self.as_str() == *other
250    }
251}
252
253impl PartialEq<String> for Email {
254    fn eq(&self, other: &String) -> bool {
255        self.as_str() == other.as_str()
256    }
257}
258
259impl PartialEq<&String> for Email {
260    fn eq(&self, other: &&String) -> bool {
261        self.as_str() == other.as_str()
262    }
263}
264
265impl From<Email> for String {
266    fn from(value: Email) -> Self {
267        value.into_inner()
268    }
269}
270
271// ── HttpUrl ───────────────────────────────────────────────────────────────────
272
273/// HTTP or HTTPS URL with scheme validation.
274///
275/// Must start with `http://` or `https://` and have a non-empty host.
276#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
277pub struct HttpUrl(String);
278
279impl HttpUrl {
280    /// Creates a new `HttpUrl`. Returns `Invalid` if the scheme is missing or
281    /// the host is empty.
282    pub fn new(value: impl Into<String>) -> PrimitiveResult<Self> {
283        let value = value.into();
284        if value.is_empty() {
285            return Err(PrimitiveError::Empty);
286        }
287        let after_scheme = strip_http_scheme(&value).ok_or(PrimitiveError::Invalid {
288            message: "URL must start with http:// or https://",
289        })?;
290        let host = after_scheme.split(['/', '?', '#']).next().unwrap_or("");
291        if host.is_empty() || host.chars().all(|c| c.is_whitespace()) {
292            return Err(PrimitiveError::Invalid {
293                message: "URL must have a non-empty host",
294            });
295        }
296        if after_scheme.chars().any(|c| c.is_whitespace()) {
297            return Err(PrimitiveError::Invalid {
298                message: "URL must not contain whitespace",
299            });
300        }
301        Ok(Self(value))
302    }
303
304    /// Returns the underlying URL string slice.
305    pub fn as_str(&self) -> &str {
306        &self.0
307    }
308
309    /// Consumes the wrapper and returns the inner string.
310    pub fn into_inner(self) -> String {
311        self.0
312    }
313
314    /// Returns `true` if the URL uses `https`.
315    pub fn is_https(&self) -> bool {
316        self.0.len() >= 8 && self.0[..8].eq_ignore_ascii_case("https://")
317    }
318}
319
320fn strip_http_scheme(value: &str) -> Option<&str> {
321    if value
322        .as_bytes()
323        .get(..8)
324        .is_some_and(|prefix| prefix.eq_ignore_ascii_case(b"https://"))
325    {
326        Some(&value[8..])
327    } else if value
328        .as_bytes()
329        .get(..7)
330        .is_some_and(|prefix| prefix.eq_ignore_ascii_case(b"http://"))
331    {
332        Some(&value[7..])
333    } else {
334        None
335    }
336}
337
338impl fmt::Display for HttpUrl {
339    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
340        f.write_str(&self.0)
341    }
342}
343
344impl AsRef<str> for HttpUrl {
345    fn as_ref(&self) -> &str {
346        self.as_str()
347    }
348}
349
350impl Deref for HttpUrl {
351    type Target = str;
352
353    fn deref(&self) -> &Self::Target {
354        self.as_str()
355    }
356}
357
358impl TryFrom<&str> for HttpUrl {
359    type Error = PrimitiveError;
360
361    fn try_from(value: &str) -> Result<Self, Self::Error> {
362        Self::new(value)
363    }
364}
365
366impl TryFrom<String> for HttpUrl {
367    type Error = PrimitiveError;
368
369    fn try_from(value: String) -> Result<Self, Self::Error> {
370        Self::new(value)
371    }
372}
373
374impl FromStr for HttpUrl {
375    type Err = PrimitiveError;
376
377    fn from_str(s: &str) -> Result<Self, Self::Err> {
378        Self::new(s)
379    }
380}
381
382impl PartialEq<str> for HttpUrl {
383    fn eq(&self, other: &str) -> bool {
384        self.as_str() == other
385    }
386}
387
388impl PartialEq<&str> for HttpUrl {
389    fn eq(&self, other: &&str) -> bool {
390        self.as_str() == *other
391    }
392}
393
394impl PartialEq<String> for HttpUrl {
395    fn eq(&self, other: &String) -> bool {
396        self.as_str() == other.as_str()
397    }
398}
399
400impl PartialEq<&String> for HttpUrl {
401    fn eq(&self, other: &&String) -> bool {
402        self.as_str() == other.as_str()
403    }
404}
405
406impl From<HttpUrl> for String {
407    fn from(value: HttpUrl) -> Self {
408        value.into_inner()
409    }
410}
411
412// ── HexString ─────────────────────────────────────────────────────────────────
413
414/// String of valid hexadecimal characters, with optional `0x`/`0X` prefix.
415#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
416pub struct HexString(String);
417
418impl HexString {
419    /// Creates a new `HexString`. Returns `Invalid` if any character is not a
420    /// valid hex digit (after stripping an optional `0x`/`0X` prefix).
421    pub fn new(value: impl Into<String>) -> PrimitiveResult<Self> {
422        let value = value.into();
423        if value.is_empty() {
424            return Err(PrimitiveError::Empty);
425        }
426        let hex_part = value
427            .strip_prefix("0x")
428            .or_else(|| value.strip_prefix("0X"))
429            .unwrap_or(&value);
430        if hex_part.is_empty() {
431            return Err(PrimitiveError::Invalid {
432                message: "hex string must not be empty after prefix",
433            });
434        }
435        if !hex_part.chars().all(|c| c.is_ascii_hexdigit()) {
436            return Err(PrimitiveError::Invalid {
437                message: "hex string must contain only hexadecimal characters (0-9, a-f, A-F)",
438            });
439        }
440        Ok(Self(value))
441    }
442
443    /// Returns the underlying hex string slice.
444    pub fn as_str(&self) -> &str {
445        &self.0
446    }
447
448    /// Consumes the wrapper and returns the inner string.
449    pub fn into_inner(self) -> String {
450        self.0
451    }
452
453    /// Returns `true` if the value was stored with a `0x`/`0X` prefix.
454    pub fn has_prefix(&self) -> bool {
455        self.0.starts_with("0x") || self.0.starts_with("0X")
456    }
457
458    /// Returns only the hex digit characters, without any `0x`/`0X` prefix.
459    pub fn hex_digits(&self) -> &str {
460        self.0
461            .strip_prefix("0x")
462            .or_else(|| self.0.strip_prefix("0X"))
463            .unwrap_or(&self.0)
464    }
465}
466
467impl fmt::Display for HexString {
468    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
469        f.write_str(&self.0)
470    }
471}
472
473impl AsRef<str> for HexString {
474    fn as_ref(&self) -> &str {
475        self.as_str()
476    }
477}
478
479impl Deref for HexString {
480    type Target = str;
481
482    fn deref(&self) -> &Self::Target {
483        self.as_str()
484    }
485}
486
487impl TryFrom<&str> for HexString {
488    type Error = PrimitiveError;
489
490    fn try_from(value: &str) -> Result<Self, Self::Error> {
491        Self::new(value)
492    }
493}
494
495impl TryFrom<String> for HexString {
496    type Error = PrimitiveError;
497
498    fn try_from(value: String) -> Result<Self, Self::Error> {
499        Self::new(value)
500    }
501}
502
503impl FromStr for HexString {
504    type Err = PrimitiveError;
505
506    fn from_str(s: &str) -> Result<Self, Self::Err> {
507        Self::new(s)
508    }
509}
510
511impl PartialEq<str> for HexString {
512    fn eq(&self, other: &str) -> bool {
513        self.as_str() == other
514    }
515}
516
517impl PartialEq<&str> for HexString {
518    fn eq(&self, other: &&str) -> bool {
519        self.as_str() == *other
520    }
521}
522
523impl PartialEq<String> for HexString {
524    fn eq(&self, other: &String) -> bool {
525        self.as_str() == other.as_str()
526    }
527}
528
529impl PartialEq<&String> for HexString {
530    fn eq(&self, other: &&String) -> bool {
531        self.as_str() == other.as_str()
532    }
533}
534
535impl From<HexString> for String {
536    fn from(value: HexString) -> Self {
537        value.into_inner()
538    }
539}
540
541// ── Base64 ────────────────────────────────────────────────────────────────────
542
543/// Standard (RFC 4648) base64 string with required, correct padding.
544///
545/// Rules: non-empty, length is a multiple of `4`, every non-padding character is
546/// in the standard alphabet (`A-Z`, `a-z`, `0-9`, `+`, `/`), and `=` padding (at
547/// most two) appears only at the end. This is a *format* check; it does not
548/// decode the data. The URL-safe alphabet (`-`/`_`) is not accepted.
549#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
550pub struct Base64(String);
551
552impl Base64 {
553    /// Creates a new `Base64`. Returns an error if the value is empty or is not
554    /// well-formed standard base64 (see the type docs for the exact rules).
555    pub fn new(value: impl Into<String>) -> PrimitiveResult<Self> {
556        let value = value.into();
557        if value.is_empty() {
558            return Err(PrimitiveError::Empty);
559        }
560        let bytes = value.as_bytes();
561        if bytes.len() % 4 != 0 {
562            return Err(PrimitiveError::Invalid {
563                message: "base64 length must be a multiple of 4",
564            });
565        }
566        let pad = bytes.iter().rev().take_while(|&&b| b == b'=').count();
567        if pad > 2 {
568            return Err(PrimitiveError::Invalid {
569                message: "base64 has at most two padding characters",
570            });
571        }
572        // Every character before the padding must be in the standard alphabet;
573        // because `=` is not in the alphabet, this also rejects interior padding.
574        if !bytes[..bytes.len() - pad]
575            .iter()
576            .all(|&b| b.is_ascii_alphanumeric() || b == b'+' || b == b'/')
577        {
578            return Err(PrimitiveError::Invalid {
579                message: "base64 contains a character outside the standard alphabet",
580            });
581        }
582        Ok(Self(value))
583    }
584
585    /// Returns the underlying base64 string slice.
586    pub fn as_str(&self) -> &str {
587        &self.0
588    }
589
590    /// Consumes the wrapper and returns the inner string.
591    pub fn into_inner(self) -> String {
592        self.0
593    }
594
595    /// Returns `true` if the value carries `=` padding.
596    pub fn is_padded(&self) -> bool {
597        self.0.ends_with('=')
598    }
599
600    /// Returns the number of bytes this base64 string decodes to.
601    pub fn decoded_len(&self) -> usize {
602        let pad = self.0.bytes().rev().take_while(|&b| b == b'=').count();
603        self.0.len() / 4 * 3 - pad
604    }
605}
606
607impl fmt::Display for Base64 {
608    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
609        f.write_str(&self.0)
610    }
611}
612
613impl AsRef<str> for Base64 {
614    fn as_ref(&self) -> &str {
615        self.as_str()
616    }
617}
618
619impl Deref for Base64 {
620    type Target = str;
621
622    fn deref(&self) -> &Self::Target {
623        self.as_str()
624    }
625}
626
627impl TryFrom<&str> for Base64 {
628    type Error = PrimitiveError;
629
630    fn try_from(value: &str) -> Result<Self, Self::Error> {
631        Self::new(value)
632    }
633}
634
635impl TryFrom<String> for Base64 {
636    type Error = PrimitiveError;
637
638    fn try_from(value: String) -> Result<Self, Self::Error> {
639        Self::new(value)
640    }
641}
642
643impl FromStr for Base64 {
644    type Err = PrimitiveError;
645
646    fn from_str(s: &str) -> Result<Self, Self::Err> {
647        Self::new(s)
648    }
649}
650
651impl PartialEq<str> for Base64 {
652    fn eq(&self, other: &str) -> bool {
653        self.as_str() == other
654    }
655}
656
657impl PartialEq<&str> for Base64 {
658    fn eq(&self, other: &&str) -> bool {
659        self.as_str() == *other
660    }
661}
662
663impl PartialEq<String> for Base64 {
664    fn eq(&self, other: &String) -> bool {
665        self.as_str() == other.as_str()
666    }
667}
668
669impl PartialEq<&String> for Base64 {
670    fn eq(&self, other: &&String) -> bool {
671        self.as_str() == other.as_str()
672    }
673}
674
675impl From<Base64> for String {
676    fn from(value: Base64) -> Self {
677        value.into_inner()
678    }
679}
680
681// ── Base32 ────────────────────────────────────────────────────────────────────
682
683/// Standard (RFC 4648) base32 string with required, correct padding.
684///
685/// Rules: non-empty, length is a multiple of `8`, every non-padding character is
686/// in the standard alphabet (`A-Z`, `2-7`), and `=` padding appears only at the
687/// end in a valid amount (`1`, `3`, `4`, or `6` characters). This is a *format*
688/// check; it does not decode the data. Lowercase letters and the extended-hex
689/// alphabet are not accepted.
690#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
691pub struct Base32(String);
692
693impl Base32 {
694    /// Creates a new `Base32`. Returns an error if the value is empty or is not
695    /// well-formed standard base32 (see the type docs for the exact rules).
696    pub fn new(value: impl Into<String>) -> PrimitiveResult<Self> {
697        let value = value.into();
698        if value.is_empty() {
699            return Err(PrimitiveError::Empty);
700        }
701        let bytes = value.as_bytes();
702        if bytes.len() % 8 != 0 {
703            return Err(PrimitiveError::Invalid {
704                message: "base32 length must be a multiple of 8",
705            });
706        }
707        let pad = bytes.iter().rev().take_while(|&&b| b == b'=').count();
708        // Valid trailing-pad counts for base32 are 0, 1, 3, 4, or 6.
709        if !matches!(pad, 0 | 1 | 3 | 4 | 6) {
710            return Err(PrimitiveError::Invalid {
711                message: "base32 has an invalid amount of padding",
712            });
713        }
714        // Every character before the padding must be in the standard alphabet;
715        // because `=` is not in the alphabet, this also rejects interior padding.
716        if !bytes[..bytes.len() - pad]
717            .iter()
718            .all(|&b| b.is_ascii_uppercase() || (b'2'..=b'7').contains(&b))
719        {
720            return Err(PrimitiveError::Invalid {
721                message: "base32 contains a character outside the standard alphabet",
722            });
723        }
724        Ok(Self(value))
725    }
726
727    /// Returns the underlying base32 string slice.
728    pub fn as_str(&self) -> &str {
729        &self.0
730    }
731
732    /// Consumes the wrapper and returns the inner string.
733    pub fn into_inner(self) -> String {
734        self.0
735    }
736
737    /// Returns `true` if the value carries `=` padding.
738    pub fn is_padded(&self) -> bool {
739        self.0.ends_with('=')
740    }
741
742    /// Returns the number of bytes this base32 string decodes to.
743    pub fn decoded_len(&self) -> usize {
744        let pad = self.0.bytes().rev().take_while(|&b| b == b'=').count();
745        let missing = match pad {
746            6 => 4,
747            4 => 3,
748            3 => 2,
749            1 => 1,
750            _ => 0,
751        };
752        self.0.len() / 8 * 5 - missing
753    }
754}
755
756impl fmt::Display for Base32 {
757    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
758        f.write_str(&self.0)
759    }
760}
761
762impl AsRef<str> for Base32 {
763    fn as_ref(&self) -> &str {
764        self.as_str()
765    }
766}
767
768impl Deref for Base32 {
769    type Target = str;
770
771    fn deref(&self) -> &Self::Target {
772        self.as_str()
773    }
774}
775
776impl TryFrom<&str> for Base32 {
777    type Error = PrimitiveError;
778
779    fn try_from(value: &str) -> Result<Self, Self::Error> {
780        Self::new(value)
781    }
782}
783
784impl TryFrom<String> for Base32 {
785    type Error = PrimitiveError;
786
787    fn try_from(value: String) -> Result<Self, Self::Error> {
788        Self::new(value)
789    }
790}
791
792impl FromStr for Base32 {
793    type Err = PrimitiveError;
794
795    fn from_str(s: &str) -> Result<Self, Self::Err> {
796        Self::new(s)
797    }
798}
799
800impl PartialEq<str> for Base32 {
801    fn eq(&self, other: &str) -> bool {
802        self.as_str() == other
803    }
804}
805
806impl PartialEq<&str> for Base32 {
807    fn eq(&self, other: &&str) -> bool {
808        self.as_str() == *other
809    }
810}
811
812impl PartialEq<String> for Base32 {
813    fn eq(&self, other: &String) -> bool {
814        self.as_str() == other.as_str()
815    }
816}
817
818impl PartialEq<&String> for Base32 {
819    fn eq(&self, other: &&String) -> bool {
820        self.as_str() == other.as_str()
821    }
822}
823
824impl From<Base32> for String {
825    fn from(value: Base32) -> Self {
826        value.into_inner()
827    }
828}
829
830// ── Identifier ────────────────────────────────────────────────────────────────
831
832/// A conservative ASCII identifier: a letter or `_`, then letters, digits, or
833/// `_`.
834///
835/// Rules: non-empty, the first character is `[A-Za-z_]`, and every remaining
836/// character is `[A-Za-z0-9_]`. Useful for handles, keys, and machine-generated
837/// names that must be safe across many systems.
838#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
839pub struct Identifier(String);
840
841impl Identifier {
842    /// Creates a new `Identifier`. Returns an error if the value is empty or
843    /// contains a character not allowed at its position (see the type docs).
844    pub fn new(value: impl Into<String>) -> PrimitiveResult<Self> {
845        let value = value.into();
846        let mut chars = value.chars();
847        match chars.next() {
848            None => return Err(PrimitiveError::Empty),
849            Some(first) if !(first.is_ascii_alphabetic() || first == '_') => {
850                return Err(PrimitiveError::Invalid {
851                    message: "identifier must start with an ASCII letter or underscore",
852                });
853            }
854            Some(_) => {}
855        }
856        if !chars.all(|c| c.is_ascii_alphanumeric() || c == '_') {
857            return Err(PrimitiveError::Invalid {
858                message: "identifier may contain only ASCII letters, digits, and underscores",
859            });
860        }
861        Ok(Self(value))
862    }
863
864    /// Returns the underlying identifier string slice.
865    pub fn as_str(&self) -> &str {
866        &self.0
867    }
868
869    /// Consumes the wrapper and returns the inner string.
870    pub fn into_inner(self) -> String {
871        self.0
872    }
873}
874
875impl fmt::Display for Identifier {
876    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
877        f.write_str(&self.0)
878    }
879}
880
881impl AsRef<str> for Identifier {
882    fn as_ref(&self) -> &str {
883        self.as_str()
884    }
885}
886
887impl Deref for Identifier {
888    type Target = str;
889
890    fn deref(&self) -> &Self::Target {
891        self.as_str()
892    }
893}
894
895impl TryFrom<&str> for Identifier {
896    type Error = PrimitiveError;
897
898    fn try_from(value: &str) -> Result<Self, Self::Error> {
899        Self::new(value)
900    }
901}
902
903impl TryFrom<String> for Identifier {
904    type Error = PrimitiveError;
905
906    fn try_from(value: String) -> Result<Self, Self::Error> {
907        Self::new(value)
908    }
909}
910
911impl FromStr for Identifier {
912    type Err = PrimitiveError;
913
914    fn from_str(s: &str) -> Result<Self, Self::Err> {
915        Self::new(s)
916    }
917}
918
919impl PartialEq<str> for Identifier {
920    fn eq(&self, other: &str) -> bool {
921        self.as_str() == other
922    }
923}
924
925impl PartialEq<&str> for Identifier {
926    fn eq(&self, other: &&str) -> bool {
927        self.as_str() == *other
928    }
929}
930
931impl PartialEq<String> for Identifier {
932    fn eq(&self, other: &String) -> bool {
933        self.as_str() == other.as_str()
934    }
935}
936
937impl PartialEq<&String> for Identifier {
938    fn eq(&self, other: &&String) -> bool {
939        self.as_str() == other.as_str()
940    }
941}
942
943impl From<Identifier> for String {
944    fn from(value: Identifier) -> Self {
945        value.into_inner()
946    }
947}
948
949// ── Hostname ──────────────────────────────────────────────────────────────────
950
951/// A DNS hostname following the RFC 1123 rules.
952///
953/// Rules: non-empty, at most 253 characters total, split into dot-separated
954/// labels where each label is 1–63 characters of `[A-Za-z0-9-]` and does not
955/// start or end with a hyphen. Empty labels (a leading, trailing, or doubled
956/// dot) are rejected. The check is case-preserving and does not resolve the name.
957#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
958pub struct Hostname(String);
959
960impl Hostname {
961    /// Creates a new `Hostname`. Returns an error if the value is empty, too
962    /// long, or has a label that violates the RFC 1123 rules (see the type docs).
963    pub fn new(value: impl Into<String>) -> PrimitiveResult<Self> {
964        let value = value.into();
965        if value.is_empty() {
966            return Err(PrimitiveError::Empty);
967        }
968        if value.len() > 253 {
969            return Err(PrimitiveError::TooLong {
970                max: 253,
971                actual: value.len(),
972            });
973        }
974        for label in value.split('.') {
975            if label.is_empty() {
976                return Err(PrimitiveError::Invalid {
977                    message: "hostname label must not be empty",
978                });
979            }
980            if label.len() > 63 {
981                return Err(PrimitiveError::Invalid {
982                    message: "hostname label must not exceed 63 characters",
983                });
984            }
985            if label.starts_with('-') || label.ends_with('-') {
986                return Err(PrimitiveError::Invalid {
987                    message: "hostname label must not start or end with a hyphen",
988                });
989            }
990            if !label
991                .bytes()
992                .all(|b| b.is_ascii_alphanumeric() || b == b'-')
993            {
994                return Err(PrimitiveError::Invalid {
995                    message: "hostname label may contain only letters, digits, and hyphens",
996                });
997            }
998        }
999        Ok(Self(value))
1000    }
1001
1002    /// Returns the underlying hostname string slice.
1003    pub fn as_str(&self) -> &str {
1004        &self.0
1005    }
1006
1007    /// Consumes the wrapper and returns the inner string.
1008    pub fn into_inner(self) -> String {
1009        self.0
1010    }
1011
1012    /// Iterates over the dot-separated labels, from left to right.
1013    pub fn labels(&self) -> impl Iterator<Item = &str> + '_ {
1014        self.0.split('.')
1015    }
1016}
1017
1018impl fmt::Display for Hostname {
1019    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1020        f.write_str(&self.0)
1021    }
1022}
1023
1024impl AsRef<str> for Hostname {
1025    fn as_ref(&self) -> &str {
1026        self.as_str()
1027    }
1028}
1029
1030impl Deref for Hostname {
1031    type Target = str;
1032
1033    fn deref(&self) -> &Self::Target {
1034        self.as_str()
1035    }
1036}
1037
1038impl TryFrom<&str> for Hostname {
1039    type Error = PrimitiveError;
1040
1041    fn try_from(value: &str) -> Result<Self, Self::Error> {
1042        Self::new(value)
1043    }
1044}
1045
1046impl TryFrom<String> for Hostname {
1047    type Error = PrimitiveError;
1048
1049    fn try_from(value: String) -> Result<Self, Self::Error> {
1050        Self::new(value)
1051    }
1052}
1053
1054impl FromStr for Hostname {
1055    type Err = PrimitiveError;
1056
1057    fn from_str(s: &str) -> Result<Self, Self::Err> {
1058        Self::new(s)
1059    }
1060}
1061
1062impl PartialEq<str> for Hostname {
1063    fn eq(&self, other: &str) -> bool {
1064        self.as_str() == other
1065    }
1066}
1067
1068impl PartialEq<&str> for Hostname {
1069    fn eq(&self, other: &&str) -> bool {
1070        self.as_str() == *other
1071    }
1072}
1073
1074impl PartialEq<String> for Hostname {
1075    fn eq(&self, other: &String) -> bool {
1076        self.as_str() == other.as_str()
1077    }
1078}
1079
1080impl PartialEq<&String> for Hostname {
1081    fn eq(&self, other: &&String) -> bool {
1082        self.as_str() == other.as_str()
1083    }
1084}
1085
1086impl From<Hostname> for String {
1087    fn from(value: Hostname) -> Self {
1088        value.into_inner()
1089    }
1090}
1091
1092// ── Tests ─────────────────────────────────────────────────────────────────────
1093
1094#[cfg(test)]
1095mod tests {
1096    use super::{Base32, Base64, Email, HexString, Hostname, HttpUrl, Identifier, Slug};
1097    use crate::{PrimitiveError, PrimitiveErrorKind};
1098
1099    // Slug
1100    #[test]
1101    fn slug_accepts_valid() {
1102        assert_eq!(Slug::new("my-service").unwrap().as_str(), "my-service");
1103        assert_eq!(Slug::new("api-v2").unwrap().as_str(), "api-v2");
1104        assert_eq!(Slug::new("user123").unwrap().as_str(), "user123");
1105    }
1106
1107    #[test]
1108    fn slug_rejects_empty() {
1109        assert_eq!(Slug::new("").unwrap_err(), PrimitiveError::Empty);
1110    }
1111
1112    #[test]
1113    fn slug_rejects_uppercase() {
1114        assert!(Slug::new("MySlug").is_err());
1115    }
1116
1117    #[test]
1118    fn slug_rejects_leading_hyphen() {
1119        assert!(Slug::new("-bad").is_err());
1120    }
1121
1122    #[test]
1123    fn slug_rejects_trailing_hyphen() {
1124        assert!(Slug::new("bad-").is_err());
1125    }
1126
1127    #[test]
1128    fn slug_rejects_consecutive_hyphens() {
1129        assert!(Slug::new("bad--slug").is_err());
1130    }
1131
1132    #[test]
1133    fn slug_rejects_spaces() {
1134        assert!(Slug::new("has space").is_err());
1135    }
1136
1137    #[test]
1138    fn slug_display() {
1139        use alloc::string::ToString;
1140        assert_eq!(Slug::new("hello").unwrap().to_string(), "hello");
1141    }
1142
1143    #[test]
1144    fn slug_deref() {
1145        let s = Slug::new("hello").unwrap();
1146        assert_eq!(&*s, "hello");
1147    }
1148
1149    #[test]
1150    fn slug_from_str_and_string_comparisons() {
1151        let slug = "hello".parse::<Slug>().unwrap();
1152        let owned = String::from("hello");
1153        assert_eq!(slug, "hello");
1154        assert_eq!(slug, owned);
1155        assert!("Hello".parse::<Slug>().is_err());
1156    }
1157
1158    #[test]
1159    fn slug_converts_into_string() {
1160        let slug = Slug::new("hello").unwrap();
1161        let inner = String::from(slug);
1162        assert_eq!(inner, "hello");
1163    }
1164
1165    // Email
1166    #[test]
1167    fn email_accepts_valid() {
1168        let e = Email::new("user@example.com").unwrap();
1169        assert_eq!(e.local(), "user");
1170        assert_eq!(e.domain(), "example.com");
1171    }
1172
1173    #[test]
1174    fn email_rejects_empty() {
1175        assert_eq!(Email::new("").unwrap_err(), PrimitiveError::Empty);
1176    }
1177
1178    #[test]
1179    fn email_rejects_missing_at() {
1180        assert!(Email::new("nodomain").is_err());
1181    }
1182
1183    #[test]
1184    fn email_rejects_multiple_at() {
1185        assert!(Email::new("a@b@c.com").is_err());
1186    }
1187
1188    #[test]
1189    fn email_rejects_no_dot_in_domain() {
1190        assert!(Email::new("user@nodot").is_err());
1191    }
1192
1193    #[test]
1194    fn email_rejects_empty_domain_labels() {
1195        assert!(Email::new("user@example..com").is_err());
1196        assert!(Email::new("user@.example.com").is_err());
1197        assert!(Email::new("user@example.com.").is_err());
1198    }
1199
1200    #[test]
1201    fn email_rejects_spaces() {
1202        assert!(Email::new("us er@example.com").is_err());
1203    }
1204
1205    #[test]
1206    fn email_rejects_tab() {
1207        assert!(Email::new("user\t@example.com").is_err());
1208    }
1209
1210    #[test]
1211    fn email_rejects_newline() {
1212        assert!(Email::new("user\n@example.com").is_err());
1213    }
1214
1215    #[test]
1216    fn url_rejects_whitespace_host() {
1217        assert!(HttpUrl::new("http://   ").is_err());
1218    }
1219
1220    #[test]
1221    fn url_rejects_whitespace_in_path() {
1222        assert!(HttpUrl::new("https://ex ample.com").is_err());
1223    }
1224
1225    #[test]
1226    fn email_display() {
1227        use alloc::string::ToString;
1228        assert_eq!(Email::new("a@b.com").unwrap().to_string(), "a@b.com");
1229    }
1230
1231    #[test]
1232    fn email_from_str_and_string_comparisons() {
1233        let email = "a@b.com".parse::<Email>().unwrap();
1234        let owned = String::from("a@b.com");
1235        assert_eq!(email, "a@b.com");
1236        assert_eq!(email, owned);
1237        assert!("bad".parse::<Email>().is_err());
1238    }
1239
1240    #[test]
1241    fn email_string_ergonomics() {
1242        let email = Email::try_from(String::from("a@b.com")).unwrap();
1243        let borrowed: &str = email.as_ref();
1244        assert_eq!(borrowed, "a@b.com");
1245        assert_eq!(&*email, "a@b.com");
1246
1247        let inner = String::from(email);
1248        assert_eq!(inner, "a@b.com");
1249    }
1250
1251    // HttpUrl
1252    #[test]
1253    fn url_accepts_http() {
1254        let u = HttpUrl::new("http://example.com").unwrap();
1255        assert!(!u.is_https());
1256    }
1257
1258    #[test]
1259    fn url_accepts_https() {
1260        let u = HttpUrl::new("https://example.com/path").unwrap();
1261        assert!(u.is_https());
1262    }
1263
1264    #[test]
1265    fn url_rejects_empty() {
1266        assert_eq!(HttpUrl::new("").unwrap_err(), PrimitiveError::Empty);
1267    }
1268
1269    #[test]
1270    fn url_rejects_missing_scheme() {
1271        assert!(HttpUrl::new("ftp://example.com").is_err());
1272    }
1273
1274    #[test]
1275    fn url_rejects_empty_host() {
1276        assert!(HttpUrl::new("https://").is_err());
1277    }
1278
1279    #[test]
1280    fn url_rejects_missing_host_before_path() {
1281        assert!(HttpUrl::new("https:///path").is_err());
1282    }
1283
1284    #[test]
1285    fn url_display() {
1286        use alloc::string::ToString;
1287        let u = HttpUrl::new("https://example.com").unwrap();
1288        assert_eq!(u.to_string(), "https://example.com");
1289    }
1290
1291    #[test]
1292    fn url_is_https_uppercase_scheme() {
1293        let u = HttpUrl::new("HTTPS://example.com").unwrap();
1294        assert!(u.is_https());
1295    }
1296
1297    #[test]
1298    fn url_accepts_uppercase_http_scheme() {
1299        let u = HttpUrl::new("HTTP://example.com").unwrap();
1300        assert!(!u.is_https());
1301    }
1302
1303    #[test]
1304    fn url_is_http_not_https() {
1305        let u = HttpUrl::new("http://example.com").unwrap();
1306        assert!(!u.is_https());
1307    }
1308
1309    #[test]
1310    fn url_from_str_and_string_comparisons() {
1311        let url = "https://example.com".parse::<HttpUrl>().unwrap();
1312        let owned = String::from("https://example.com");
1313        assert_eq!(url, "https://example.com");
1314        assert_eq!(url, owned);
1315        assert!("ftp://example.com".parse::<HttpUrl>().is_err());
1316    }
1317
1318    #[test]
1319    fn url_string_ergonomics() {
1320        let url = HttpUrl::try_from(String::from("https://example.com")).unwrap();
1321        let borrowed: &str = url.as_ref();
1322        assert_eq!(borrowed, "https://example.com");
1323        assert_eq!(&*url, "https://example.com");
1324
1325        let inner = String::from(url);
1326        assert_eq!(inner, "https://example.com");
1327    }
1328
1329    // HexString
1330    #[test]
1331    fn hex_accepts_plain() {
1332        let h = HexString::new("deadbeef").unwrap();
1333        assert_eq!(h.hex_digits(), "deadbeef");
1334        assert!(!h.has_prefix());
1335    }
1336
1337    #[test]
1338    fn hex_accepts_prefixed() {
1339        let h = HexString::new("0xdeadbeef").unwrap();
1340        assert_eq!(h.hex_digits(), "deadbeef");
1341        assert!(h.has_prefix());
1342    }
1343
1344    #[test]
1345    fn hex_accepts_uppercase() {
1346        assert!(HexString::new("DEADBEEF").is_ok());
1347    }
1348
1349    #[test]
1350    fn hex_rejects_empty() {
1351        assert_eq!(HexString::new("").unwrap_err(), PrimitiveError::Empty);
1352    }
1353
1354    #[test]
1355    fn hex_rejects_prefix_only() {
1356        assert!(HexString::new("0x").is_err());
1357    }
1358
1359    #[test]
1360    fn hex_rejects_invalid_chars() {
1361        assert!(HexString::new("xyz").is_err());
1362    }
1363
1364    #[test]
1365    fn hex_display() {
1366        use alloc::string::ToString;
1367        assert_eq!(HexString::new("ff00").unwrap().to_string(), "ff00");
1368    }
1369
1370    #[test]
1371    fn hex_from_str_and_string_comparisons() {
1372        let hex = "ff00".parse::<HexString>().unwrap();
1373        let owned = String::from("ff00");
1374        assert_eq!(hex, "ff00");
1375        assert_eq!(hex, owned);
1376        assert!("xyz".parse::<HexString>().is_err());
1377    }
1378
1379    #[test]
1380    fn hex_string_ergonomics() {
1381        let hex = HexString::try_from(String::from("ff00")).unwrap();
1382        let borrowed: &str = hex.as_ref();
1383        assert_eq!(borrowed, "ff00");
1384        assert_eq!(&*hex, "ff00");
1385
1386        let inner = String::from(hex);
1387        assert_eq!(inner, "ff00");
1388    }
1389
1390    #[test]
1391    fn base64_accepts_valid() {
1392        assert_eq!(Base64::new("aGVsbG8=").unwrap().as_str(), "aGVsbG8=");
1393        assert!(Base64::new("YWJjZA==").is_ok()); // two pads
1394        assert!(Base64::new("YWJjZGU+").is_ok()); // '+' and no pad
1395        assert!(Base64::new("ab/+ZZ90").is_ok());
1396    }
1397
1398    #[test]
1399    fn base64_rejects_bad() {
1400        assert_eq!(
1401            Base64::new("").unwrap_err().kind(),
1402            PrimitiveErrorKind::Empty
1403        );
1404        assert_eq!(
1405            Base64::new("aGVsbG8").unwrap_err().kind(), // not a multiple of 4
1406            PrimitiveErrorKind::InvalidFormat
1407        );
1408        assert!(Base64::new("ab-_ZZ90").is_err()); // url-safe alphabet
1409        assert!(Base64::new("ab=cZZ90").is_err()); // interior padding
1410        assert!(Base64::new("ab======").is_err()); // too much padding
1411    }
1412
1413    #[test]
1414    fn base64_padding_and_decoded_len() {
1415        let b = Base64::new("aGVsbG8=").unwrap(); // "hello" -> 5 bytes
1416        assert!(b.is_padded());
1417        assert_eq!(b.decoded_len(), 5);
1418        let b = Base64::new("YWJjZA==").unwrap(); // "abcd" -> 4 bytes
1419        assert_eq!(b.decoded_len(), 4);
1420        let b = Base64::new("YWJjZGZn").unwrap(); // 6 bytes, no pad
1421        assert!(!b.is_padded());
1422        assert_eq!(b.decoded_len(), 6);
1423    }
1424
1425    // Base32 (RFC 4648 test vectors: f/fo/foo/foob/fooba/foobar)
1426    #[test]
1427    fn base32_accepts_valid() {
1428        assert_eq!(Base32::new("MZXW6YTB").unwrap().as_str(), "MZXW6YTB"); // "fooba"
1429        assert!(Base32::new("MY======").is_ok()); // "f"
1430        assert!(Base32::new("MZXQ====").is_ok()); // "fo"
1431        assert!(Base32::new("MZXW6===").is_ok()); // "foo"
1432        assert!(Base32::new("MZXW6YQ=").is_ok()); // "foob"
1433        assert!(Base32::new("MZXW6YTBOI======").is_ok()); // "foobar"
1434    }
1435
1436    #[test]
1437    fn base32_rejects_bad() {
1438        assert_eq!(
1439            Base32::new("").unwrap_err().kind(),
1440            PrimitiveErrorKind::Empty
1441        );
1442        assert_eq!(
1443            Base32::new("MZXW6YT").unwrap_err().kind(), // not a multiple of 8
1444            PrimitiveErrorKind::InvalidFormat
1445        );
1446        assert!(Base32::new("mzxw6ytb").is_err()); // lowercase not accepted
1447        assert!(Base32::new("MZXW6YT1").is_err()); // '1' outside the 2-7 digits
1448        assert!(Base32::new("MZXW6Y==").is_err()); // invalid padding amount (2)
1449        assert!(Base32::new("M=XW6YTB").is_err()); // interior padding
1450    }
1451
1452    #[test]
1453    fn base32_padding_and_decoded_len() {
1454        let b = Base32::new("MY======").unwrap(); // "f" -> 1 byte
1455        assert!(b.is_padded());
1456        assert_eq!(b.decoded_len(), 1);
1457        assert_eq!(Base32::new("MZXQ====").unwrap().decoded_len(), 2); // "fo"
1458        assert_eq!(Base32::new("MZXW6===").unwrap().decoded_len(), 3); // "foo"
1459        assert_eq!(Base32::new("MZXW6YQ=").unwrap().decoded_len(), 4); // "foob"
1460        let full = Base32::new("MZXW6YTB").unwrap(); // "fooba" -> 5 bytes
1461        assert!(!full.is_padded());
1462        assert_eq!(full.decoded_len(), 5);
1463        assert_eq!(Base32::new("MZXW6YTBOI======").unwrap().decoded_len(), 6); // "foobar"
1464    }
1465
1466    #[test]
1467    fn identifier_accepts_valid() {
1468        assert_eq!(Identifier::new("user_id").unwrap().as_str(), "user_id");
1469        assert!(Identifier::new("_private").is_ok());
1470        assert!(Identifier::new("A1").is_ok());
1471        assert!(Identifier::new("x").is_ok());
1472    }
1473
1474    #[test]
1475    fn identifier_rejects_bad() {
1476        assert_eq!(
1477            Identifier::new("").unwrap_err().kind(),
1478            PrimitiveErrorKind::Empty
1479        );
1480        assert!(Identifier::new("3bad").is_err()); // starts with digit
1481        assert!(Identifier::new("has space").is_err());
1482        assert!(Identifier::new("dash-no").is_err());
1483        assert!(Identifier::new("café").is_err()); // non-ascii
1484    }
1485
1486    #[test]
1487    fn hostname_accepts_valid() {
1488        assert_eq!(
1489            Hostname::new("api.example.com").unwrap().as_str(),
1490            "api.example.com"
1491        );
1492        assert!(Hostname::new("localhost").is_ok());
1493        assert!(Hostname::new("a-b.c-d.example").is_ok());
1494        let h = Hostname::new("api.example.com").unwrap();
1495        let labels: alloc::vec::Vec<&str> = h.labels().collect();
1496        assert_eq!(labels, ["api", "example", "com"]);
1497    }
1498
1499    #[test]
1500    fn hostname_rejects_bad() {
1501        assert_eq!(
1502            Hostname::new("").unwrap_err().kind(),
1503            PrimitiveErrorKind::Empty
1504        );
1505        assert!(Hostname::new("-bad.com").is_err()); // leading hyphen
1506        assert!(Hostname::new("bad-.com").is_err()); // trailing hyphen
1507        assert!(Hostname::new("a..b").is_err()); // empty label
1508        assert!(Hostname::new(".leading").is_err());
1509        assert!(Hostname::new("trailing.").is_err());
1510        assert!(Hostname::new("under_score.com").is_err()); // underscore not allowed
1511        assert!(Hostname::new(String::from("a").repeat(64)).is_err());
1512        let too_long = alloc::format!("{}.com", String::from("a").repeat(252));
1513        assert!(Hostname::new(too_long).is_err());
1514    }
1515
1516    // The owned/reference comparisons below intentionally exercise the
1517    // `PartialEq<String>` and `PartialEq<&String>` impls, which `cmp_owned` and
1518    // `op_ref` would otherwise rewrite away.
1519    #[test]
1520    #[allow(clippy::cmp_owned, clippy::op_ref)]
1521    fn base64_conversions_and_traits() {
1522        let from_str: Base64 = "YWJj".parse().unwrap();
1523        let try_ref = Base64::try_from("YWJj").unwrap();
1524        let try_owned = Base64::try_from(String::from("YWJj")).unwrap();
1525        assert_eq!(from_str, try_ref);
1526        assert_eq!(try_ref, try_owned);
1527
1528        assert_eq!(try_ref.to_string(), "YWJj"); // Display
1529        let as_ref: &str = try_ref.as_ref(); // AsRef
1530        assert_eq!(as_ref, "YWJj");
1531        assert_eq!(&*try_ref, "YWJj"); // Deref
1532        assert!(try_ref == "YWJj"); // PartialEq<&str>
1533        assert!(try_ref == *"YWJj"); // PartialEq<str>
1534        assert!(try_ref == String::from("YWJj")); // PartialEq<String>
1535        assert!(try_ref == &String::from("YWJj")); // PartialEq<&String>
1536        assert_eq!(String::from(try_owned), "YWJj"); // From<Base64> for String
1537    }
1538
1539    #[test]
1540    #[allow(clippy::cmp_owned, clippy::op_ref)]
1541    fn identifier_conversions_and_traits() {
1542        let from_str: Identifier = "user_id".parse().unwrap();
1543        let try_ref = Identifier::try_from("user_id").unwrap();
1544        let try_owned = Identifier::try_from(String::from("user_id")).unwrap();
1545        assert_eq!(from_str, try_ref);
1546        assert_eq!(try_ref, try_owned);
1547
1548        assert_eq!(try_ref.to_string(), "user_id");
1549        let as_ref: &str = try_ref.as_ref();
1550        assert_eq!(as_ref, "user_id");
1551        assert_eq!(&*try_ref, "user_id");
1552        assert!(try_ref == "user_id");
1553        assert!(try_ref == *"user_id");
1554        assert!(try_ref == String::from("user_id"));
1555        assert!(try_ref == &String::from("user_id"));
1556        assert_eq!(String::from(try_owned), "user_id");
1557    }
1558
1559    #[test]
1560    #[allow(clippy::cmp_owned, clippy::op_ref)]
1561    fn hostname_conversions_and_traits() {
1562        let from_str: Hostname = "example.com".parse().unwrap();
1563        let try_ref = Hostname::try_from("example.com").unwrap();
1564        let try_owned = Hostname::try_from(String::from("example.com")).unwrap();
1565        assert_eq!(from_str, try_ref);
1566        assert_eq!(try_ref, try_owned);
1567
1568        assert_eq!(try_ref.to_string(), "example.com");
1569        let as_ref: &str = try_ref.as_ref();
1570        assert_eq!(as_ref, "example.com");
1571        assert_eq!(&*try_ref, "example.com");
1572        assert!(try_ref == "example.com");
1573        assert!(try_ref == *"example.com");
1574        assert!(try_ref == String::from("example.com"));
1575        assert!(try_ref == &String::from("example.com"));
1576        assert_eq!(String::from(try_owned), "example.com");
1577    }
1578}