Skip to main content

tuwunel_core/utils/string/
unquoted.rs

1use std::ops::Deref;
2
3use serde::{Deserialize, Deserializer, de};
4
5use super::Unquote;
6use crate::{Result, err};
7
8/// A string view with surrounding quotes removed when present.
9///
10/// Conversion from `&str` is infallible and accepts already unquoted input.
11/// Deserialization requires quoted input and fails otherwise.
12#[repr(transparent)]
13pub struct Unquoted(str);
14
15impl<'a> Unquoted {
16	/// Returns the underlying string view without surrounding quotes.
17	///
18	/// The returned slice borrows the transparent wrapper and performs no
19	/// allocation or copy. Its lifetime is tied to the wrapper reference.
20	#[inline]
21	#[must_use]
22	pub fn as_str(&'a self) -> &'a str { &self.0 }
23}
24
25impl<'a, 'de: 'a> Deserialize<'de> for &'a Unquoted {
26	fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
27		let s = <&'a str>::deserialize(deserializer)?;
28		s.is_quoted()
29			.then_some(s)
30			.ok_or(err!(SerdeDe("expected quoted string")))
31			.map_err(de::Error::custom)
32			.map(Into::into)
33	}
34}
35
36impl<'a> From<&'a str> for &'a Unquoted {
37	fn from(s: &'a str) -> &'a Unquoted {
38		let s: &'a str = s.unquote_infallible();
39
40		//SAFETY: This is a pattern I lifted from ruma-identifiers for strong-type strs
41		// by wrapping in a tuple-struct.
42		#[expect(clippy::transmute_ptr_to_ptr)]
43		unsafe {
44			std::mem::transmute(s)
45		}
46	}
47}
48
49impl Deref for Unquoted {
50	type Target = str;
51
52	fn deref(&self) -> &Self::Target { &self.0 }
53}
54
55impl<'a> AsRef<str> for &'a Unquoted {
56	fn as_ref(&self) -> &'a str { &self.0 }
57}