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)]
690pub struct Identifier(String);
691
692impl Identifier {
693 pub fn new(value: impl Into<String>) -> PrimitiveResult<Self> {
696 let value = value.into();
697 let mut chars = value.chars();
698 match chars.next() {
699 None => return Err(PrimitiveError::Empty),
700 Some(first) if !(first.is_ascii_alphabetic() || first == '_') => {
701 return Err(PrimitiveError::Invalid {
702 message: "identifier must start with an ASCII letter or underscore",
703 });
704 }
705 Some(_) => {}
706 }
707 if !chars.all(|c| c.is_ascii_alphanumeric() || c == '_') {
708 return Err(PrimitiveError::Invalid {
709 message: "identifier may contain only ASCII letters, digits, and underscores",
710 });
711 }
712 Ok(Self(value))
713 }
714
715 pub fn as_str(&self) -> &str {
717 &self.0
718 }
719
720 pub fn into_inner(self) -> String {
722 self.0
723 }
724}
725
726impl fmt::Display for Identifier {
727 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
728 f.write_str(&self.0)
729 }
730}
731
732impl AsRef<str> for Identifier {
733 fn as_ref(&self) -> &str {
734 self.as_str()
735 }
736}
737
738impl Deref for Identifier {
739 type Target = str;
740
741 fn deref(&self) -> &Self::Target {
742 self.as_str()
743 }
744}
745
746impl TryFrom<&str> for Identifier {
747 type Error = PrimitiveError;
748
749 fn try_from(value: &str) -> Result<Self, Self::Error> {
750 Self::new(value)
751 }
752}
753
754impl TryFrom<String> for Identifier {
755 type Error = PrimitiveError;
756
757 fn try_from(value: String) -> Result<Self, Self::Error> {
758 Self::new(value)
759 }
760}
761
762impl FromStr for Identifier {
763 type Err = PrimitiveError;
764
765 fn from_str(s: &str) -> Result<Self, Self::Err> {
766 Self::new(s)
767 }
768}
769
770impl PartialEq<str> for Identifier {
771 fn eq(&self, other: &str) -> bool {
772 self.as_str() == other
773 }
774}
775
776impl PartialEq<&str> for Identifier {
777 fn eq(&self, other: &&str) -> bool {
778 self.as_str() == *other
779 }
780}
781
782impl PartialEq<String> for Identifier {
783 fn eq(&self, other: &String) -> bool {
784 self.as_str() == other.as_str()
785 }
786}
787
788impl PartialEq<&String> for Identifier {
789 fn eq(&self, other: &&String) -> bool {
790 self.as_str() == other.as_str()
791 }
792}
793
794impl From<Identifier> for String {
795 fn from(value: Identifier) -> Self {
796 value.into_inner()
797 }
798}
799
800#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
809pub struct Hostname(String);
810
811impl Hostname {
812 pub fn new(value: impl Into<String>) -> PrimitiveResult<Self> {
815 let value = value.into();
816 if value.is_empty() {
817 return Err(PrimitiveError::Empty);
818 }
819 if value.len() > 253 {
820 return Err(PrimitiveError::TooLong {
821 max: 253,
822 actual: value.len(),
823 });
824 }
825 for label in value.split('.') {
826 if label.is_empty() {
827 return Err(PrimitiveError::Invalid {
828 message: "hostname label must not be empty",
829 });
830 }
831 if label.len() > 63 {
832 return Err(PrimitiveError::Invalid {
833 message: "hostname label must not exceed 63 characters",
834 });
835 }
836 if label.starts_with('-') || label.ends_with('-') {
837 return Err(PrimitiveError::Invalid {
838 message: "hostname label must not start or end with a hyphen",
839 });
840 }
841 if !label
842 .bytes()
843 .all(|b| b.is_ascii_alphanumeric() || b == b'-')
844 {
845 return Err(PrimitiveError::Invalid {
846 message: "hostname label may contain only letters, digits, and hyphens",
847 });
848 }
849 }
850 Ok(Self(value))
851 }
852
853 pub fn as_str(&self) -> &str {
855 &self.0
856 }
857
858 pub fn into_inner(self) -> String {
860 self.0
861 }
862
863 pub fn labels(&self) -> impl Iterator<Item = &str> + '_ {
865 self.0.split('.')
866 }
867}
868
869impl fmt::Display for Hostname {
870 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
871 f.write_str(&self.0)
872 }
873}
874
875impl AsRef<str> for Hostname {
876 fn as_ref(&self) -> &str {
877 self.as_str()
878 }
879}
880
881impl Deref for Hostname {
882 type Target = str;
883
884 fn deref(&self) -> &Self::Target {
885 self.as_str()
886 }
887}
888
889impl TryFrom<&str> for Hostname {
890 type Error = PrimitiveError;
891
892 fn try_from(value: &str) -> Result<Self, Self::Error> {
893 Self::new(value)
894 }
895}
896
897impl TryFrom<String> for Hostname {
898 type Error = PrimitiveError;
899
900 fn try_from(value: String) -> Result<Self, Self::Error> {
901 Self::new(value)
902 }
903}
904
905impl FromStr for Hostname {
906 type Err = PrimitiveError;
907
908 fn from_str(s: &str) -> Result<Self, Self::Err> {
909 Self::new(s)
910 }
911}
912
913impl PartialEq<str> for Hostname {
914 fn eq(&self, other: &str) -> bool {
915 self.as_str() == other
916 }
917}
918
919impl PartialEq<&str> for Hostname {
920 fn eq(&self, other: &&str) -> bool {
921 self.as_str() == *other
922 }
923}
924
925impl PartialEq<String> for Hostname {
926 fn eq(&self, other: &String) -> bool {
927 self.as_str() == other.as_str()
928 }
929}
930
931impl PartialEq<&String> for Hostname {
932 fn eq(&self, other: &&String) -> bool {
933 self.as_str() == other.as_str()
934 }
935}
936
937impl From<Hostname> for String {
938 fn from(value: Hostname) -> Self {
939 value.into_inner()
940 }
941}
942
943#[cfg(test)]
946mod tests {
947 use super::{Base64, Email, HexString, Hostname, HttpUrl, Identifier, Slug};
948 use crate::{PrimitiveError, PrimitiveErrorKind};
949
950 #[test]
952 fn slug_accepts_valid() {
953 assert_eq!(Slug::new("my-service").unwrap().as_str(), "my-service");
954 assert_eq!(Slug::new("api-v2").unwrap().as_str(), "api-v2");
955 assert_eq!(Slug::new("user123").unwrap().as_str(), "user123");
956 }
957
958 #[test]
959 fn slug_rejects_empty() {
960 assert_eq!(Slug::new("").unwrap_err(), PrimitiveError::Empty);
961 }
962
963 #[test]
964 fn slug_rejects_uppercase() {
965 assert!(Slug::new("MySlug").is_err());
966 }
967
968 #[test]
969 fn slug_rejects_leading_hyphen() {
970 assert!(Slug::new("-bad").is_err());
971 }
972
973 #[test]
974 fn slug_rejects_trailing_hyphen() {
975 assert!(Slug::new("bad-").is_err());
976 }
977
978 #[test]
979 fn slug_rejects_consecutive_hyphens() {
980 assert!(Slug::new("bad--slug").is_err());
981 }
982
983 #[test]
984 fn slug_rejects_spaces() {
985 assert!(Slug::new("has space").is_err());
986 }
987
988 #[test]
989 fn slug_display() {
990 use alloc::string::ToString;
991 assert_eq!(Slug::new("hello").unwrap().to_string(), "hello");
992 }
993
994 #[test]
995 fn slug_deref() {
996 let s = Slug::new("hello").unwrap();
997 assert_eq!(&*s, "hello");
998 }
999
1000 #[test]
1001 fn slug_from_str_and_string_comparisons() {
1002 let slug = "hello".parse::<Slug>().unwrap();
1003 let owned = String::from("hello");
1004 assert_eq!(slug, "hello");
1005 assert_eq!(slug, owned);
1006 assert!("Hello".parse::<Slug>().is_err());
1007 }
1008
1009 #[test]
1010 fn slug_converts_into_string() {
1011 let slug = Slug::new("hello").unwrap();
1012 let inner = String::from(slug);
1013 assert_eq!(inner, "hello");
1014 }
1015
1016 #[test]
1018 fn email_accepts_valid() {
1019 let e = Email::new("user@example.com").unwrap();
1020 assert_eq!(e.local(), "user");
1021 assert_eq!(e.domain(), "example.com");
1022 }
1023
1024 #[test]
1025 fn email_rejects_empty() {
1026 assert_eq!(Email::new("").unwrap_err(), PrimitiveError::Empty);
1027 }
1028
1029 #[test]
1030 fn email_rejects_missing_at() {
1031 assert!(Email::new("nodomain").is_err());
1032 }
1033
1034 #[test]
1035 fn email_rejects_multiple_at() {
1036 assert!(Email::new("a@b@c.com").is_err());
1037 }
1038
1039 #[test]
1040 fn email_rejects_no_dot_in_domain() {
1041 assert!(Email::new("user@nodot").is_err());
1042 }
1043
1044 #[test]
1045 fn email_rejects_empty_domain_labels() {
1046 assert!(Email::new("user@example..com").is_err());
1047 assert!(Email::new("user@.example.com").is_err());
1048 assert!(Email::new("user@example.com.").is_err());
1049 }
1050
1051 #[test]
1052 fn email_rejects_spaces() {
1053 assert!(Email::new("us er@example.com").is_err());
1054 }
1055
1056 #[test]
1057 fn email_rejects_tab() {
1058 assert!(Email::new("user\t@example.com").is_err());
1059 }
1060
1061 #[test]
1062 fn email_rejects_newline() {
1063 assert!(Email::new("user\n@example.com").is_err());
1064 }
1065
1066 #[test]
1067 fn url_rejects_whitespace_host() {
1068 assert!(HttpUrl::new("http:// ").is_err());
1069 }
1070
1071 #[test]
1072 fn url_rejects_whitespace_in_path() {
1073 assert!(HttpUrl::new("https://ex ample.com").is_err());
1074 }
1075
1076 #[test]
1077 fn email_display() {
1078 use alloc::string::ToString;
1079 assert_eq!(Email::new("a@b.com").unwrap().to_string(), "a@b.com");
1080 }
1081
1082 #[test]
1083 fn email_from_str_and_string_comparisons() {
1084 let email = "a@b.com".parse::<Email>().unwrap();
1085 let owned = String::from("a@b.com");
1086 assert_eq!(email, "a@b.com");
1087 assert_eq!(email, owned);
1088 assert!("bad".parse::<Email>().is_err());
1089 }
1090
1091 #[test]
1092 fn email_string_ergonomics() {
1093 let email = Email::try_from(String::from("a@b.com")).unwrap();
1094 let borrowed: &str = email.as_ref();
1095 assert_eq!(borrowed, "a@b.com");
1096 assert_eq!(&*email, "a@b.com");
1097
1098 let inner = String::from(email);
1099 assert_eq!(inner, "a@b.com");
1100 }
1101
1102 #[test]
1104 fn url_accepts_http() {
1105 let u = HttpUrl::new("http://example.com").unwrap();
1106 assert!(!u.is_https());
1107 }
1108
1109 #[test]
1110 fn url_accepts_https() {
1111 let u = HttpUrl::new("https://example.com/path").unwrap();
1112 assert!(u.is_https());
1113 }
1114
1115 #[test]
1116 fn url_rejects_empty() {
1117 assert_eq!(HttpUrl::new("").unwrap_err(), PrimitiveError::Empty);
1118 }
1119
1120 #[test]
1121 fn url_rejects_missing_scheme() {
1122 assert!(HttpUrl::new("ftp://example.com").is_err());
1123 }
1124
1125 #[test]
1126 fn url_rejects_empty_host() {
1127 assert!(HttpUrl::new("https://").is_err());
1128 }
1129
1130 #[test]
1131 fn url_rejects_missing_host_before_path() {
1132 assert!(HttpUrl::new("https:///path").is_err());
1133 }
1134
1135 #[test]
1136 fn url_display() {
1137 use alloc::string::ToString;
1138 let u = HttpUrl::new("https://example.com").unwrap();
1139 assert_eq!(u.to_string(), "https://example.com");
1140 }
1141
1142 #[test]
1143 fn url_is_https_uppercase_scheme() {
1144 let u = HttpUrl::new("HTTPS://example.com").unwrap();
1145 assert!(u.is_https());
1146 }
1147
1148 #[test]
1149 fn url_accepts_uppercase_http_scheme() {
1150 let u = HttpUrl::new("HTTP://example.com").unwrap();
1151 assert!(!u.is_https());
1152 }
1153
1154 #[test]
1155 fn url_is_http_not_https() {
1156 let u = HttpUrl::new("http://example.com").unwrap();
1157 assert!(!u.is_https());
1158 }
1159
1160 #[test]
1161 fn url_from_str_and_string_comparisons() {
1162 let url = "https://example.com".parse::<HttpUrl>().unwrap();
1163 let owned = String::from("https://example.com");
1164 assert_eq!(url, "https://example.com");
1165 assert_eq!(url, owned);
1166 assert!("ftp://example.com".parse::<HttpUrl>().is_err());
1167 }
1168
1169 #[test]
1170 fn url_string_ergonomics() {
1171 let url = HttpUrl::try_from(String::from("https://example.com")).unwrap();
1172 let borrowed: &str = url.as_ref();
1173 assert_eq!(borrowed, "https://example.com");
1174 assert_eq!(&*url, "https://example.com");
1175
1176 let inner = String::from(url);
1177 assert_eq!(inner, "https://example.com");
1178 }
1179
1180 #[test]
1182 fn hex_accepts_plain() {
1183 let h = HexString::new("deadbeef").unwrap();
1184 assert_eq!(h.hex_digits(), "deadbeef");
1185 assert!(!h.has_prefix());
1186 }
1187
1188 #[test]
1189 fn hex_accepts_prefixed() {
1190 let h = HexString::new("0xdeadbeef").unwrap();
1191 assert_eq!(h.hex_digits(), "deadbeef");
1192 assert!(h.has_prefix());
1193 }
1194
1195 #[test]
1196 fn hex_accepts_uppercase() {
1197 assert!(HexString::new("DEADBEEF").is_ok());
1198 }
1199
1200 #[test]
1201 fn hex_rejects_empty() {
1202 assert_eq!(HexString::new("").unwrap_err(), PrimitiveError::Empty);
1203 }
1204
1205 #[test]
1206 fn hex_rejects_prefix_only() {
1207 assert!(HexString::new("0x").is_err());
1208 }
1209
1210 #[test]
1211 fn hex_rejects_invalid_chars() {
1212 assert!(HexString::new("xyz").is_err());
1213 }
1214
1215 #[test]
1216 fn hex_display() {
1217 use alloc::string::ToString;
1218 assert_eq!(HexString::new("ff00").unwrap().to_string(), "ff00");
1219 }
1220
1221 #[test]
1222 fn hex_from_str_and_string_comparisons() {
1223 let hex = "ff00".parse::<HexString>().unwrap();
1224 let owned = String::from("ff00");
1225 assert_eq!(hex, "ff00");
1226 assert_eq!(hex, owned);
1227 assert!("xyz".parse::<HexString>().is_err());
1228 }
1229
1230 #[test]
1231 fn hex_string_ergonomics() {
1232 let hex = HexString::try_from(String::from("ff00")).unwrap();
1233 let borrowed: &str = hex.as_ref();
1234 assert_eq!(borrowed, "ff00");
1235 assert_eq!(&*hex, "ff00");
1236
1237 let inner = String::from(hex);
1238 assert_eq!(inner, "ff00");
1239 }
1240
1241 #[test]
1242 fn base64_accepts_valid() {
1243 assert_eq!(Base64::new("aGVsbG8=").unwrap().as_str(), "aGVsbG8=");
1244 assert!(Base64::new("YWJjZA==").is_ok()); assert!(Base64::new("YWJjZGU+").is_ok()); assert!(Base64::new("ab/+ZZ90").is_ok());
1247 }
1248
1249 #[test]
1250 fn base64_rejects_bad() {
1251 assert_eq!(
1252 Base64::new("").unwrap_err().kind(),
1253 PrimitiveErrorKind::Empty
1254 );
1255 assert_eq!(
1256 Base64::new("aGVsbG8").unwrap_err().kind(), PrimitiveErrorKind::InvalidFormat
1258 );
1259 assert!(Base64::new("ab-_ZZ90").is_err()); assert!(Base64::new("ab=cZZ90").is_err()); assert!(Base64::new("ab======").is_err()); }
1263
1264 #[test]
1265 fn base64_padding_and_decoded_len() {
1266 let b = Base64::new("aGVsbG8=").unwrap(); assert!(b.is_padded());
1268 assert_eq!(b.decoded_len(), 5);
1269 let b = Base64::new("YWJjZA==").unwrap(); assert_eq!(b.decoded_len(), 4);
1271 let b = Base64::new("YWJjZGZn").unwrap(); assert!(!b.is_padded());
1273 assert_eq!(b.decoded_len(), 6);
1274 }
1275
1276 #[test]
1277 fn identifier_accepts_valid() {
1278 assert_eq!(Identifier::new("user_id").unwrap().as_str(), "user_id");
1279 assert!(Identifier::new("_private").is_ok());
1280 assert!(Identifier::new("A1").is_ok());
1281 assert!(Identifier::new("x").is_ok());
1282 }
1283
1284 #[test]
1285 fn identifier_rejects_bad() {
1286 assert_eq!(
1287 Identifier::new("").unwrap_err().kind(),
1288 PrimitiveErrorKind::Empty
1289 );
1290 assert!(Identifier::new("3bad").is_err()); assert!(Identifier::new("has space").is_err());
1292 assert!(Identifier::new("dash-no").is_err());
1293 assert!(Identifier::new("café").is_err()); }
1295
1296 #[test]
1297 fn hostname_accepts_valid() {
1298 assert_eq!(
1299 Hostname::new("api.example.com").unwrap().as_str(),
1300 "api.example.com"
1301 );
1302 assert!(Hostname::new("localhost").is_ok());
1303 assert!(Hostname::new("a-b.c-d.example").is_ok());
1304 let h = Hostname::new("api.example.com").unwrap();
1305 let labels: alloc::vec::Vec<&str> = h.labels().collect();
1306 assert_eq!(labels, ["api", "example", "com"]);
1307 }
1308
1309 #[test]
1310 fn hostname_rejects_bad() {
1311 assert_eq!(
1312 Hostname::new("").unwrap_err().kind(),
1313 PrimitiveErrorKind::Empty
1314 );
1315 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());
1319 assert!(Hostname::new("trailing.").is_err());
1320 assert!(Hostname::new("under_score.com").is_err()); assert!(Hostname::new(String::from("a").repeat(64)).is_err());
1322 let too_long = alloc::format!("{}.com", String::from("a").repeat(252));
1323 assert!(Hostname::new(too_long).is_err());
1324 }
1325
1326 #[test]
1330 #[allow(clippy::cmp_owned, clippy::op_ref)]
1331 fn base64_conversions_and_traits() {
1332 let from_str: Base64 = "YWJj".parse().unwrap();
1333 let try_ref = Base64::try_from("YWJj").unwrap();
1334 let try_owned = Base64::try_from(String::from("YWJj")).unwrap();
1335 assert_eq!(from_str, try_ref);
1336 assert_eq!(try_ref, try_owned);
1337
1338 assert_eq!(try_ref.to_string(), "YWJj"); let as_ref: &str = try_ref.as_ref(); assert_eq!(as_ref, "YWJj");
1341 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"); }
1348
1349 #[test]
1350 #[allow(clippy::cmp_owned, clippy::op_ref)]
1351 fn identifier_conversions_and_traits() {
1352 let from_str: Identifier = "user_id".parse().unwrap();
1353 let try_ref = Identifier::try_from("user_id").unwrap();
1354 let try_owned = Identifier::try_from(String::from("user_id")).unwrap();
1355 assert_eq!(from_str, try_ref);
1356 assert_eq!(try_ref, try_owned);
1357
1358 assert_eq!(try_ref.to_string(), "user_id");
1359 let as_ref: &str = try_ref.as_ref();
1360 assert_eq!(as_ref, "user_id");
1361 assert_eq!(&*try_ref, "user_id");
1362 assert!(try_ref == "user_id");
1363 assert!(try_ref == *"user_id");
1364 assert!(try_ref == String::from("user_id"));
1365 assert!(try_ref == &String::from("user_id"));
1366 assert_eq!(String::from(try_owned), "user_id");
1367 }
1368
1369 #[test]
1370 #[allow(clippy::cmp_owned, clippy::op_ref)]
1371 fn hostname_conversions_and_traits() {
1372 let from_str: Hostname = "example.com".parse().unwrap();
1373 let try_ref = Hostname::try_from("example.com").unwrap();
1374 let try_owned = Hostname::try_from(String::from("example.com")).unwrap();
1375 assert_eq!(from_str, try_ref);
1376 assert_eq!(try_ref, try_owned);
1377
1378 assert_eq!(try_ref.to_string(), "example.com");
1379 let as_ref: &str = try_ref.as_ref();
1380 assert_eq!(as_ref, "example.com");
1381 assert_eq!(&*try_ref, "example.com");
1382 assert!(try_ref == "example.com");
1383 assert!(try_ref == *"example.com");
1384 assert!(try_ref == String::from("example.com"));
1385 assert!(try_ref == &String::from("example.com"));
1386 assert_eq!(String::from(try_owned), "example.com");
1387 }
1388}