kvx_types/
segment.rs

1use std::{
2    borrow::Borrow,
3    fmt::{Display, Formatter},
4    ops::Deref,
5    str::FromStr,
6};
7
8use thiserror::Error;
9
10use crate::Scope;
11
12/// A nonempty string that does not start or end with whitespace and does not
13/// contain any instances of [`Scope::SEPARATOR`].
14///
15/// This is the owned variant of [`Segment`].
16#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
17#[repr(transparent)]
18pub struct SegmentBuf(String);
19
20impl AsRef<Segment> for SegmentBuf {
21    fn as_ref(&self) -> &Segment {
22        self
23    }
24}
25
26impl Borrow<Segment> for SegmentBuf {
27    fn borrow(&self) -> &Segment {
28        self
29    }
30}
31
32impl Deref for SegmentBuf {
33    type Target = Segment;
34
35    fn deref(&self) -> &Self::Target {
36        unsafe { Segment::from_str_unchecked(&self.0) }
37    }
38}
39
40impl Display for SegmentBuf {
41    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
42        write!(f, "{}", self.0)
43    }
44}
45
46impl FromStr for SegmentBuf {
47    type Err = ParseSegmentError;
48
49    fn from_str(s: &str) -> Result<Self, Self::Err> {
50        Ok(Segment::parse(s)?.to_owned())
51    }
52}
53
54impl From<&Segment> for SegmentBuf {
55    fn from(value: &Segment) -> Self {
56        value.to_owned()
57    }
58}
59
60/// A nonempty string slice that does not start or end with whitespace and does
61/// not contain any instances of [`Scope::SEPARATOR`].
62///
63/// For the owned variant, see [`SegmentBuf`].
64#[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
65#[repr(transparent)]
66pub struct Segment(str);
67
68impl Segment {
69    /// Parse a Segment from a string.
70    ///
71    /// # Examples
72    /// ```rust
73    /// # use kvx_types::ParseSegmentError;
74    /// use kvx_types::Segment;
75    ///
76    /// # fn main() -> Result<(), ParseSegmentError> {
77    /// Segment::parse("segment")?;
78    /// # Ok(())
79    /// # }
80    /// ```
81    ///
82    /// # Errors
83    /// If the string is empty, starts or ends with whitespace, or contains a
84    /// [`Scope::SEPARATOR`] a [`ParseSegmentError`] variant will be returned.
85    pub const fn parse(value: &str) -> Result<&Self, ParseSegmentError> {
86        if value.is_empty() {
87            Err(ParseSegmentError::Empty)
88        } else {
89            let bytes = value.as_bytes();
90            if Self::leading_whitespace(bytes) || Self::trailing_whitespace(bytes) {
91                Err(ParseSegmentError::TrailingWhitespace)
92            } else if Self::contains_separator(bytes) {
93                Err(ParseSegmentError::ContainsSeparator)
94            } else {
95                unsafe { Ok(Segment::from_str_unchecked(value)) }
96            }
97        }
98    }
99
100    /// Return the encapsulated string.
101    ///
102    /// # Examples
103    /// ```rust
104    /// # use kvx_types::ParseSegmentError;
105    /// use kvx_types::Segment;
106    ///
107    /// # fn main() -> Result<(), ParseSegmentError> {
108    /// let segment_str = "segment";
109    /// let segment = Segment::parse(segment_str)?;
110    /// assert_eq!(segment.as_str(), segment_str);
111    /// # Ok(())
112    /// # }
113    /// ```
114    pub fn as_str(&self) -> &str {
115        &self.0
116    }
117
118    /// Creates a Segment from a string without performing any checks.
119    ///
120    /// # Safety
121    /// This function should only be called from the [`kvx_macros`] crate - do
122    /// not use directly.
123    ///
124    /// [`kvx_macros`]: ../kvx_macros/index.html
125    pub const unsafe fn from_str_unchecked(s: &str) -> &Self {
126        &*(s as *const _ as *const Self)
127    }
128
129    const fn leading_whitespace(bytes: &[u8]) -> bool {
130        matches!(bytes[0], 9 | 10 | 32)
131    }
132
133    const fn trailing_whitespace(bytes: &[u8]) -> bool {
134        matches!(bytes[bytes.len() - 1], 9 | 10 | 32)
135    }
136
137    const fn contains_separator(bytes: &[u8]) -> bool {
138        let mut index = 0;
139
140        while index < bytes.len() {
141            if bytes[index] == Scope::SEPARATOR as u8 {
142                return true;
143            }
144            index += 1;
145        }
146
147        false
148    }
149}
150
151impl Display for Segment {
152    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
153        write!(f, "{}", &self.0)
154    }
155}
156
157impl ToOwned for Segment {
158    type Owned = SegmentBuf;
159
160    fn to_owned(&self) -> Self::Owned {
161        SegmentBuf(self.0.to_owned())
162    }
163}
164
165/// Represents all ways parsing a string as a [`Segment`] can fail.
166#[derive(Debug, Error)]
167pub enum ParseSegmentError {
168    #[error("segments must not start or end with whitespace")]
169    TrailingWhitespace,
170    #[error("segments must be nonempty")]
171    Empty,
172    #[error("segments must not contain scope separators")]
173    ContainsSeparator,
174}
175
176#[cfg(feature = "postgres")]
177mod postgres_impls {
178    use crate::segment::{Segment, SegmentBuf};
179
180    impl postgres::types::ToSql for &Segment {
181        fn to_sql(
182            &self,
183            ty: &postgres_types::Type,
184            out: &mut postgres_types::private::BytesMut,
185        ) -> Result<postgres_types::IsNull, Box<dyn std::error::Error + Sync + Send>>
186        where
187            Self: Sized,
188        {
189            (&self.0).to_sql(ty, out)
190        }
191
192        fn accepts(ty: &postgres_types::Type) -> bool
193        where
194            Self: Sized,
195        {
196            <&str>::accepts(ty)
197        }
198
199        fn to_sql_checked(
200            &self,
201            ty: &postgres_types::Type,
202            out: &mut postgres_types::private::BytesMut,
203        ) -> Result<postgres_types::IsNull, Box<dyn std::error::Error + Sync + Send>> {
204            (&self.0).to_sql_checked(ty, out)
205        }
206    }
207
208    impl postgres::types::ToSql for SegmentBuf {
209        fn to_sql(
210            &self,
211            ty: &postgres_types::Type,
212            out: &mut postgres_types::private::BytesMut,
213        ) -> Result<postgres_types::IsNull, Box<dyn std::error::Error + Sync + Send>>
214        where
215            Self: Sized,
216        {
217            self.0.to_sql(ty, out)
218        }
219
220        fn accepts(ty: &postgres_types::Type) -> bool
221        where
222            Self: Sized,
223        {
224            String::accepts(ty)
225        }
226
227        fn to_sql_checked(
228            &self,
229            ty: &postgres_types::Type,
230            out: &mut postgres_types::private::BytesMut,
231        ) -> Result<postgres_types::IsNull, Box<dyn std::error::Error + Sync + Send>> {
232            self.0.to_sql_checked(ty, out)
233        }
234    }
235
236    impl<'a> postgres::types::FromSql<'a> for SegmentBuf {
237        fn from_sql(
238            ty: &postgres_types::Type,
239            raw: &'a [u8],
240        ) -> Result<Self, Box<dyn std::error::Error + Sync + Send>> {
241            let value = String::from_sql(ty, raw)?;
242            Ok(Segment::parse(&value)?.to_owned())
243        }
244
245        fn accepts(ty: &postgres_types::Type) -> bool {
246            String::accepts(ty)
247        }
248    }
249}
250
251#[cfg(test)]
252mod tests {
253    use super::{Scope, Segment};
254
255    #[test]
256    fn test_trailing_separator_fails() {
257        assert!(Segment::parse(&format!("test{}", Scope::SEPARATOR)).is_err());
258    }
259
260    #[test]
261    fn test_trailing_space_fails() {
262        assert!(Segment::parse("test ").is_err());
263    }
264
265    #[test]
266    fn test_trailing_tab_fails() {
267        assert!(Segment::parse("test\t").is_err());
268    }
269
270    #[test]
271    fn test_trailing_newline_fails() {
272        assert!(Segment::parse("test\n").is_err());
273    }
274
275    #[test]
276    fn test_leading_separator_fails() {
277        assert!(Segment::parse(&format!("{}test", Scope::SEPARATOR)).is_err());
278    }
279
280    #[test]
281    fn test_leading_space_fails() {
282        assert!(Segment::parse(" test").is_err());
283    }
284
285    #[test]
286    fn test_leading_tab_fails() {
287        assert!(Segment::parse("\ttest").is_err());
288    }
289
290    #[test]
291    fn test_leading_newline_fails() {
292        assert!(Segment::parse("\ntest").is_err());
293    }
294
295    #[test]
296    fn test_containing_separator_fails() {
297        assert!(Segment::parse(&format!("te{}st", Scope::SEPARATOR)).is_err());
298    }
299
300    #[test]
301    fn test_containing_space_succeeds() {
302        assert!(Segment::parse("te st").is_ok());
303    }
304
305    #[test]
306    fn test_containing_tab_succeeds() {
307        assert!(Segment::parse("te\tst").is_ok());
308    }
309
310    #[test]
311    fn test_containing_newline_succeeds() {
312        assert!(Segment::parse("te\nst").is_ok());
313    }
314
315    #[test]
316    fn test_segment_succeeds() {
317        assert!(Segment::parse("test").is_ok())
318    }
319}