rsonpath_syntax/str.rs
1//! JSON string types expressible in a JSONPath query.
2//!
3//! This may refer to a member name when used in a name selector,
4//! or a raw string value used for comparison or matching in a filter expression.
5
6/// String value or JSON member name, conforming to the
7/// [RFC7159, section 7](https://www.rfc-editor.org/rfc/rfc7159#section-7)
8///
9/// Represents the UTF-8 bytes defining a string key or value in a JSON object
10/// that can be matched against when executing a query.
11///
12/// # Examples
13///
14/// ```rust
15/// # use rsonpath_syntax::str::JsonString;
16/// let needle = JsonString::new("needle");
17///
18/// assert_eq!(needle.unquoted(), "needle");
19/// assert_eq!(needle.quoted(), "\"needle\"");
20/// ```
21#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
22#[derive(Clone)]
23pub struct JsonString {
24 quoted: String,
25}
26
27#[derive(Debug)]
28pub(crate) struct JsonStringBuilder {
29 quoted: String,
30}
31
32impl std::fmt::Debug for JsonString {
33 #[inline]
34 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
35 write!(f, "JsonString({})", self.quoted)
36 }
37}
38
39impl JsonStringBuilder {
40 pub(crate) fn new() -> Self {
41 Self {
42 quoted: String::from('"'),
43 }
44 }
45
46 pub(crate) fn push(&mut self, char: char) -> &mut Self {
47 self.quoted.push(char);
48 self
49 }
50
51 pub(crate) fn finish(mut self) -> JsonString {
52 self.quoted.push('"');
53 JsonString { quoted: self.quoted }
54 }
55}
56
57impl From<JsonStringBuilder> for JsonString {
58 #[inline(always)]
59 fn from(value: JsonStringBuilder) -> Self {
60 value.finish()
61 }
62}
63
64impl From<&str> for JsonString {
65 #[inline(always)]
66 fn from(value: &str) -> Self {
67 Self::new(value)
68 }
69}
70
71impl FromIterator<char> for JsonString {
72 #[inline]
73 fn from_iter<T: IntoIterator<Item = char>>(iter: T) -> Self {
74 let mut quoted = String::new();
75 quoted.push('"');
76 for c in iter {
77 quoted.push(c);
78 }
79 quoted.push('"');
80 Self { quoted }
81 }
82}
83
84/// Escape mode for the [`escape`] function.
85#[derive(Clone, Copy, PartialEq, Eq, Debug)]
86pub enum EscapeMode {
87 /// Treat the string as within single quotes `'`.
88 SingleQuoted,
89 /// Treat the string as within double quotes `"`.
90 DoubleQuoted,
91}
92
93/// Escape a string according to JSONPath rules in a given quotation context.
94///
95/// ## Quotes
96///
97/// Processing quotes, `'` and `"`, depends on the `mode`:
98/// - in [`EscapeMode::SingleQuoted`], the string is escaped as if written in a single-quoted
99/// name selector `['<str>']`; single quotes are escaped as `\'`, double-quotes are copied as-is.
100/// - in [`EscapeMode::DoubleQuoted`], the string is escaped as if written in double-quotes,
101/// which is the same as a member name in a JSON document or a double-quoted name selector `["<str>"]`;
102/// double quotes are escaped as `\"`, single quotes are copied as-is.
103///
104/// ### Examples
105///
106/// ```rust
107/// # use rsonpath_syntax::str::{self, EscapeMode};
108/// let result_single = str::escape(r#"'rust' or "rust"\n"#, EscapeMode::SingleQuoted);
109/// let result_double = str::escape(r#"'rust' or "rust"\n"#, EscapeMode::DoubleQuoted);
110/// assert_eq!(result_single, r#"\'rust\' or "rust"\\n"#);
111/// assert_eq!(result_double, r#"'rust' or \"rust\"\\n"#);
112/// ```
113///
114/// ## Control characters
115///
116/// Control characters (U+0000 to U+001F) are escaped as special sequences
117/// where possible, e.g. Form Feed U+000C is escaped as `\f`.
118/// Other control sequences are escaped as a Unicode sequence, e.g.
119/// a null byte is escaped as `\u0000`.
120///
121/// ### Examples
122///
123/// ```rust
124/// # use rsonpath_syntax::str::{self, EscapeMode};
125/// let result = str::escape("\u{08}\u{09}\u{0A}\u{0B}\u{0C}\u{0D}", EscapeMode::DoubleQuoted);
126/// assert_eq!(result, r"\b\t\n\u000b\f\r");
127/// ```
128///
129/// ## Other
130///
131/// Characters that don't have to be escaped are not.
132///
133/// ### Examples
134///
135/// ```rust
136/// # use rsonpath_syntax::str::{self, EscapeMode};
137/// let result = str::escape("🦀", EscapeMode::DoubleQuoted);
138/// assert_eq!(result, "🦀");
139/// ```
140///
141/// Among other things, this means Unicode escapes are only produced
142/// for control characters.
143#[inline]
144#[must_use]
145pub fn escape(str: &str, mode: EscapeMode) -> String {
146 use std::fmt::Write as _;
147 let mut result = String::new();
148 for c in str.chars() {
149 match c {
150 // # Mode-dependent quote escapes.
151 '\'' if mode == EscapeMode::SingleQuoted => result.push_str(r"\'"),
152 '\'' if mode == EscapeMode::DoubleQuoted => result.push('\''),
153 '"' if mode == EscapeMode::SingleQuoted => result.push('"'),
154 '"' if mode == EscapeMode::DoubleQuoted => result.push_str(r#"\""#),
155 // # Mode-independent escapes.
156 '\\' => result.push_str(r"\\"),
157 // ## Special control sequences.
158 '\u{0008}' => result.push_str(r"\b"),
159 '\u{000C}' => result.push_str(r"\f"),
160 '\n' => result.push_str(r"\n"),
161 '\r' => result.push_str(r"\r"),
162 '\t' => result.push_str(r"\t"),
163 // ## Other control sequences escaped as Unicode escapes.
164 '\u{0000}'..='\u{001F}' => write!(result, "\\u{:0>4x}", c as u8).expect("writing to string never fails"),
165 // # Non-escapable characters.
166 _ => result.push(c),
167 }
168 }
169
170 result
171}
172
173impl JsonString {
174 /// Create a new JSON string from UTF8 input.
175 ///
176 /// # Examples
177 /// ```rust
178 /// # use rsonpath_syntax::str::JsonString;
179 /// let str = JsonString::new(r#"Stri\ng With \u00c9scapes \\n"#);
180 /// assert_eq!(str.unquoted(), r#"Stri\ng With \u00c9scapes \\n"#);
181 /// ```
182 #[inline]
183 #[must_use]
184 pub fn new(string: &str) -> Self {
185 let mut quoted = String::with_capacity(string.len() + 2);
186 quoted.push('"');
187 quoted += string;
188 quoted.push('"');
189 Self { quoted }
190 }
191
192 /// Return the contents of the string.
193 /// # Examples
194 /// ```rust
195 /// # use rsonpath_syntax::str::JsonString;
196 /// let needle = JsonString::new(r#"Stri\ng With \u00c9scapes \\n"#);
197 /// assert_eq!(needle.unquoted(), r#"Stri\ng With \u00c9scapes \\n"#);
198 /// ```
199 #[must_use]
200 #[inline(always)]
201 pub fn unquoted(&self) -> &str {
202 let len = self.quoted.len();
203 debug_assert!(len >= 2, "self.quoted must contain at least the two quote characters");
204 &self.quoted[1..len - 1]
205 }
206
207 /// Return the contents of the string with the leading and trailing
208 /// double quote symbol `"`.
209 /// # Examples
210 /// ```rust
211 /// # use rsonpath_syntax::str::JsonString;
212 /// let needle = JsonString::new(r#"Stri\ng With \u00c9scapes \\n"#);
213 /// assert_eq!(needle.quoted(), r#""Stri\ng With \u00c9scapes \\n""#);
214 /// ```
215 #[must_use]
216 #[inline(always)]
217 pub fn quoted(&self) -> &str {
218 &self.quoted
219 }
220}
221
222impl PartialEq<Self> for JsonString {
223 #[inline(always)]
224 fn eq(&self, other: &Self) -> bool {
225 self.unquoted() == other.unquoted()
226 }
227}
228
229impl Eq for JsonString {}
230
231impl std::hash::Hash for JsonString {
232 #[inline(always)]
233 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
234 self.unquoted().hash(state);
235 }
236}
237
238#[cfg(test)]
239mod tests {
240 use super::*;
241 use pretty_assertions::{assert_eq, assert_ne};
242 use std::{
243 collections::hash_map::DefaultHasher,
244 hash::{Hash, Hasher},
245 };
246 use test_case::test_case;
247
248 #[test_case("dog", "dog"; "dog")]
249 #[test_case("", ""; "empty")]
250 fn equal_json_strings_are_equal(s1: &str, s2: &str) {
251 let string1 = JsonString::new(s1);
252 let string2 = JsonString::new(s2);
253
254 assert_eq!(string1, string2);
255 }
256
257 #[test]
258 fn different_json_strings_are_not_equal() {
259 let string1 = JsonString::new("dog");
260 let string2 = JsonString::new("doc");
261
262 assert_ne!(string1, string2);
263 }
264
265 #[test_case("dog", "dog"; "dog")]
266 #[test_case("", ""; "empty")]
267 fn equal_json_strings_have_equal_hashes(s1: &str, s2: &str) {
268 let string1 = JsonString::new(s1);
269 let string2 = JsonString::new(s2);
270
271 let mut hasher1 = DefaultHasher::new();
272 string1.hash(&mut hasher1);
273 let hash1 = hasher1.finish();
274
275 let mut hasher2 = DefaultHasher::new();
276 string2.hash(&mut hasher2);
277 let hash2 = hasher2.finish();
278
279 assert_eq!(hash1, hash2);
280 }
281}