Skip to main content

calendar_types/
string.rs

1//! String data model types.
2
3use std::str::FromStr;
4
5use dizzy::DstNewtype;
6use thiserror::Error;
7
8/// Re-export of the error type from the `language-tags` crate.
9pub use language_tags::ParseError as LanguageTagParseError;
10
11/// A BCP 47 language tag (RFC 5646).
12#[derive(Debug, Clone, PartialEq, Eq, Hash)]
13pub struct LanguageTag(language_tags::LanguageTag);
14
15impl LanguageTag {
16    /// Parses a language tag from a string.
17    pub fn parse(s: &str) -> Result<Self, language_tags::ParseError> {
18        language_tags::LanguageTag::parse(s).map(LanguageTag)
19    }
20
21    /// Returns the language tag as a string.
22    #[inline]
23    pub fn as_str(&self) -> &str {
24        self.0.as_str()
25    }
26
27    /// Returns the primary language subtag.
28    #[inline]
29    pub fn primary_language(&self) -> &str {
30        self.0.primary_language()
31    }
32}
33
34impl FromStr for LanguageTag {
35    type Err = language_tags::ParseError;
36
37    fn from_str(s: &str) -> Result<Self, Self::Err> {
38        Self::parse(s)
39    }
40}
41
42impl std::fmt::Display for LanguageTag {
43    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44        std::fmt::Display::fmt(&self.0, f)
45    }
46}
47
48/// An error indicating that a string is not a valid UID.
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
50pub enum InvalidUidError {
51    #[error("expected at least one character")]
52    EmptyString,
53}
54
55/// A globally unique identifier.
56///
57/// This type is used by both JSCalendar (RFC 8984 §4.1.1) and iCalendar (RFC 5545 §3.8.4.7).
58/// The value is an arbitrary non-empty string with no particular format required.
59/// Uniqueness is a semantic property and is not enforced by this type.
60#[derive(PartialEq, Eq, PartialOrd, Ord, Hash, DstNewtype)]
61#[dizzy(invariant = Uid::str_is_uid, error = InvalidUidError)]
62#[dizzy(constructor = pub new)]
63#[dizzy(getter = pub const as_str)]
64#[dizzy(derive(Debug, CloneBoxed, IntoBoxed))]
65#[dizzy(owned = pub UidBuf(String))]
66#[dizzy(derive_owned(Debug, IntoBoxed))]
67#[repr(transparent)]
68pub struct Uid(str);
69
70impl std::fmt::Display for Uid {
71    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72        f.write_str(self.as_str())
73    }
74}
75
76impl Uid {
77    fn str_is_uid(s: &str) -> Result<(), InvalidUidError> {
78        if s.is_empty() {
79            return Err(InvalidUidError::EmptyString);
80        }
81        Ok(())
82    }
83}
84
85/// An error indicating that a string is not a valid URI.
86#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
87pub enum InvalidUriError {
88    #[error("expected at least one character")]
89    EmptyString,
90    #[error("missing colon after scheme")]
91    MissingColon,
92    #[error("scheme must start with a letter")]
93    SchemeStartsWithNonLetter,
94    #[error("invalid character in scheme: {c}")]
95    InvalidSchemeChar { index: usize, c: char },
96}
97
98/// A URI string (RFC 3986).
99///
100/// # Invariants
101/// 1. The underlying string is not empty.
102/// 2. The string contains a colon separating the scheme from the rest.
103/// 3. The scheme starts with a letter and contains only letters, digits, `+`, `-`, or `.`.
104#[derive(PartialEq, Eq, PartialOrd, Ord, Hash, DstNewtype)]
105#[dizzy(invariant = Uri::str_is_uri, error = InvalidUriError)]
106#[dizzy(constructor = pub new)]
107#[dizzy(getter = pub const as_str)]
108#[dizzy(derive(Debug, CloneBoxed, IntoBoxed))]
109#[dizzy(owned = pub UriBuf(String))]
110#[dizzy(derive_owned(Debug, IntoBoxed))]
111#[repr(transparent)]
112pub struct Uri(str);
113
114impl std::fmt::Display for Uri {
115    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116        f.write_str(self.as_str())
117    }
118}
119
120impl Uri {
121    fn str_is_uri(s: &str) -> Result<(), InvalidUriError> {
122        let (scheme, _rest) = s.split_once(':').ok_or(if s.is_empty() {
123            InvalidUriError::EmptyString
124        } else {
125            InvalidUriError::MissingColon
126        })?;
127
128        let mut chars = scheme.chars().enumerate();
129
130        match chars.next() {
131            None => return Err(InvalidUriError::MissingColon),
132            Some((_, c)) if !c.is_ascii_alphabetic() => {
133                return Err(InvalidUriError::SchemeStartsWithNonLetter);
134            }
135            Some(_) => {}
136        }
137
138        for (index, c) in chars {
139            if !c.is_ascii_alphanumeric() && c != '+' && c != '-' && c != '.' {
140                return Err(InvalidUriError::InvalidSchemeChar { index, c });
141            }
142        }
143
144        Ok(())
145    }
146
147    /// Returns the scheme portion of the URI (before the first colon).
148    #[inline(always)]
149    pub fn scheme(&self) -> &str {
150        self.as_str()
151            .split_once(':')
152            .expect("a Uri must contain a colon")
153            .0
154    }
155}