apollo_encoder/
string_value.rs

1use std::fmt::{self, Write};
2
3/// Write and optionally escape a character inside a GraphQL string value.
4fn write_character(c: char, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5    match c {
6        '"' => f.write_str(r#"\""#),
7        '\u{0008}' => f.write_str(r"\b"),
8        '\u{000c}' => f.write_str(r"\f"),
9        '\n' => f.write_str(r"\n"),
10        '\r' => f.write_str(r"\r"),
11        '\t' => f.write_str(r"\t"),
12        '\\' => f.write_str(r"\\"),
13        c if c.is_control() => write!(f, "\\u{:04x}", c as u32),
14        // Other unicode chars are written as is
15        c => write!(f, "{c}"),
16    }
17}
18
19/// Format a string as a """block string""".
20#[derive(Debug)]
21struct BlockStringFormatter<'a> {
22    string: &'a str,
23    /// Indentation for the whole block string: it expects to be printed
24    /// on its own line, and the caller is responsible for ensuring that.
25    indent: usize,
26}
27
28/// Write one line of a block string value, escaping characters as necessary.
29fn write_block_string_line(line: &'_ str, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30    let mut char_iter = line.char_indices();
31
32    while let Some((pos, c)) = char_iter.next() {
33        // Output """ as \"""
34        if c == '"' && line.get(pos..pos + 3) == Some("\"\"\"") {
35            // We know there will be two more " characters.
36            // Skip them so we can output """""" as \"""\""" instead of as \"\"\"\"""
37            char_iter.next();
38            char_iter.next();
39
40            f.write_str("\\\"\"\"")?;
41            continue;
42        }
43
44        f.write_char(c)?;
45    }
46
47    Ok(())
48}
49
50impl fmt::Display for BlockStringFormatter<'_> {
51    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52        let indent = " ".repeat(self.indent);
53
54        write!(f, "{indent}\"\"\"")?;
55        for line in self.string.lines() {
56            write!(f, "\n{indent}")?;
57            write_block_string_line(line, f)?;
58        }
59        write!(f, "\n{indent}\"\"\"")?;
60
61        Ok(())
62    }
63}
64
65/// Format a string, handling escape sequences.
66#[derive(Debug)]
67struct StringFormatter<'a>(&'a str);
68impl fmt::Display for StringFormatter<'_> {
69    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70        f.write_char('"')?;
71        for c in self.0.chars() {
72            write_character(c, f)?;
73        }
74        f.write_char('"')?;
75        Ok(())
76    }
77}
78
79#[derive(Debug, PartialEq, Eq, Clone)]
80/// Convenience enum to create a Description. Can be a `Top` level, a `Field`
81/// level or an `Input` level. The variants are distinguished by the way they
82/// get displayed, e.g. number of leading spaces.
83pub enum StringValue {
84    /// Top-level description.
85    Top {
86        /// Description.
87        source: String,
88    },
89    /// Field-level description.
90    /// This description gets additional leading spaces.
91    Field {
92        /// Description.
93        source: String,
94    },
95    /// Input-level description.
96    /// This description get an additional space at the end.
97    Input {
98        /// Description.
99        source: String,
100    },
101}
102
103impl fmt::Display for StringValue {
104    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105        let source = match self {
106            StringValue::Top { source }
107            | StringValue::Field { source }
108            | StringValue::Input { source } => source,
109        };
110        // TODO(@goto-bus-stop): We could instead pass in the indentation as a
111        // fmt parameter whenever a StringValue is printed.
112        let indent = match self {
113            StringValue::Top { .. } => 0,
114            StringValue::Field { .. } => 2,
115            StringValue::Input { .. } => 4,
116        };
117
118        if should_use_block_string(source) {
119            write!(
120                f,
121                "{}",
122                BlockStringFormatter {
123                    string: source,
124                    indent,
125                }
126            )
127        } else {
128            // TODO(@goto-bus-stop) We should probably not prepend the indentation here
129            // but let the caller handle it
130            write!(
131                f,
132                "{:indent$}{string}",
133                "",
134                indent = indent,
135                string = StringFormatter(source),
136            )
137        }
138    }
139}
140
141/// For multi-line strings and strings containing ", try to use a block string.
142/// It's not possible to use a block string if characters would need to be escaped.
143fn should_use_block_string(s: &str) -> bool {
144    s.contains(['"', '\n']) && s.lines().all(|line| !line.contains(char::is_control))
145}
146
147#[cfg(test)]
148mod test {
149    use super::*;
150    use pretty_assertions::assert_eq;
151
152    #[test]
153    fn it_encodes_description_without_block_string_character() {
154        let desc = StringValue::Top {
155            source: "Favourite cat nap spots include: plant corner, pile of clothes.".to_string(),
156        };
157
158        assert_eq!(
159            desc.to_string(),
160            r#""Favourite cat nap spots include: plant corner, pile of clothes.""#
161        );
162    }
163
164    #[test]
165    fn it_encodes_description_with_quotations() {
166        let desc = StringValue::Top {
167            source: r#""Favourite "cat" nap spots include: plant corner, pile of clothes.""#
168                .to_string(),
169        };
170
171        assert_eq!(
172            desc.to_string(),
173            r#""""
174"Favourite "cat" nap spots include: plant corner, pile of clothes."
175""""#
176        );
177    }
178
179    #[test]
180    fn it_encodes_description_with_other_languages() {
181        let desc = StringValue::Top {
182            source: r#"котя(猫, ねこ, قطة) любить дрімати в "кутку" з рослинами"#.to_string(),
183        };
184
185        assert_eq!(
186            desc.to_string(),
187            r#""""
188котя(猫, ねこ, قطة) любить дрімати в "кутку" з рослинами
189""""#
190        );
191    }
192
193    #[test]
194    fn it_encodes_description_with_new_line() {
195        let desc = StringValue::Top {
196            source: "Favourite cat nap spots include:\nplant corner, pile of clothes.".to_string(),
197        };
198
199        assert_eq!(
200            desc.to_string(),
201            r#""""
202Favourite cat nap spots include:
203plant corner, pile of clothes.
204""""#
205        );
206    }
207
208    #[test]
209    fn it_encodes_description_with_carriage_return() {
210        let desc = StringValue::Top {
211            source: "Favourite cat nap spots include:\rplant corner,\rpile of clothes.".to_string(),
212        };
213
214        assert_eq!(
215            desc.to_string(),
216            r#""Favourite cat nap spots include:\rplant corner,\rpile of clothes.""#,
217        );
218    }
219
220    #[test]
221    fn it_encodes_indented_desciption() {
222        let desc = StringValue::Field {
223            source: "Favourite cat nap spots include:\n  plant corner,\n  pile of clothes."
224                .to_string(),
225        };
226
227        assert_eq!(
228            desc.to_string(),
229            r#"  """
230  Favourite cat nap spots include:
231    plant corner,
232    pile of clothes.
233  """"#,
234        );
235
236        let desc = StringValue::Field {
237            source: "Favourite cat nap spots include:\r\n  plant corner,\r\n  pile of clothes."
238                .to_string(),
239        };
240
241        assert_eq!(
242            desc.to_string(),
243            r#"  """
244  Favourite cat nap spots include:
245    plant corner,
246    pile of clothes.
247  """"#,
248        );
249
250        let desc = StringValue::Field {
251            source: "Favourite cat nap spots include:\r  plant corner,\r  pile of clothes."
252                .to_string(),
253        };
254
255        assert_eq!(
256            desc.to_string(),
257            r#"  "Favourite cat nap spots include:\r  plant corner,\r  pile of clothes.""#,
258        );
259    }
260
261    #[test]
262    fn it_encodes_ends_with_quote() {
263        let source = r#"ends with ""#.to_string();
264
265        let desc = StringValue::Top {
266            source: source.clone(),
267        };
268        assert_eq!(
269            desc.to_string(),
270            r#""""
271ends with "
272""""#
273        );
274
275        let desc = StringValue::Field {
276            source: source.clone(),
277        };
278        assert_eq!(
279            desc.to_string(),
280            r#"  """
281  ends with "
282  """"#
283        );
284
285        let desc = StringValue::Input { source };
286        assert_eq!(
287            desc.to_string(),
288            r#"    """
289    ends with "
290    """"#
291        );
292    }
293
294    #[test]
295    fn it_encodes_triple_quotes() {
296        let source = r#"this """ has """ triple """ quotes"#.to_string();
297
298        let desc = StringValue::Top { source };
299        assert_eq!(
300            desc.to_string(),
301            r#""""
302this \""" has \""" triple \""" quotes
303""""#
304        );
305
306        let source = r#"this """ has """" many """"""" quotes"#.to_string();
307
308        let desc = StringValue::Top { source };
309        println!("{desc}");
310        assert_eq!(
311            desc.to_string(),
312            r#""""
313this \""" has \"""" many \"""\"""" quotes
314""""#
315        );
316    }
317
318    #[test]
319    fn it_encodes_control_characters() {
320        let source = "control \u{009c} character".to_string();
321
322        let desc = StringValue::Top { source };
323        assert_eq!(desc.to_string(), r#""control \u009c character""#);
324
325        let source = "multi-line\nwith control \u{009c} character\n :)".to_string();
326
327        let desc = StringValue::Top { source };
328        println!("{desc}");
329        assert_eq!(
330            desc.to_string(),
331            r#""multi-line\nwith control \u009c character\n :)""#
332        );
333    }
334}