graphql_tools/parser/query/
minify.rs

1use crate::parser::{
2    query::{
3        Definition, Directive, Document, FragmentDefinition, OperationDefinition, Selection,
4        SelectionSet, Text, Type, TypeCondition, Value, VariableDefinition,
5    },
6    tokenizer::{Kind, Token, TokenStream},
7};
8use combine::StreamOnce;
9use thiserror::Error;
10
11/// Error minifying query
12#[derive(Error, Debug)]
13#[error("query minify error: {}", _0)]
14pub struct MinifyError(String);
15
16pub fn minify_query(source: &str) -> Result<String, MinifyError> {
17    let mut bits: Vec<&str> = Vec::new();
18    let mut stream = TokenStream::new(source);
19    let mut prev_was_punctuator = false;
20
21    loop {
22        match stream.uncons() {
23            Ok(x) => {
24                let token: Token = x;
25                let is_non_punctuator = token.kind != Kind::Punctuator;
26
27                if prev_was_punctuator && is_non_punctuator {
28                    bits.push(" ");
29                }
30
31                bits.push(token.value);
32                prev_was_punctuator = is_non_punctuator;
33            }
34            Err(ref e) if e == &combine::easy::Error::end_of_input() => break,
35            Err(e) => return Err(MinifyError(e.to_string())),
36        }
37    }
38
39    Ok(bits.join(""))
40}
41
42/// Minify a document according to the same rules as `minify_query`
43pub fn minify_query_document<'a, T: Text<'a>>(doc: &Document<'a, T>) -> String {
44    let mut minifier = Minifier::new();
45    minifier.write_document(doc);
46    minifier.buffer
47}
48
49/// Minifier builds a minified GraphQL query string from a parsed AST.
50///
51/// This struct uses a single-pass traversal of the document AST, writing directly to a buffer
52/// instead of creating intermediate string representations. This approach is significantly
53/// faster than the `minify_query` approach which required converting the AST to a Display string first.
54///
55/// Key optimizations:
56/// - Direct buffer writing avoids intermediate allocations
57/// - Reusable buffers for number formatting (itoa, ryu) reduce allocation overhead
58/// - Tracking `last_was_non_punctuator` allows spacing without post-processing
59struct Minifier {
60    /// Accumulates the minified output as we traverse the AST
61    buffer: String,
62    /// Current indentation level (in spaces). Used only for block string formatting.
63    block_indent: u16,
64    /// Tracks whether the last character written were non-punctuator tokens (identifiers, keywords).
65    /// This is essential for maintaining valid GraphQL syntax: identifiers must be separated by spaces.
66    /// For example: "query Foo" requires a space between "query" and "Foo", but "query{" doesn't.
67    last_was_non_punctuator: bool,
68    /// Reusable buffer for converting integers to strings using the `itoa` crate,
69    /// that's optimized for fast, allocation-free integer formatting.
70    int_buffer: itoa::Buffer,
71    /// Reusable buffer for converting floats to strings using the `ryu` crate.
72    floats_buffer: ryu::Buffer,
73}
74
75impl Minifier {
76    fn new() -> Self {
77        Self {
78            // preallocate a buffer size to minimize reallocations,
79            // most queries will fit within 1KB
80            buffer: String::with_capacity(1024),
81            last_was_non_punctuator: false,
82            block_indent: 2,
83            int_buffer: itoa::Buffer::new(),
84            floats_buffer: ryu::Buffer::new(),
85        }
86    }
87
88    /// Writes a non-punctuator token (identifier, keyword) to the output.
89    ///
90    /// We add a space before this token if the last thing written was also a non-punctuator.
91    /// We mark ourselves as the last token written, so the next non-punctuator knows to add a space.
92    #[inline(always)]
93    fn write_non_punctuator(&mut self, s: &str) {
94        if self.last_was_non_punctuator {
95            self.buffer.push(' ');
96        }
97        self.buffer.push_str(s);
98        self.last_was_non_punctuator = true;
99    }
100
101    /// Writes a punctuator token (operator) to the output without spacing logic.
102    ///
103    /// Punctuators are: {([:!$=@ and so on.
104    /// These never need spaces around them in minified GraphQL (e.g., "a:b", not "a : b").
105    ///
106    /// We reset `last_was_non_punctuator` to false because punctuators don't require spacing before the next token.
107    #[inline(always)]
108    fn write_punctuator(&mut self, s: &str) {
109        self.buffer.push_str(s);
110        self.last_was_non_punctuator = false;
111    }
112
113    /// Writes a single-character punctuator to the output.
114    ///
115    /// This is a single-character variant of `write_punctuator` for better performance.
116    /// Using `push(char)` is faster than `push_str()` for single characters.
117    #[inline(always)]
118    fn write_punctuator_char(&mut self, c: char) {
119        self.buffer.push(c);
120        self.last_was_non_punctuator = false;
121    }
122
123    /// Writes the top-level document by iterating through all definitions
124    #[inline]
125    fn write_document<'a, T: Text<'a>>(&mut self, doc: &Document<'a, T>) {
126        for def in &doc.definitions {
127            self.write_definition(def);
128        }
129    }
130
131    #[inline]
132    fn write_definition<'a, T: Text<'a>>(&mut self, def: &Definition<'a, T>) {
133        match def {
134            Definition::Operation(op) => self.write_operation(op),
135            Definition::Fragment(frag) => self.write_fragment(frag),
136        }
137    }
138
139    #[inline]
140    fn write_operation<'a, T: Text<'a>>(&mut self, op: &OperationDefinition<'a, T>) {
141        match op {
142            OperationDefinition::SelectionSet(set) => self.write_selection_set(set),
143            OperationDefinition::Query(q) => {
144                self.write_non_punctuator("query");
145                if let Some(ref name) = q.name {
146                    self.write_non_punctuator(name.as_ref());
147                }
148                self.write_variable_definitions(&q.variable_definitions);
149                self.write_directives(&q.directives);
150                self.write_selection_set(&q.selection_set);
151            }
152            OperationDefinition::Mutation(m) => {
153                self.write_non_punctuator("mutation");
154                if let Some(ref name) = m.name {
155                    self.write_non_punctuator(name.as_ref());
156                }
157                self.write_variable_definitions(&m.variable_definitions);
158                self.write_directives(&m.directives);
159                self.write_selection_set(&m.selection_set);
160            }
161            OperationDefinition::Subscription(s) => {
162                self.write_non_punctuator("subscription");
163                if let Some(ref name) = s.name {
164                    self.write_non_punctuator(name.as_ref());
165                }
166                self.write_variable_definitions(&s.variable_definitions);
167                self.write_directives(&s.directives);
168                self.write_selection_set(&s.selection_set);
169            }
170        }
171    }
172
173    #[inline]
174    fn write_fragment<'a, T: Text<'a>>(&mut self, frag: &FragmentDefinition<'a, T>) {
175        self.write_non_punctuator("fragment");
176        self.write_non_punctuator(frag.name.as_ref());
177        self.write_type_condition(&frag.type_condition);
178        self.write_directives(&frag.directives);
179        self.write_selection_set(&frag.selection_set);
180    }
181
182    /// No separators between items,
183    /// each item's trailing punctuation determines spacing.
184    #[inline]
185    fn write_selection_set<'a, T: Text<'a>>(&mut self, set: &SelectionSet<'a, T>) {
186        self.write_punctuator_char('{');
187        for item in &set.items {
188            self.write_selection(item);
189        }
190        self.write_punctuator_char('}');
191    }
192
193    #[inline]
194    fn write_selection<'a, T: Text<'a>>(&mut self, selection: &Selection<'a, T>) {
195        match selection {
196            Selection::Field(f) => {
197                if let Some(ref alias) = f.alias {
198                    self.write_non_punctuator(alias.as_ref());
199                    self.write_punctuator_char(':');
200                }
201                self.write_non_punctuator(f.name.as_ref());
202                self.write_arguments(&f.arguments);
203                self.write_directives(&f.directives);
204                if !f.selection_set.items.is_empty() {
205                    self.write_selection_set(&f.selection_set);
206                }
207            }
208            Selection::FragmentSpread(fs) => {
209                self.write_punctuator("...");
210                self.write_non_punctuator(fs.fragment_name.as_ref());
211                self.write_directives(&fs.directives);
212            }
213            Selection::InlineFragment(ifrag) => {
214                self.write_punctuator("...");
215                if let Some(ref tc) = ifrag.type_condition {
216                    self.write_type_condition(tc);
217                }
218                self.write_directives(&ifrag.directives);
219                self.write_selection_set(&ifrag.selection_set);
220            }
221        }
222    }
223
224    #[inline]
225    fn write_type_condition<'a, T: Text<'a>>(&mut self, tc: &TypeCondition<'a, T>) {
226        match tc {
227            TypeCondition::On(name) => {
228                self.write_non_punctuator("on");
229                self.write_non_punctuator(name.as_ref());
230            }
231        }
232    }
233
234    #[inline]
235    fn write_variable_definitions<'a, T: Text<'a>>(&mut self, vars: &[VariableDefinition<'a, T>]) {
236        if vars.is_empty() {
237            return;
238        }
239        self.write_punctuator_char('(');
240        for var in vars {
241            self.write_punctuator_char('$');
242            self.write_non_punctuator(var.name.as_ref());
243            self.write_punctuator_char(':');
244            self.write_type(&var.var_type);
245            if let Some(ref def) = var.default_value {
246                self.write_punctuator_char('=');
247                self.write_value(def);
248            }
249        }
250        self.write_punctuator_char(')');
251    }
252
253    #[inline]
254    fn write_type<'a, T: Text<'a>>(&mut self, ty: &Type<'a, T>) {
255        match ty {
256            Type::NamedType(name) => self.write_non_punctuator(name.as_ref()),
257            Type::ListType(inner) => {
258                self.write_punctuator_char('[');
259                self.write_type(inner);
260                self.write_punctuator_char(']');
261            }
262            Type::NonNullType(inner) => {
263                self.write_type(inner);
264                self.write_punctuator_char('!');
265            }
266        }
267    }
268
269    #[inline]
270    fn write_directives<'a, T: Text<'a>>(&mut self, dirs: &[Directive<'a, T>]) {
271        for dir in dirs {
272            self.write_punctuator_char('@');
273            self.write_non_punctuator(dir.name.as_ref());
274            self.write_arguments(&dir.arguments);
275        }
276    }
277
278    #[inline]
279    fn write_arguments<'a, T: Text<'a>>(&mut self, args: &[(T::Value, Value<'a, T>)]) {
280        if args.is_empty() {
281            return;
282        }
283        self.write_punctuator_char('(');
284        for (name, val) in args {
285            self.write_non_punctuator(name.as_ref());
286            self.write_punctuator_char(':');
287            self.write_value(val);
288        }
289        self.write_punctuator_char(')');
290    }
291
292    /// Writes a string value with proper escaping and formatting.
293    ///
294    /// This method handles three different cases to optimize for common scenarios:
295    ///
296    /// Case 1: Simple strings (the fast path)
297    /// If the string contains no escaping-needed characters and no newlines, we write it
298    /// directly with quotes. This is the most common case and avoids character-by-character scanning.
299    ///
300    /// Case 2: Strings needing escaping but no newlines
301    /// We iterate through each character and escape special characters:
302    /// - Control characters get special escapes
303    /// - Quotes and backslashes: escaped with backslash
304    /// - Regular characters: copied as-is
305    ///
306    /// Case 3: Block strings (multi-line with newlines)
307    /// GraphQL supports block strings (triple quotes) for multi-line content.
308    /// Block strings preserve line breaks, escape triple-quote sequences, and use indentation.
309    /// We use this format if the string contains newlines.
310    ///
311    /// Spacing: Strings are non-punctuators that require spacing when preceded by another non-punctuator.
312    /// We add the space at the beginning if needed, maintaining `last_was_non_punctuator = true`.
313    #[inline(always)]
314    pub fn write_quoted(&mut self, s: &str) {
315        if self.last_was_non_punctuator {
316            self.buffer.push(' ');
317        }
318
319        let bytes = s.as_bytes();
320        let mut has_newline = false;
321        let mut needs_escaping = false;
322
323        for &byte in bytes {
324            if byte == b'\n' {
325                has_newline = true;
326            // Check for control characters that need escaping. The parser accepts raw control characters in strings,
327            // so we must escape them here to produce valid minified output.
328            } else if byte == b'"' || byte == b'\\' || byte < 0x20 || byte == 0x7F {
329                needs_escaping = true;
330            }
331
332            if has_newline && needs_escaping {
333                break;
334            }
335        }
336
337        if !needs_escaping && !has_newline {
338            self.buffer.reserve(s.len() + 2);
339            self.buffer.push('"');
340            self.buffer.push_str(s);
341            self.buffer.push('"');
342            self.last_was_non_punctuator = true;
343            return;
344        }
345
346        if !has_newline {
347            use std::fmt::Write;
348            // Reserve extra space for escape sequences. Most strings need 2 bytes for quotes,
349            // and ~16 bytes accounts for typical escape sequences (e.g., \", \\, \u00XX).
350            // The buffer will grow dynamically if this estimate is too small.
351            self.buffer.reserve(s.len() + 16);
352            self.buffer.push('"');
353            for c in s.chars() {
354                match c {
355                    '\r' => self.buffer.push_str(r"\r"),
356                    '\n' => self.buffer.push_str(r"\n"),
357                    '\t' => self.buffer.push_str(r"\t"),
358                    '"' => self.buffer.push_str("\\\""),
359                    '\\' => self.buffer.push_str(r"\\"),
360                    // Regular text characters. These are safe to write directly without escaping.
361                    '\u{0020}'..='\u{FFFF}' => self.buffer.push(c),
362                    // Characters outside the printable range are escaped as \uXXXX.
363                    _ => write!(&mut self.buffer, "\\u{:04}", c as u32).unwrap(),
364                }
365            }
366            self.buffer.push('"');
367        } else {
368            // Block strings can expand significantly with indentation and escape sequences.
369            // Reserve space upfront to avoid repeated reallocations.
370            self.buffer.reserve(s.len() + 32);
371            self.buffer.push_str(r#"""""#);
372            self.buffer.push('\n');
373
374            self.block_indent += 2;
375
376            for line in s.lines() {
377                if !line.trim().is_empty() {
378                    self.indent();
379                    let mut last_pos = 0;
380                    for (pos, _) in line.match_indices(r#"""""#) {
381                        self.buffer.push_str(&line[last_pos..pos]);
382                        self.buffer.push_str(r#"\"""#);
383                        last_pos = pos + 3;
384                    }
385                    self.buffer.push_str(&line[last_pos..]);
386                }
387                self.buffer.push('\n');
388            }
389
390            self.block_indent -= 2;
391            self.indent();
392
393            self.buffer.push_str(r#"""""#);
394        }
395        self.last_was_non_punctuator = true;
396    }
397
398    /// Writes the current indentation level (in spaces).
399    #[inline]
400    pub fn indent(&mut self) {
401        for _ in 0..self.block_indent {
402            self.buffer.push(' ');
403        }
404    }
405
406    #[inline]
407    fn write_value<'a, T: Text<'a>>(&mut self, val: &Value<'a, T>) {
408        match val {
409            Value::Variable(name) => {
410                self.write_punctuator_char('$');
411                self.write_non_punctuator(name.as_ref());
412            }
413            Value::Int(n) => {
414                // Use itoa's format method for fast, accurate integer-to-string conversion.
415                // itoa is much faster than standard Rust integer formatting and produces
416                // minimal output (no unnecessary padding or precision).
417                let s = self.int_buffer.format(n.0);
418                if self.last_was_non_punctuator {
419                    self.buffer.push(' ');
420                }
421                self.buffer.push_str(s);
422                self.last_was_non_punctuator = true;
423            }
424            Value::Float(f) => {
425                // Use ryu's format method for fast, accurate float-to-string conversion.
426                // ryu produces the shortest decimal representation that correctly round-trips,
427                // which is more efficient than using Display or other methods.
428                let s = self.floats_buffer.format(*f);
429                if self.last_was_non_punctuator {
430                    self.buffer.push(' ');
431                }
432                self.buffer.push_str(s);
433                self.last_was_non_punctuator = true;
434            }
435            Value::String(s) => self.write_quoted(s),
436            Value::Boolean(b) => self.write_non_punctuator(if *b { "true" } else { "false" }),
437            Value::Null => self.write_non_punctuator("null"),
438            Value::Enum(name) => self.write_non_punctuator(name.as_ref()),
439            Value::List(items) => {
440                self.write_punctuator_char('[');
441                for item in items {
442                    self.write_value(item);
443                }
444                self.write_punctuator_char(']');
445            }
446            Value::Object(fields) => {
447                self.write_punctuator_char('{');
448                for (name, val) in fields {
449                    self.write_non_punctuator(name.as_ref());
450                    self.write_punctuator_char(':');
451                    self.write_value(val);
452                }
453                self.write_punctuator_char('}');
454            }
455        }
456    }
457}
458
459#[cfg(test)]
460mod tests {
461    #[test]
462    fn strip_ignored_characters() {
463        let source = "
464        query SomeQuery($foo: String!, $bar: String) {
465            someField(foo: $foo, bar: $bar) {
466                a
467                b {
468                    ... on B {
469                        c
470                        d
471                    }
472                }
473            }
474        }
475        ";
476
477        let minified =
478            super::minify_query(source.to_string().as_str()).expect("minification failed");
479
480        assert_eq!(
481            &minified,
482            "query SomeQuery($foo:String!$bar:String){someField(foo:$foo bar:$bar){a b{...on B{c d}}}}"
483        );
484    }
485
486    #[test]
487    fn unexpected_token() {
488        let source = "
489        query foo {
490            bar;
491        }
492        ";
493
494        let minified = super::minify_query(source.to_string().as_str());
495
496        assert!(minified.is_err());
497
498        assert_eq!(
499            minified.unwrap_err().to_string(),
500            "query minify error: Unexpected unexpected character ';'"
501        );
502    }
503
504    #[test]
505    fn minify_document_test() {
506        let source = "
507        query SomeQuery($foo: String!, $bar: String) {
508            someField(foo: $foo, bar: $bar) {
509                a
510                b {
511                    ... on B {
512                        c
513                        d
514                    }
515                }
516            }
517        }
518        ";
519
520        let doc =
521            crate::parser::query::grammar::parse_query::<String>(source).expect("parse failed");
522        let minified_doc = super::minify_query_document(&doc);
523        let minified_query = super::minify_query(source).expect("minification failed");
524
525        assert_eq!(minified_doc, minified_query);
526    }
527
528    #[test]
529    fn minify_document_complex() {
530        let source = r#"
531        mutation DoSomething($input: UpdateInput! = { a: 1, b: "foo" }) @opt(level: 1) {
532            updateItem(id: "123", data: $input) {
533                id
534                ... on Item {
535                    name
536                    tags
537                }
538                ...FragmentName
539            }
540        }
541        fragment FragmentName on Item {
542            owner {
543                id
544                email
545            }
546        }
547        "#;
548
549        let doc =
550            crate::parser::query::grammar::parse_query::<String>(source).expect("parse failed");
551        let minified_doc = super::minify_query_document(&doc);
552        let minified_query = super::minify_query(source).expect("minification failed");
553
554        assert_eq!(minified_doc, minified_query);
555    }
556
557    #[test]
558    fn test_minify_directive_args() {
559        let source =
560            std::fs::read_to_string("src/parser/tests/queries/directive_args.graphql").unwrap();
561        let minified_query = super::minify_query(&source).unwrap();
562        // In all the tests below, I first use snapshots to assert the output of `minify_query`,
563        // and only then compare it with the output of `minify_query_document`.
564        // While I could compare the outputs directly without snapshots,
565        // this setup makes sure that future changes in either function are caught independently.
566        insta::assert_snapshot!(minified_query, @r#"query{node@dir(a:1 b:"2" c:true d:false e:null)}"#);
567
568        let minified_document = super::minify_query_document(
569            &crate::parser::query::grammar::parse_query::<String>(&source).unwrap(),
570        );
571        assert_eq!(
572            minified_query, minified_document,
573            "minify_query and minify_document outputs differ"
574        );
575    }
576
577    #[test]
578    fn test_minify_directive_args_multiline() {
579        let source =
580            std::fs::read_to_string("src/parser/tests/queries/directive_args_multiline.graphql")
581                .unwrap();
582        let minified_query = super::minify_query(&source).unwrap();
583        insta::assert_snapshot!(minified_query, @r#"query{node@dir(a:1 b:"2" c:true d:false e:null)}"#);
584
585        let minified_document = super::minify_query_document(
586            &crate::parser::query::grammar::parse_query::<String>(&source).unwrap(),
587        );
588        assert_eq!(
589            minified_query, minified_document,
590            "minify_query and minify_document outputs differ"
591        );
592    }
593
594    #[test]
595    fn test_minify_fragment() {
596        let source = std::fs::read_to_string("src/parser/tests/queries/fragment.graphql").unwrap();
597        let minified_query = super::minify_query(&source).unwrap();
598        insta::assert_snapshot!(minified_query, @"fragment frag on Friend{node}");
599
600        let minified_document = super::minify_query_document(
601            &crate::parser::query::grammar::parse_query::<String>(&source).unwrap(),
602        );
603        assert_eq!(
604            minified_query, minified_document,
605            "minify_query and minify_document outputs differ"
606        );
607    }
608
609    #[test]
610    fn test_minify_fragment_spread() {
611        let source =
612            std::fs::read_to_string("src/parser/tests/queries/fragment_spread.graphql").unwrap();
613        let minified_query = super::minify_query(&source).unwrap();
614        insta::assert_snapshot!(minified_query, @"query{node{id...something}}");
615
616        let minified_document = super::minify_query_document(
617            &crate::parser::query::grammar::parse_query::<String>(&source).unwrap(),
618        );
619        assert_eq!(
620            minified_query, minified_document,
621            "minify_query and minify_document outputs differ"
622        );
623    }
624
625    #[test]
626    fn test_minify_inline_fragment() {
627        let source =
628            std::fs::read_to_string("src/parser/tests/queries/inline_fragment.graphql").unwrap();
629        let minified_query = super::minify_query(&source).unwrap();
630        insta::assert_snapshot!(minified_query, @"query{node{id...on User{name}}}");
631
632        let minified_document = super::minify_query_document(
633            &crate::parser::query::grammar::parse_query::<String>(&source).unwrap(),
634        );
635        assert_eq!(
636            minified_query, minified_document,
637            "minify_query and minify_document outputs differ"
638        );
639    }
640
641    #[test]
642    fn test_minify_inline_fragment_dir() {
643        let source =
644            std::fs::read_to_string("src/parser/tests/queries/inline_fragment_dir.graphql")
645                .unwrap();
646        let minified_query = super::minify_query(&source).unwrap();
647        insta::assert_snapshot!(minified_query, @"query{node{id...on User@defer{name}}}");
648
649        let minified_document = super::minify_query_document(
650            &crate::parser::query::grammar::parse_query::<String>(&source).unwrap(),
651        );
652        assert_eq!(
653            minified_query, minified_document,
654            "minify_query and minify_document outputs differ"
655        );
656    }
657
658    #[test]
659    fn test_minify_kitchen_sink() {
660        let source =
661            std::fs::read_to_string("src/parser/tests/queries/kitchen-sink.graphql").unwrap();
662        let minified_query = super::minify_query(&source).unwrap();
663        insta::assert_snapshot!(minified_query, @r#"
664        query queryName($foo:ComplexType$site:Site=MOBILE){whoever123is:node(id:[123 456]){id...on User@defer{field2{id alias:field1(first:10 after:$foo)@include(if:$foo){id...frag}}}...@skip(unless:$foo){id}...{id}}}mutation likeStory{like(story:123)@defer{story{id}}}subscription StoryLikeSubscription($input:StoryLikeSubscribeInput){storyLikeSubscribe(input:$input){story{likers{count}likeSentence{text}}}}fragment frag on Friend{foo(size:$size bar:$b obj:{key:"value" block:"""
665
666              block string uses \"""
667
668          """})}{unnamed(truthy:true falsey:false nullish:null)query}
669        "#);
670
671        let minified_document = super::minify_query_document(
672            &crate::parser::query::grammar::parse_query::<String>(&source).unwrap(),
673        );
674
675        insta::assert_snapshot!(minified_document, @r#"query queryName($foo:ComplexType$site:Site=MOBILE){whoever123is:node(id:[123 456]){id...on User@defer{field2{id alias:field1(first:10 after:$foo)@include(if:$foo){id...frag}}}...@skip(unless:$foo){id}...{id}}}mutation likeStory{like(story:123)@defer{story{id}}}subscription StoryLikeSubscription($input:StoryLikeSubscribeInput){storyLikeSubscribe(input:$input){story{likers{count}likeSentence{text}}}}fragment frag on Friend{foo(size:$size bar:$b obj:{block:"block string uses \"\"\"" key:"value"})}{unnamed(truthy:true falsey:false nullish:null)query}"#);
676
677        // The output is different because the parsed AST normalizes multiline strings,
678        // while minify_query preserves the original formatting.
679        // The minify_document has no idea about the original formatting.
680    }
681
682    #[test]
683    fn test_minify_kitchen_sink_canonical() {
684        let source =
685            std::fs::read_to_string("src/parser/tests/queries/kitchen-sink_canonical.graphql")
686                .unwrap();
687        let minified_query = super::minify_query(&source).unwrap();
688        insta::assert_snapshot!(minified_query, @r#"query queryName($foo:ComplexType$site:Site=MOBILE){whoever123is:node(id:[123 456]){id...on User@defer{field2{id alias:field1(first:10 after:$foo)@include(if:$foo){id...frag}}}...@skip(unless:$foo){id}...{id}}}mutation likeStory{like(story:123)@defer{story{id}}}subscription StoryLikeSubscription($input:StoryLikeSubscribeInput){storyLikeSubscribe(input:$input){story{likers{count}likeSentence{text}}}}fragment frag on Friend{foo(size:$size bar:$b obj:{block:"block string uses \"\"\"" key:"value"})}{unnamed(truthy:true falsey:false nullish:null)query}"#);
689
690        let minified_document = super::minify_query_document(
691            &crate::parser::query::grammar::parse_query::<String>(&source).unwrap(),
692        );
693        assert_eq!(
694            minified_query, minified_document,
695            "minify_query and minify_document outputs differ"
696        );
697    }
698
699    #[test]
700    fn test_minify_minimal() {
701        let source = std::fs::read_to_string("src/parser/tests/queries/minimal.graphql").unwrap();
702        let minified_query = super::minify_query(&source).unwrap();
703        insta::assert_snapshot!(minified_query, @"{a}");
704
705        let minified_document = super::minify_query_document(
706            &crate::parser::query::grammar::parse_query::<String>(&source).unwrap(),
707        );
708        assert_eq!(
709            minified_query, minified_document,
710            "minify_query and minify_document outputs differ"
711        );
712    }
713
714    #[test]
715    fn test_minify_minimal_mutation() {
716        let source =
717            std::fs::read_to_string("src/parser/tests/queries/minimal_mutation.graphql").unwrap();
718        let minified_query = super::minify_query(&source).unwrap();
719        insta::assert_snapshot!(minified_query, @"mutation{notify}");
720
721        let minified_document = super::minify_query_document(
722            &crate::parser::query::grammar::parse_query::<String>(&source).unwrap(),
723        );
724        assert_eq!(
725            minified_query, minified_document,
726            "minify_query and minify_document outputs differ"
727        );
728    }
729
730    #[test]
731    fn test_minify_minimal_query() {
732        let source =
733            std::fs::read_to_string("src/parser/tests/queries/minimal_query.graphql").unwrap();
734        let minified_query = super::minify_query(&source).unwrap();
735        insta::assert_snapshot!(minified_query, @"query{node}");
736
737        let minified_document = super::minify_query_document(
738            &crate::parser::query::grammar::parse_query::<String>(&source).unwrap(),
739        );
740        assert_eq!(
741            minified_query, minified_document,
742            "minify_query and minify_document outputs differ"
743        );
744    }
745
746    #[test]
747    fn test_minify_mutation_directive() {
748        let source =
749            std::fs::read_to_string("src/parser/tests/queries/mutation_directive.graphql").unwrap();
750        let minified_query = super::minify_query(&source).unwrap();
751        insta::assert_snapshot!(minified_query, @"mutation@directive{node}");
752
753        let minified_document = super::minify_query_document(
754            &crate::parser::query::grammar::parse_query::<String>(&source).unwrap(),
755        );
756        assert_eq!(
757            minified_query, minified_document,
758            "minify_query and minify_document outputs differ"
759        );
760    }
761
762    #[test]
763    fn test_minify_mutation_nameless_vars() {
764        let source =
765            std::fs::read_to_string("src/parser/tests/queries/mutation_nameless_vars.graphql")
766                .unwrap();
767        let minified_query = super::minify_query(&source).unwrap();
768        insta::assert_snapshot!(minified_query, @"mutation($first:Int$second:Int){field1(first:$first)field2(second:$second)}");
769
770        let minified_document = super::minify_query_document(
771            &crate::parser::query::grammar::parse_query::<String>(&source).unwrap(),
772        );
773        assert_eq!(
774            minified_query, minified_document,
775            "minify_query and minify_document outputs differ"
776        );
777    }
778
779    #[test]
780    fn test_minify_named_query() {
781        let source =
782            std::fs::read_to_string("src/parser/tests/queries/named_query.graphql").unwrap();
783        let minified_query = super::minify_query(&source).unwrap();
784        insta::assert_snapshot!(minified_query, @"query Foo{field}");
785
786        let minified_document = super::minify_query_document(
787            &crate::parser::query::grammar::parse_query::<String>(&source).unwrap(),
788        );
789        assert_eq!(
790            minified_query, minified_document,
791            "minify_query and minify_document outputs differ"
792        );
793    }
794
795    #[test]
796    fn test_minify_nested_selection() {
797        let source =
798            std::fs::read_to_string("src/parser/tests/queries/nested_selection.graphql").unwrap();
799        let minified_query = super::minify_query(&source).unwrap();
800        insta::assert_snapshot!(minified_query, @"query{node{id}}");
801
802        let minified_document = super::minify_query_document(
803            &crate::parser::query::grammar::parse_query::<String>(&source).unwrap(),
804        );
805        assert_eq!(
806            minified_query, minified_document,
807            "minify_query and minify_document outputs differ"
808        );
809    }
810
811    #[test]
812    fn test_minify_query_aliases() {
813        let source =
814            std::fs::read_to_string("src/parser/tests/queries/query_aliases.graphql").unwrap();
815        let minified_query = super::minify_query(&source).unwrap();
816        insta::assert_snapshot!(minified_query, @"query{an_alias:node}");
817
818        let minified_document = super::minify_query_document(
819            &crate::parser::query::grammar::parse_query::<String>(&source).unwrap(),
820        );
821        assert_eq!(
822            minified_query, minified_document,
823            "minify_query and minify_document outputs differ"
824        );
825    }
826
827    #[test]
828    fn test_minify_query_arguments() {
829        let source =
830            std::fs::read_to_string("src/parser/tests/queries/query_arguments.graphql").unwrap();
831        let minified_query = super::minify_query(&source).unwrap();
832        insta::assert_snapshot!(minified_query, @"query{node(id:1)}");
833
834        let minified_document = super::minify_query_document(
835            &crate::parser::query::grammar::parse_query::<String>(&source).unwrap(),
836        );
837        assert_eq!(
838            minified_query, minified_document,
839            "minify_query and minify_document outputs differ"
840        );
841    }
842
843    #[test]
844    fn test_minify_query_arguments_multiline() {
845        let source =
846            std::fs::read_to_string("src/parser/tests/queries/query_arguments_multiline.graphql")
847                .unwrap();
848        let minified_query = super::minify_query(&source).unwrap();
849        insta::assert_snapshot!(minified_query, @"query{node(id:1)node(id:1 one:3)}");
850
851        let minified_document = super::minify_query_document(
852            &crate::parser::query::grammar::parse_query::<String>(&source).unwrap(),
853        );
854        assert_eq!(
855            minified_query, minified_document,
856            "minify_query and minify_document outputs differ"
857        );
858    }
859
860    #[test]
861    fn test_minify_query_array_argument_multiline() {
862        let source = std::fs::read_to_string(
863            "src/parser/tests/queries/query_array_argument_multiline.graphql",
864        )
865        .unwrap();
866        let minified_query = super::minify_query(&source).unwrap();
867        insta::assert_snapshot!(minified_query, @"query{node(id:[5 6 7])}");
868
869        let minified_document = super::minify_query_document(
870            &crate::parser::query::grammar::parse_query::<String>(&source).unwrap(),
871        );
872        assert_eq!(
873            minified_query, minified_document,
874            "minify_query and minify_document outputs differ"
875        );
876    }
877
878    #[test]
879    fn test_minify_query_directive() {
880        let source =
881            std::fs::read_to_string("src/parser/tests/queries/query_directive.graphql").unwrap();
882        let minified_query = super::minify_query(&source).unwrap();
883        insta::assert_snapshot!(minified_query, @"query@directive{node}");
884
885        let minified_document = super::minify_query_document(
886            &crate::parser::query::grammar::parse_query::<String>(&source).unwrap(),
887        );
888        assert_eq!(
889            minified_query, minified_document,
890            "minify_query and minify_document outputs differ"
891        );
892    }
893
894    #[test]
895    fn test_minify_query_list_argument() {
896        let source =
897            std::fs::read_to_string("src/parser/tests/queries/query_list_argument.graphql")
898                .unwrap();
899        let minified_query = super::minify_query(&source).unwrap();
900        insta::assert_snapshot!(minified_query, @"query{node(id:1 list:[123 456])}");
901
902        let minified_document = super::minify_query_document(
903            &crate::parser::query::grammar::parse_query::<String>(&source).unwrap(),
904        );
905        assert_eq!(
906            minified_query, minified_document,
907            "minify_query and minify_document outputs differ"
908        );
909    }
910
911    #[test]
912    fn test_minify_query_nameless_vars() {
913        let source =
914            std::fs::read_to_string("src/parser/tests/queries/query_nameless_vars.graphql")
915                .unwrap();
916        let minified_query = super::minify_query(&source).unwrap();
917        insta::assert_snapshot!(minified_query, @"query($first:Int$second:Int){field1(first:$first)field2(second:$second)}");
918
919        let minified_document = super::minify_query_document(
920            &crate::parser::query::grammar::parse_query::<String>(&source).unwrap(),
921        );
922        assert_eq!(
923            minified_query, minified_document,
924            "minify_query and minify_document outputs differ"
925        );
926    }
927
928    #[test]
929    fn test_minify_query_nameless_vars_multiple_fields() {
930        let source = std::fs::read_to_string(
931            "src/parser/tests/queries/query_nameless_vars_multiple_fields.graphql",
932        )
933        .unwrap();
934        let minified_query = super::minify_query(&source).unwrap();
935        insta::assert_snapshot!(minified_query, @"query($houseId:String!$streetNumber:Int!){house(id:$houseId){id name lat lng}street(number:$streetNumber){id}houseStreet(id:$houseId number:$streetNumber){id}}");
936
937        let minified_document = super::minify_query_document(
938            &crate::parser::query::grammar::parse_query::<String>(&source).unwrap(),
939        );
940        assert_eq!(
941            minified_query, minified_document,
942            "minify_query and minify_document outputs differ"
943        );
944    }
945
946    #[test]
947    fn test_minify_query_nameless_vars_multiple_fields_canonical() {
948        let source = std::fs::read_to_string(
949            "src/parser/tests/queries/query_nameless_vars_multiple_fields_canonical.graphql",
950        )
951        .unwrap();
952        let minified_query = super::minify_query(&source).unwrap();
953        insta::assert_snapshot!(minified_query, @"query($houseId:String!$streetNumber:Int!){house(id:$houseId){id name lat lng}street(number:$streetNumber){id}houseStreet(id:$houseId number:$streetNumber){id}}");
954
955        let minified_document = super::minify_query_document(
956            &crate::parser::query::grammar::parse_query::<String>(&source).unwrap(),
957        );
958
959        assert_eq!(
960            minified_query, minified_document,
961            "minify_query and minify_document outputs differ"
962        );
963    }
964
965    #[test]
966    fn test_minify_query_object_argument() {
967        let source =
968            std::fs::read_to_string("src/parser/tests/queries/query_object_argument.graphql")
969                .unwrap();
970        let minified_query = super::minify_query(&source).unwrap();
971        insta::assert_snapshot!(minified_query, @"query{node(id:1 obj:{key1:123 key2:456})}");
972
973        let minified_document = super::minify_query_document(
974            &crate::parser::query::grammar::parse_query::<String>(&source).unwrap(),
975        );
976        assert_eq!(
977            minified_query, minified_document,
978            "minify_query and minify_document outputs differ"
979        );
980    }
981
982    #[test]
983    fn test_minify_query_object_argument_multiline() {
984        let source = std::fs::read_to_string(
985            "src/parser/tests/queries/query_object_argument_multiline.graphql",
986        )
987        .unwrap();
988        let minified_query = super::minify_query(&source).unwrap();
989        insta::assert_snapshot!(minified_query, @"query{node(id:1 obj:{key1:123 key2:456})}");
990
991        let minified_document = super::minify_query_document(
992            &crate::parser::query::grammar::parse_query::<String>(&source).unwrap(),
993        );
994        assert_eq!(
995            minified_query, minified_document,
996            "minify_query and minify_document outputs differ"
997        );
998    }
999
1000    #[test]
1001    fn test_minify_query_var_default_float() {
1002        let source =
1003            std::fs::read_to_string("src/parser/tests/queries/query_var_default_float.graphql")
1004                .unwrap();
1005        let minified_query = super::minify_query(&source).unwrap();
1006        insta::assert_snapshot!(minified_query, @"query Foo($site:Float=0.5){field}");
1007
1008        let minified_document = super::minify_query_document(
1009            &crate::parser::query::grammar::parse_query::<String>(&source).unwrap(),
1010        );
1011        assert_eq!(
1012            minified_query, minified_document,
1013            "minify_query and minify_document outputs differ"
1014        );
1015    }
1016
1017    #[test]
1018    fn test_minify_query_var_default_list() {
1019        let source =
1020            std::fs::read_to_string("src/parser/tests/queries/query_var_default_list.graphql")
1021                .unwrap();
1022        let minified_query = super::minify_query(&source).unwrap();
1023        insta::assert_snapshot!(minified_query, @"query Foo($site:[Int]=[123 456]){field}");
1024
1025        let minified_document = super::minify_query_document(
1026            &crate::parser::query::grammar::parse_query::<String>(&source).unwrap(),
1027        );
1028        assert_eq!(
1029            minified_query, minified_document,
1030            "minify_query and minify_document outputs differ"
1031        );
1032    }
1033
1034    #[test]
1035    fn test_minify_query_var_default_object() {
1036        let source =
1037            std::fs::read_to_string("src/parser/tests/queries/query_var_default_object.graphql")
1038                .unwrap();
1039        let minified_query = super::minify_query(&source).unwrap();
1040        insta::assert_snapshot!(minified_query, @"query Foo($site:Site={url:null}){field}");
1041
1042        let minified_document = super::minify_query_document(
1043            &crate::parser::query::grammar::parse_query::<String>(&source).unwrap(),
1044        );
1045        assert_eq!(
1046            minified_query, minified_document,
1047            "minify_query and minify_document outputs differ"
1048        );
1049    }
1050
1051    #[test]
1052    fn test_minify_query_var_default_string() {
1053        let source =
1054            std::fs::read_to_string("src/parser/tests/queries/query_var_default_string.graphql")
1055                .unwrap();
1056        let minified_query = super::minify_query(&source).unwrap();
1057        insta::assert_snapshot!(minified_query, @r#"query Foo($site:String="string"){field}"#);
1058
1059        let minified_document = super::minify_query_document(
1060            &crate::parser::query::grammar::parse_query::<String>(&source).unwrap(),
1061        );
1062        assert_eq!(
1063            minified_query, minified_document,
1064            "minify_query and minify_document outputs differ"
1065        );
1066    }
1067
1068    #[test]
1069    fn test_minify_query_var_defaults() {
1070        let source =
1071            std::fs::read_to_string("src/parser/tests/queries/query_var_defaults.graphql").unwrap();
1072        let minified_query = super::minify_query(&source).unwrap();
1073        insta::assert_snapshot!(minified_query, @"query Foo($site:Site=MOBILE){field}");
1074    }
1075
1076    #[test]
1077    fn test_minify_query_vars() {
1078        let source =
1079            std::fs::read_to_string("src/parser/tests/queries/query_vars.graphql").unwrap();
1080        let minified_query = super::minify_query(&source).unwrap();
1081        insta::assert_snapshot!(minified_query, @"query Foo($arg:SomeType){field}");
1082
1083        let minified_document = super::minify_query_document(
1084            &crate::parser::query::grammar::parse_query::<String>(&source).unwrap(),
1085        );
1086        assert_eq!(
1087            minified_query, minified_document,
1088            "minify_query and minify_document outputs differ"
1089        );
1090    }
1091
1092    #[test]
1093    fn test_minify_string_literal() {
1094        let source =
1095            std::fs::read_to_string("src/parser/tests/queries/string_literal.graphql").unwrap();
1096        let minified_query = super::minify_query(&source).unwrap();
1097        insta::assert_snapshot!(minified_query, @r#"query{node(id:"hello")}"#);
1098
1099        let minified_document = super::minify_query_document(
1100            &crate::parser::query::grammar::parse_query::<String>(&source).unwrap(),
1101        );
1102        assert_eq!(
1103            minified_query, minified_document,
1104            "minify_query and minify_document outputs differ"
1105        );
1106    }
1107
1108    #[test]
1109    fn test_minify_subscription_directive() {
1110        let source =
1111            std::fs::read_to_string("src/parser/tests/queries/subscription_directive.graphql")
1112                .unwrap();
1113        let minified_query = super::minify_query(&source).unwrap();
1114        insta::assert_snapshot!(minified_query, @"subscription@directive{node}");
1115
1116        let minified_document = super::minify_query_document(
1117            &crate::parser::query::grammar::parse_query::<String>(&source).unwrap(),
1118        );
1119        assert_eq!(
1120            minified_query, minified_document,
1121            "minify_query and minify_document outputs differ"
1122        );
1123    }
1124
1125    #[test]
1126    fn test_minify_triple_quoted_literal() {
1127        let source =
1128            std::fs::read_to_string("src/parser/tests/queries/triple_quoted_literal.graphql")
1129                .unwrap();
1130        let minified_query = super::minify_query(&source).unwrap();
1131        insta::assert_snapshot!(minified_query, @r#"
1132        query{node(id:"""
1133            Hello,
1134              world!
1135          """)}
1136        "#);
1137
1138        let minified_document = super::minify_query_document(
1139            &crate::parser::query::grammar::parse_query::<String>(&source).unwrap(),
1140        );
1141        assert_eq!(
1142            minified_query, minified_document,
1143            "minify_query and minify_document outputs differ"
1144        );
1145    }
1146}