Skip to main content

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}