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, r#"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
71/// Escape mode for the [`escape`] function.
72#[derive(Clone, Copy, PartialEq, Eq, Debug)]
73pub enum EscapeMode {
74 /// Treat the string as within single quotes `'`.
75 SingleQuoted,
76 /// Treat the string as withing double quotes `"`.
77 DoubleQuoted,
78}
79
80/// Escape a string according to JSONPath rules in a given quotation context.
81///
82/// ## Quotes
83///
84/// Processing quotes, `'` and `"`, depends on the `mode`:
85/// - in [`EscapeMode::SingleQuoted`], the string is escaped as if written in a single-quoted
86/// name selector `['<str>']`; single quotes are escaped as `\'`, double-quotes are copied as-is.
87/// - in [`EscapeMode::DoubleQuoted`], the string is escaped as if written in double-quotes,
88/// which is the same as a member name in a JSON document or a double-quoted name selector `["<str>"]`;
89/// double quotes are escaped as `\"`, single quotes are copied as-is.
90///
91/// ### Examples
92///
93/// ```rust
94/// # use rsonpath_syntax::str::{self, EscapeMode};
95/// let result_single = str::escape(r#"'rust' or "rust"\n"#, EscapeMode::SingleQuoted);
96/// let result_double = str::escape(r#"'rust' or "rust"\n"#, EscapeMode::DoubleQuoted);
97/// assert_eq!(result_single, r#"\'rust\' or "rust"\\n"#);
98/// assert_eq!(result_double, r#"'rust' or \"rust\"\\n"#);
99/// ```
100///
101/// ## Control characters
102///
103/// Control characters (U+0000 to U+001F) are escaped as special sequences
104/// where possible, e.g. Form Feed U+000C is escaped as `\f`.
105/// Other control sequences are escaped as a Unicode sequence, e.g.
106/// a null byte is escaped as `\u0000`.
107///
108/// ### Examples
109///
110/// ```rust
111/// # use rsonpath_syntax::str::{self, EscapeMode};
112/// let result = str::escape("\u{08}\u{09}\u{0A}\u{0B}\u{0C}\u{0D}", EscapeMode::DoubleQuoted);
113/// assert_eq!(result, r"\b\t\n\u000b\f\r");
114/// ```
115///
116/// ## Other
117///
118/// Characters that don't have to be escaped are not.
119///
120/// ### Examples
121///
122/// ```rust
123/// # use rsonpath_syntax::str::{self, EscapeMode};
124/// let result = str::escape("🦀", EscapeMode::DoubleQuoted);
125/// assert_eq!(result, "🦀");
126/// ```
127///
128/// Among other things, this means Unicode escapes are only produced
129/// for control characters.
130#[inline]
131#[must_use]
132pub fn escape(str: &str, mode: EscapeMode) -> String {
133 use std::fmt::Write;
134 let mut result = String::new();
135 for c in str.chars() {
136 match c {
137 // # Mode-dependent quote escapes.
138 '\'' if mode == EscapeMode::SingleQuoted => result.push_str(r"\'"),
139 '\'' if mode == EscapeMode::DoubleQuoted => result.push('\''),
140 '"' if mode == EscapeMode::SingleQuoted => result.push('"'),
141 '"' if mode == EscapeMode::DoubleQuoted => result.push_str(r#"\""#),
142 // # Mode-independent escapes.
143 '\\' => result.push_str(r"\\"),
144 // ## Special control sequences.
145 '\u{0008}' => result.push_str(r"\b"),
146 '\u{000C}' => result.push_str(r"\f"),
147 '\n' => result.push_str(r"\n"),
148 '\r' => result.push_str(r"\r"),
149 '\t' => result.push_str(r"\t"),
150 // ## Other control sequences escaped as Unicode escapes.
151 '\u{0000}'..='\u{001F}' => write!(result, "\\u{:0>4x}", c as u8).unwrap(),
152 // # Non-escapable characters.
153 _ => result.push(c),
154 }
155 }
156
157 result
158}
159
160impl JsonString {
161 /// Create a new JSON string from UTF8 input.
162 ///
163 /// # Examples
164 /// ```rust
165 /// # use rsonpath_syntax::str::JsonString;
166 /// let str = JsonString::new(r#"Stri\ng With \u00c9scapes \\n"#);
167 /// assert_eq!(str.unquoted(), r#"Stri\ng With \u00c9scapes \\n"#);
168 /// ```
169 #[inline]
170 #[must_use]
171 pub fn new(string: &str) -> Self {
172 let mut quoted = String::with_capacity(string.len() + 2);
173 quoted.push('"');
174 quoted += string;
175 quoted.push('"');
176 Self { quoted }
177 }
178
179 /// Return the contents of the string.
180 /// # Examples
181 /// ```rust
182 /// # use rsonpath_syntax::str::JsonString;
183 /// let needle = JsonString::new(r#"Stri\ng With \u00c9scapes \\n"#);
184 /// assert_eq!(needle.unquoted(), r#"Stri\ng With \u00c9scapes \\n"#);
185 /// ```
186 #[must_use]
187 #[inline(always)]
188 pub fn unquoted(&self) -> &str {
189 let len = self.quoted.len();
190 debug_assert!(len >= 2);
191 &self.quoted[1..len - 1]
192 }
193
194 /// Return the contents of the string with the leading and trailing
195 /// double quote symbol `"`.
196 /// # Examples
197 /// ```rust
198 /// # use rsonpath_syntax::str::JsonString;
199 /// let needle = JsonString::new(r#"Stri\ng With \u00c9scapes \\n"#);
200 /// assert_eq!(needle.quoted(), r#""Stri\ng With \u00c9scapes \\n""#);
201 /// ```
202 #[must_use]
203 #[inline(always)]
204 pub fn quoted(&self) -> &str {
205 &self.quoted
206 }
207}
208
209impl PartialEq<Self> for JsonString {
210 #[inline(always)]
211 fn eq(&self, other: &Self) -> bool {
212 self.unquoted() == other.unquoted()
213 }
214}
215
216impl Eq for JsonString {}
217
218impl std::hash::Hash for JsonString {
219 #[inline(always)]
220 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
221 self.unquoted().hash(state);
222 }
223}
224
225#[cfg(feature = "arbitrary")]
226#[cfg_attr(docsrs, doc(cfg(feature = "arbitrary")))]
227impl<'a> arbitrary::Arbitrary<'a> for JsonString {
228 #[inline]
229 fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
230 let chars = u.arbitrary_iter()?;
231 let mut builder = JsonStringBuilder::new();
232
233 // RFC 7159: All Unicode characters may be placed [in the string],
234 // except for characters that must be escaped: quotation mark,
235 // reverse solidus, and the control characters (U+0000 through U+001F).
236 for c in chars {
237 let c = c?;
238 match c {
239 '\u{0000}'..='\u{001F}' | '\"' | '\\' => {
240 builder.push('\\');
241 builder.push(c);
242 }
243 _ => {
244 builder.push(c);
245 }
246 }
247 }
248
249 Ok(builder.into())
250 }
251}
252
253#[cfg(test)]
254mod tests {
255 use super::*;
256 use pretty_assertions::{assert_eq, assert_ne};
257 use std::{
258 collections::hash_map::DefaultHasher,
259 hash::{Hash, Hasher},
260 };
261 use test_case::test_case;
262
263 #[test_case("dog", "dog"; "dog")]
264 #[test_case("", ""; "empty")]
265 fn equal_json_strings_are_equal(s1: &str, s2: &str) {
266 let string1 = JsonString::new(s1);
267 let string2 = JsonString::new(s2);
268
269 assert_eq!(string1, string2);
270 }
271
272 #[test]
273 fn different_json_strings_are_not_equal() {
274 let string1 = JsonString::new("dog");
275 let string2 = JsonString::new("doc");
276
277 assert_ne!(string1, string2);
278 }
279
280 #[test_case("dog", "dog"; "dog")]
281 #[test_case("", ""; "empty")]
282 fn equal_json_strings_have_equal_hashes(s1: &str, s2: &str) {
283 let string1 = JsonString::new(s1);
284 let string2 = JsonString::new(s2);
285
286 let mut hasher1 = DefaultHasher::new();
287 string1.hash(&mut hasher1);
288 let hash1 = hasher1.finish();
289
290 let mut hasher2 = DefaultHasher::new();
291 string2.hash(&mut hasher2);
292 let hash2 = hasher2.finish();
293
294 assert_eq!(hash1, hash2);
295 }
296}