1use crate::{PrimitiveError, PrimitiveResult};
2use alloc::string::String;
3use core::{fmt, ops::Deref, str::FromStr};
4
5#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
12pub struct Slug(String);
13
14impl Slug {
15 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 pub fn as_str(&self) -> &str {
31 &self.0
32 }
33
34 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#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
139pub struct Email(String);
140
141impl Email {
142 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 pub fn as_str(&self) -> &str {
158 &self.0
159 }
160
161 pub fn into_inner(self) -> String {
163 self.0
164 }
165
166 pub fn local(&self) -> &str {
168 self.0.split('@').next().unwrap_or("")
169 }
170
171 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#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
277pub struct HttpUrl(String);
278
279impl HttpUrl {
280 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 pub fn as_str(&self) -> &str {
306 &self.0
307 }
308
309 pub fn into_inner(self) -> String {
311 self.0
312 }
313
314 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#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
416pub struct HexString(String);
417
418impl HexString {
419 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 pub fn as_str(&self) -> &str {
445 &self.0
446 }
447
448 pub fn into_inner(self) -> String {
450 self.0
451 }
452
453 pub fn has_prefix(&self) -> bool {
455 self.0.starts_with("0x") || self.0.starts_with("0X")
456 }
457
458 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#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
550pub struct Base64(String);
551
552impl Base64 {
553 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 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 pub fn as_str(&self) -> &str {
587 &self.0
588 }
589
590 pub fn into_inner(self) -> String {
592 self.0
593 }
594
595 pub fn is_padded(&self) -> bool {
597 self.0.ends_with('=')
598 }
599
600 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#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
691pub struct Base32(String);
692
693impl Base32 {
694 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 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 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 pub fn as_str(&self) -> &str {
729 &self.0
730 }
731
732 pub fn into_inner(self) -> String {
734 self.0
735 }
736
737 pub fn is_padded(&self) -> bool {
739 self.0.ends_with('=')
740 }
741
742 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#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
839pub struct Identifier(String);
840
841impl Identifier {
842 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 pub fn as_str(&self) -> &str {
866 &self.0
867 }
868
869 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#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
958pub struct Hostname(String);
959
960impl Hostname {
961 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 pub fn as_str(&self) -> &str {
1004 &self.0
1005 }
1006
1007 pub fn into_inner(self) -> String {
1009 self.0
1010 }
1011
1012 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#[cfg(test)]
1095mod tests {
1096 use super::{Base32, Base64, Email, HexString, Hostname, HttpUrl, Identifier, Slug};
1097 use crate::{PrimitiveError, PrimitiveErrorKind};
1098
1099 #[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 #[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 #[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 #[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()); assert!(Base64::new("YWJjZGU+").is_ok()); 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(), PrimitiveErrorKind::InvalidFormat
1407 );
1408 assert!(Base64::new("ab-_ZZ90").is_err()); assert!(Base64::new("ab=cZZ90").is_err()); assert!(Base64::new("ab======").is_err()); }
1412
1413 #[test]
1414 fn base64_padding_and_decoded_len() {
1415 let b = Base64::new("aGVsbG8=").unwrap(); assert!(b.is_padded());
1417 assert_eq!(b.decoded_len(), 5);
1418 let b = Base64::new("YWJjZA==").unwrap(); assert_eq!(b.decoded_len(), 4);
1420 let b = Base64::new("YWJjZGZn").unwrap(); assert!(!b.is_padded());
1422 assert_eq!(b.decoded_len(), 6);
1423 }
1424
1425 #[test]
1427 fn base32_accepts_valid() {
1428 assert_eq!(Base32::new("MZXW6YTB").unwrap().as_str(), "MZXW6YTB"); assert!(Base32::new("MY======").is_ok()); assert!(Base32::new("MZXQ====").is_ok()); assert!(Base32::new("MZXW6===").is_ok()); assert!(Base32::new("MZXW6YQ=").is_ok()); assert!(Base32::new("MZXW6YTBOI======").is_ok()); }
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(), PrimitiveErrorKind::InvalidFormat
1445 );
1446 assert!(Base32::new("mzxw6ytb").is_err()); assert!(Base32::new("MZXW6YT1").is_err()); assert!(Base32::new("MZXW6Y==").is_err()); assert!(Base32::new("M=XW6YTB").is_err()); }
1451
1452 #[test]
1453 fn base32_padding_and_decoded_len() {
1454 let b = Base32::new("MY======").unwrap(); assert!(b.is_padded());
1456 assert_eq!(b.decoded_len(), 1);
1457 assert_eq!(Base32::new("MZXQ====").unwrap().decoded_len(), 2); assert_eq!(Base32::new("MZXW6===").unwrap().decoded_len(), 3); assert_eq!(Base32::new("MZXW6YQ=").unwrap().decoded_len(), 4); let full = Base32::new("MZXW6YTB").unwrap(); assert!(!full.is_padded());
1462 assert_eq!(full.decoded_len(), 5);
1463 assert_eq!(Base32::new("MZXW6YTBOI======").unwrap().decoded_len(), 6); }
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()); assert!(Identifier::new("has space").is_err());
1482 assert!(Identifier::new("dash-no").is_err());
1483 assert!(Identifier::new("café").is_err()); }
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()); assert!(Hostname::new("bad-.com").is_err()); assert!(Hostname::new("a..b").is_err()); assert!(Hostname::new(".leading").is_err());
1509 assert!(Hostname::new("trailing.").is_err());
1510 assert!(Hostname::new("under_score.com").is_err()); 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 #[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"); let as_ref: &str = try_ref.as_ref(); assert_eq!(as_ref, "YWJj");
1531 assert_eq!(&*try_ref, "YWJj"); assert!(try_ref == "YWJj"); assert!(try_ref == *"YWJj"); assert!(try_ref == String::from("YWJj")); assert!(try_ref == &String::from("YWJj")); assert_eq!(String::from(try_owned), "YWJj"); }
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}