From e80d6ba6ba530af70df69f99b68d8a0aab4e5164 Mon Sep 17 00:00:00 2001 From: Tom Gasson Date: Thu, 29 Sep 2022 21:40:53 -0700 Subject: [PATCH] Compact GraphQL query text (#3983) Summary: We noticed that some of our query text contains a significant amount of unnecessary whitespace. Unlike server->client requests, client->server requests generally aren't compressed and would require a js compression library to do so. This PR adds a feature flag to compact the query text. I saw the pre-existing `compact` mode wasn't wired up to anything, so I've co-opted it. As an example of the benefit, we have one query which this shrinks from `84kb` to `31kb` We're slowly [making our way to persisted queries](https://github.com/facebook/relay/pull/3917), but this is an immediate improvement and I imagine we're not the only ones who will benefit from smaller network payloads Happy to make this a config rather than a feature flag, if you don't think this should be the default output Pull Request resolved: https://github.com/facebook/relay/pull/3983 Test Plan: Imported from GitHub, without a `Test Plan:` line. Static Docs Site previews have moved into the custom phabricator field "Static Docs", and will no longer modify test plans after 5th October 2022. Reviewed By: captbaritone Differential Revision: D39891556 Pulled By: captbaritone fbshipit-source-id: c11e0914beafe80069e170b9b5800ef507356ada --- .vscode/settings.json | 3 + compiler/crates/common/src/feature_flags.rs | 4 + .../crates/graphql-ir/src/node_identifier.rs | 223 ++++++++++++--- .../crates/graphql-text-printer/Cargo.toml | 1 + .../src/print_full_operation.rs | 25 +- .../graphql-text-printer/src/print_to_text.rs | 270 ++++++++++-------- .../fixtures/basic_directives.expected | 21 ++ .../compact/fixtures/basic_directives.graphql | 18 ++ .../compact/fixtures/basic_query.expected | 14 + .../compact/fixtures/basic_query.graphql | 11 + .../compact/fixtures/basic_var_defs.expected | 17 ++ .../compact/fixtures/basic_var_defs.graphql | 14 + .../compact/fixtures/compact_test.expected | 17 ++ .../compact/fixtures/compact_test.graphql | 14 + .../compact/fixtures/empty_args.expected | 11 + .../tests/compact/fixtures/empty_args.graphql | 8 + .../compact/fixtures/kitchen-sink.expected | 79 +++++ .../compact/fixtures/kitchen-sink.graphql | 76 +++++ .../single-value-array-of-objects.expected | 11 + .../single-value-array-of-objects.graphql | 8 + .../graphql-text-printer/tests/compact/mod.rs | 52 ++++ .../tests/compact_test.rs | 62 ++++ .../tests/operation_printer/mod.rs | 6 +- .../src/build_project/generate_artifacts.rs | 10 +- .../tests/compile_relay_artifacts/mod.rs | 7 +- .../mod.rs | 7 +- .../crates/relay-lsp/src/graphql_tools/mod.rs | 1 + 27 files changed, 825 insertions(+), 165 deletions(-) create mode 100644 compiler/crates/graphql-text-printer/tests/compact/fixtures/basic_directives.expected create mode 100644 compiler/crates/graphql-text-printer/tests/compact/fixtures/basic_directives.graphql create mode 100644 compiler/crates/graphql-text-printer/tests/compact/fixtures/basic_query.expected create mode 100644 compiler/crates/graphql-text-printer/tests/compact/fixtures/basic_query.graphql create mode 100644 compiler/crates/graphql-text-printer/tests/compact/fixtures/basic_var_defs.expected create mode 100644 compiler/crates/graphql-text-printer/tests/compact/fixtures/basic_var_defs.graphql create mode 100644 compiler/crates/graphql-text-printer/tests/compact/fixtures/compact_test.expected create mode 100644 compiler/crates/graphql-text-printer/tests/compact/fixtures/compact_test.graphql create mode 100644 compiler/crates/graphql-text-printer/tests/compact/fixtures/empty_args.expected create mode 100644 compiler/crates/graphql-text-printer/tests/compact/fixtures/empty_args.graphql create mode 100644 compiler/crates/graphql-text-printer/tests/compact/fixtures/kitchen-sink.expected create mode 100644 compiler/crates/graphql-text-printer/tests/compact/fixtures/kitchen-sink.graphql create mode 100644 compiler/crates/graphql-text-printer/tests/compact/fixtures/single-value-array-of-objects.expected create mode 100644 compiler/crates/graphql-text-printer/tests/compact/fixtures/single-value-array-of-objects.graphql create mode 100644 compiler/crates/graphql-text-printer/tests/compact/mod.rs create mode 100644 compiler/crates/graphql-text-printer/tests/compact_test.rs diff --git a/.vscode/settings.json b/.vscode/settings.json index 9083f916a7f39..b35f976fbde47 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,6 +8,9 @@ "editor.defaultFormatter": "esbenp.prettier-vscode" }, "prettier.enable": true, + "rust-analyzer.rustfmt.extraArgs": [ + "+nightly" + ], // Enable these to get Relay GraphQL editor support working on test files. diff --git a/compiler/crates/common/src/feature_flags.rs b/compiler/crates/common/src/feature_flags.rs index 78bb5882b8a1f..7349cf11f15ee 100644 --- a/compiler/crates/common/src/feature_flags.rs +++ b/compiler/crates/common/src/feature_flags.rs @@ -58,6 +58,10 @@ pub struct FeatureFlags { /// Enable support for the experimental `@alias` directive on fragment spreads. #[serde(default)] pub enable_fragment_aliases: FeatureFlag, + + /// Print queries in compact form + #[serde(default)] + pub compact_query_text: FeatureFlag, } #[derive(Debug, Deserialize, Clone, Serialize)] diff --git a/compiler/crates/graphql-ir/src/node_identifier.rs b/compiler/crates/graphql-ir/src/node_identifier.rs index 964273266262c..6595537460143 100644 --- a/compiler/crates/graphql-ir/src/node_identifier.rs +++ b/compiler/crates/graphql-ir/src/node_identifier.rs @@ -11,10 +11,15 @@ use std::hash::Hasher; use std::marker::PhantomData; use std::sync::Arc; +use common::ArgumentName; use common::DirectiveName; use common::WithLocation; +use graphql_syntax::OperationKind; use intern::string_key::StringKey; +use schema::FieldID; use schema::SDLSchema; +use schema::Type; +use schema::TypeReference; use crate::*; @@ -71,28 +76,34 @@ impl NodeIdentifier { pub fn are_equal(schema: &SDLSchema, a: &Selection, b: &Selection) -> bool { match (a, b) { (Selection::FragmentSpread(a), Selection::FragmentSpread(b)) => { - a.fragment.item == b.fragment.item + a.fragment + .item + .location_agnostic_eq::(&b.fragment.item) && a.arguments.location_agnostic_eq::(&b.arguments) && a.directives .location_agnostic_eq::(&b.directives) } (Selection::InlineFragment(a), Selection::InlineFragment(b)) => { - a.type_condition == b.type_condition + a.type_condition + .location_agnostic_eq::(&b.type_condition) && a.directives .location_agnostic_eq::(&b.directives) } (Selection::LinkedField(a), Selection::LinkedField(b)) => { - a.alias_or_name(schema) == b.alias_or_name(schema) + a.alias_or_name(schema) + .location_agnostic_eq::(&b.alias_or_name(schema)) && a.directives .location_agnostic_eq::(&b.directives) } (Selection::ScalarField(a), Selection::ScalarField(b)) => { - a.alias_or_name(schema) == b.alias_or_name(schema) + a.alias_or_name(schema) + .location_agnostic_eq::(&b.alias_or_name(schema)) && a.directives .location_agnostic_eq::(&b.directives) } (Selection::Condition(a), Selection::Condition(b)) => { - a.passing_value == b.passing_value + a.passing_value + .location_agnostic_eq::(&b.passing_value) && a.value.location_agnostic_eq::(&b.value) } @@ -111,18 +122,22 @@ impl PartialEq for NodeIdentifier l.eq(r), (NodeIdentifierInner::ScalarField(l), NodeIdentifierInner::ScalarField(r)) => l.eq(r), (NodeIdentifierInner::FragmentSpread(l), NodeIdentifierInner::FragmentSpread(r)) => { - l.fragment.item == r.fragment.item + l.fragment + .item + .location_agnostic_eq::(&r.fragment.item) && l.arguments.location_agnostic_eq::(&r.arguments) && l.directives .location_agnostic_eq::(&r.directives) } (NodeIdentifierInner::InlineFragment(l), NodeIdentifierInner::InlineFragment(r)) => { - l.type_condition == r.type_condition + l.type_condition + .location_agnostic_eq::(&r.type_condition) && l.directives .location_agnostic_eq::(&r.directives) } (NodeIdentifierInner::Condition(l), NodeIdentifierInner::Condition(r)) => { - l.passing_value == r.passing_value + l.passing_value + .location_agnostic_eq::(&r.passing_value) && l.value.location_agnostic_eq::(&r.value) } (NodeIdentifierInner::LinkedField(_), _) => false, @@ -189,7 +204,8 @@ pub struct ScalarFieldIdentifier { } impl PartialEq for ScalarFieldIdentifier { fn eq(&self, other: &Self) -> bool { - self.alias_or_name == other.alias_or_name + self.alias_or_name + .location_agnostic_eq::(&other.alias_or_name) && self .node .directives @@ -222,7 +238,8 @@ pub struct LinkedFieldIdentifier { } impl PartialEq for LinkedFieldIdentifier { fn eq(&self, other: &Self) -> bool { - self.alias_or_name == other.alias_or_name + self.alias_or_name + .location_agnostic_eq::(&other.alias_or_name) && self .node .directives @@ -277,7 +294,7 @@ impl LocationAgnosticPartialEq for WithLocation } } -impl LocationAgnosticPartialEq for Option<&T> { +impl LocationAgnosticPartialEq for Option { fn location_agnostic_eq(&self, other: &Self) -> bool { match (self, other) { (Some(l), Some(r)) => l.location_agnostic_eq::(r), @@ -287,9 +304,9 @@ impl LocationAgnosticPartialEq for Option<&T> { } } -impl LocationAgnosticPartialEq for StringKey { +impl LocationAgnosticPartialEq for &T { fn location_agnostic_eq(&self, other: &Self) -> bool { - self == other + (*self).location_agnostic_eq::(*other) } } @@ -304,6 +321,9 @@ pub trait DirectlyComparableIR {} impl DirectlyComparableIR for Value {} impl DirectlyComparableIR for ConstantArgument {} impl DirectlyComparableIR for ConstantValue {} +impl DirectlyComparableIR for Selection {} +impl DirectlyComparableIR for ExecutableDefinition {} +impl DirectlyComparableIR for VariableDefinition {} impl LocationAgnosticPartialEq for Vec { fn location_agnostic_eq(&self, other: &Self) -> bool { @@ -373,12 +393,7 @@ impl LocationAgnosticHash for Directive { impl LocationAgnosticPartialEq for Directive { fn location_agnostic_eq(&self, other: &Self) -> bool { - if !self - .name - .item - .0 - .location_agnostic_eq::(&other.name.item.0) - { + if !self.name.item.location_agnostic_eq::(&other.name.item) { return false; } if B::hash_for_name_only(self.name.item) { @@ -407,10 +422,7 @@ impl LocationAgnosticHash for Argument { impl LocationAgnosticPartialEq for Argument { fn location_agnostic_eq(&self, other: &Self) -> bool { - self.name - .item - .0 - .location_agnostic_eq::(&other.name.item.0) + self.name.item.location_agnostic_eq::(&other.name.item) && self.value.location_agnostic_eq::(&other.value) } } @@ -450,11 +462,7 @@ impl LocationAgnosticHash for Variable { impl LocationAgnosticPartialEq for Variable { fn location_agnostic_eq(&self, other: &Self) -> bool { - self.name - .item - .0 - .location_agnostic_eq::(&other.name.item.0) - && self.type_.eq(&other.type_) + self.name.item.location_agnostic_eq::(&other.name.item) && self.type_.eq(&other.type_) } } @@ -467,10 +475,7 @@ impl LocationAgnosticHash for ConstantArgument { impl LocationAgnosticPartialEq for ConstantArgument { fn location_agnostic_eq(&self, other: &Self) -> bool { - self.name - .item - .0 - .location_agnostic_eq::(&other.name.item.0) + self.name.item.location_agnostic_eq::(&other.name.item) && self.value.location_agnostic_eq::(&other.value) } } @@ -525,12 +530,162 @@ impl LocationAgnosticHash for ConditionValue { impl LocationAgnosticPartialEq for ConditionValue { fn location_agnostic_eq(&self, other: &Self) -> bool { match (self, other) { - (ConditionValue::Constant(left), ConditionValue::Constant(right)) => left == right, + (ConditionValue::Constant(left), ConditionValue::Constant(right)) => { + left.location_agnostic_eq::(right) + } (ConditionValue::Variable(left), ConditionValue::Variable(right)) => { - left.name.item.eq(&right.name.item) + left.location_agnostic_eq::(right) } (ConditionValue::Constant(_), _) => false, (ConditionValue::Variable(_), _) => false, } } } + +impl LocationAgnosticPartialEq for ExecutableDefinition { + fn location_agnostic_eq(&self, other: &Self) -> bool { + match (self, other) { + (ExecutableDefinition::Operation(left), ExecutableDefinition::Operation(right)) => { + left.location_agnostic_eq::(right) + } + (ExecutableDefinition::Fragment(left), ExecutableDefinition::Fragment(right)) => { + left.location_agnostic_eq::(right) + } + _ => false, + } + } +} + +impl LocationAgnosticPartialEq for VariableDefinition { + fn location_agnostic_eq(&self, other: &Self) -> bool { + self.name.location_agnostic_eq::(&other.name) + && self.type_.location_agnostic_eq::(&other.type_) + && self + .default_value + .location_agnostic_eq::(&other.default_value) + && self.directives.location_agnostic_eq::(&other.directives) + } +} + +impl LocationAgnosticPartialEq for OperationDefinition { + fn location_agnostic_eq(&self, other: &Self) -> bool { + self.kind.location_agnostic_eq::(&other.kind) + && self.name.location_agnostic_eq::(&other.name) + && self.type_.location_agnostic_eq::(&other.type_) + && self + .variable_definitions + .location_agnostic_eq::(&other.variable_definitions) + && self.directives.location_agnostic_eq::(&other.directives) + && self.selections.location_agnostic_eq::(&other.selections) + } +} + +macro_rules! impl_location_agnostic_partial_eq_using_eq { + ($type:ident) => { + impl LocationAgnosticPartialEq for $type { + fn location_agnostic_eq(&self, other: &Self) -> bool { + self == other + } + } + }; +} +impl_location_agnostic_partial_eq_using_eq!(Type); +impl_location_agnostic_partial_eq_using_eq!(OperationKind); +impl_location_agnostic_partial_eq_using_eq!(FieldID); +impl_location_agnostic_partial_eq_using_eq!(OperationDefinitionName); +impl_location_agnostic_partial_eq_using_eq!(FragmentDefinitionName); +impl_location_agnostic_partial_eq_using_eq!(VariableName); +impl_location_agnostic_partial_eq_using_eq!(DirectiveName); +impl_location_agnostic_partial_eq_using_eq!(ArgumentName); +impl_location_agnostic_partial_eq_using_eq!(bool); +impl_location_agnostic_partial_eq_using_eq!(StringKey); + +impl LocationAgnosticPartialEq for TypeReference { + fn location_agnostic_eq(&self, other: &Self) -> bool { + *self == *other + } +} + +impl LocationAgnosticPartialEq for Selection { + fn location_agnostic_eq(&self, other: &Self) -> bool { + match (self, other) { + (Selection::FragmentSpread(l), Selection::FragmentSpread(r)) => { + l.location_agnostic_eq::(r) + } + (Selection::InlineFragment(l), Selection::InlineFragment(r)) => { + l.location_agnostic_eq::(r) + } + (Selection::LinkedField(l), Selection::LinkedField(r)) => { + l.location_agnostic_eq::(r) + } + (Selection::ScalarField(l), Selection::ScalarField(r)) => { + l.location_agnostic_eq::(r) + } + (Selection::Condition(l), Selection::Condition(r)) => l.location_agnostic_eq::(r), + _ => false, + } + } +} + +impl LocationAgnosticPartialEq for FragmentSpread { + fn location_agnostic_eq(&self, other: &Self) -> bool { + self.fragment.location_agnostic_eq::(&other.fragment) + && self.arguments.location_agnostic_eq::(&other.arguments) + && self.directives.location_agnostic_eq::(&other.directives) + } +} + +impl LocationAgnosticPartialEq for InlineFragment { + fn location_agnostic_eq(&self, other: &Self) -> bool { + self.type_condition + .location_agnostic_eq::(&other.type_condition) + && self.directives.location_agnostic_eq::(&other.directives) + && self.selections.location_agnostic_eq::(&other.selections) + } +} + +impl LocationAgnosticPartialEq for LinkedField { + fn location_agnostic_eq(&self, other: &Self) -> bool { + self.alias.location_agnostic_eq::(&other.alias) + && self.definition.location_agnostic_eq::(&other.definition) + && self.arguments.location_agnostic_eq::(&other.arguments) + && self.directives.location_agnostic_eq::(&other.directives) + && self.selections.location_agnostic_eq::(&other.selections) + } +} + +impl LocationAgnosticPartialEq for ScalarField { + fn location_agnostic_eq(&self, other: &Self) -> bool { + self.alias.location_agnostic_eq::(&other.alias) + && self.definition.location_agnostic_eq::(&other.definition) + && self.arguments.location_agnostic_eq::(&other.arguments) + && self.directives.location_agnostic_eq::(&other.directives) + } +} + +impl LocationAgnosticPartialEq for Condition { + fn location_agnostic_eq(&self, other: &Self) -> bool { + self.selections.location_agnostic_eq::(&other.selections) + && self.value.location_agnostic_eq::(&other.value) + && self + .passing_value + .location_agnostic_eq::(&other.passing_value) + } +} + +impl LocationAgnosticPartialEq for FragmentDefinition { + fn location_agnostic_eq(&self, other: &Self) -> bool { + self.name.location_agnostic_eq::(&other.name) + && self + .variable_definitions + .location_agnostic_eq::(&other.variable_definitions) + && self + .used_global_variables + .location_agnostic_eq::(&other.used_global_variables) + && self + .type_condition + .location_agnostic_eq::(&other.type_condition) + && self.directives.location_agnostic_eq::(&other.directives) + && self.selections.location_agnostic_eq::(&other.selections) + } +} diff --git a/compiler/crates/graphql-text-printer/Cargo.toml b/compiler/crates/graphql-text-printer/Cargo.toml index cfd28330dbbc3..a5c14c57abaa5 100644 --- a/compiler/crates/graphql-text-printer/Cargo.toml +++ b/compiler/crates/graphql-text-printer/Cargo.toml @@ -25,3 +25,4 @@ schema = { path = "../schema" } [dev-dependencies] fixture-tests = { path = "../fixture-tests" } relay-test-schema = { path = "../relay-test-schema" } +relay-transforms = { path = "../relay-transforms" } diff --git a/compiler/crates/graphql-text-printer/src/print_full_operation.rs b/compiler/crates/graphql-text-printer/src/print_full_operation.rs index 8145b411768b9..f22a0ec2f9227 100644 --- a/compiler/crates/graphql-text-printer/src/print_full_operation.rs +++ b/compiler/crates/graphql-text-printer/src/print_full_operation.rs @@ -18,9 +18,14 @@ use graphql_ir::Visitor; use crate::print_fragment; use crate::print_operation; +use crate::PrinterOptions; -pub fn print_full_operation(program: &Program, operation: &OperationDefinition) -> String { - let mut printer = OperationPrinter::new(program); +pub fn print_full_operation( + program: &Program, + operation: &OperationDefinition, + options: PrinterOptions, +) -> String { + let mut printer = OperationPrinter::new(program, options); printer.print(operation) } @@ -28,28 +33,34 @@ pub struct OperationPrinter<'s> { fragment_result: FnvHashMap, reachable_fragments: FnvHashMap>, program: &'s Program, + options: PrinterOptions, } impl<'s> OperationPrinter<'s> { - pub fn new(program: &'s Program) -> Self { + pub fn new(program: &'s Program, options: PrinterOptions) -> Self { Self { fragment_result: Default::default(), reachable_fragments: Default::default(), program, + options, } } pub fn print(&mut self, operation: &OperationDefinition) -> String { - let mut result = print_operation(&self.program.schema, operation, Default::default()); + let mut result = print_operation(&self.program.schema, operation, self.options); self.visit_operation(operation); let mut fragments: Vec<(FragmentDefinitionName, Arc)> = self.reachable_fragments.drain().collect(); fragments.sort_unstable_by_key(|(name, _)| *name); for (_, fragment) in fragments { - result.push_str("\n\n"); + if !self.options.compact { + result.push_str("\n\n"); + } result.push_str(self.print_fragment(&fragment)); } - result.push('\n'); + if !self.options.compact { + result.push('\n'); + } result } @@ -57,7 +68,7 @@ impl<'s> OperationPrinter<'s> { let schema = &self.program.schema; self.fragment_result .entry(fragment.name.item) - .or_insert_with(|| print_fragment(schema, fragment, Default::default())) + .or_insert_with(|| print_fragment(schema, fragment, self.options)) } } diff --git a/compiler/crates/graphql-text-printer/src/print_to_text.rs b/compiler/crates/graphql-text-printer/src/print_to_text.rs index 4c9ef9d0b4537..9ce216701f902 100644 --- a/compiler/crates/graphql-text-printer/src/print_to_text.rs +++ b/compiler/crates/graphql-text-printer/src/print_to_text.rs @@ -15,6 +15,7 @@ use common::WithLocation; use graphql_ir::Argument; use graphql_ir::Condition; use graphql_ir::ConditionValue; +use graphql_ir::ConstantArgument; use graphql_ir::ConstantValue; use graphql_ir::Directive; use graphql_ir::ExecutableDefinition; @@ -27,11 +28,13 @@ use graphql_ir::ScalarField; use graphql_ir::Selection; use graphql_ir::Value; use graphql_ir::VariableDefinition; -use graphql_syntax::OperationKind; +use graphql_ir::VariableName; use intern::string_key::Intern; use intern::string_key::StringKey; use schema::SDLSchema; use schema::Schema; +use schema::Type; +use schema::TypeReference; pub fn print_ir(schema: &SDLSchema, definitions: &[ExecutableDefinition]) -> Vec { definitions @@ -215,13 +218,7 @@ impl<'schema, 'writer, W: Write> Printer<'schema, 'writer, W> { } fn print_operation(mut self, operation: &OperationDefinition) -> FmtResult { - let operation_kind = match operation.kind { - OperationKind::Query => "query", - OperationKind::Mutation => "mutation", - OperationKind::Subscription => "subscription", - }; - let operation_name = operation.name.item.0; - write!(self.writer, "{} {}", operation_kind, operation_name)?; + write!(self.writer, "{} {}", operation.kind, operation.name.item)?; self.print_variable_definitions(&operation.variable_definitions)?; self.print_directives(&operation.directives, None, None)?; self.print_selections(&operation.selections) @@ -251,19 +248,20 @@ impl<'schema, 'writer, W: Write> Printer<'schema, 'writer, W> { fn print_selections(&mut self, selections: &[Selection]) -> FmtResult { let len = selections.len(); if len > 0 { - write!(self.writer, " {{")?; + self.print_optional_space()?; + write!(self.writer, "{{")?; self.indentation += 1; - self.next_line()?; + self.print_new_line(true)?; for (i, selection) in selections.iter().enumerate() { self.print_selection(selection, None)?; if i != len - 1 { - self.next_line()?; + self.print_item_separator()?; } } self.indentation -= 1; - self.next_line()?; + self.print_new_line(true)?; write!(self.writer, "}}")?; } else { panic!( @@ -279,23 +277,12 @@ impl<'schema, 'writer, W: Write> Printer<'schema, 'writer, W> { conditions: Option>, ) -> FmtResult { match selection { - Selection::ScalarField(field) => { - self.print_scalar_field(field, conditions)?; - } - Selection::LinkedField(field) => { - self.print_linked_field(field, conditions)?; - } - Selection::FragmentSpread(field) => { - self.print_fragment_spread(field, conditions)?; - } - Selection::InlineFragment(field) => { - self.print_inline_fragment(field, conditions)?; - } - Selection::Condition(field) => { - self.print_condition(field)?; - } + Selection::ScalarField(field) => self.print_scalar_field(field, conditions), + Selection::LinkedField(field) => self.print_linked_field(field, conditions), + Selection::FragmentSpread(field) => self.print_fragment_spread(field, conditions), + Selection::InlineFragment(field) => self.print_inline_fragment(field, conditions), + Selection::Condition(field) => self.print_condition(field), } - Ok(()) } fn print_scalar_field( @@ -331,7 +318,8 @@ impl<'schema, 'writer, W: Write> Printer<'schema, 'writer, W> { write!(self.writer, "...{}", fragment_name)?; self.print_directives(&field.directives, conditions, None)?; if !field.arguments.is_empty() { - write!(self.writer, " @arguments")?; + self.print_optional_space()?; + write!(self.writer, "@arguments")?; self.print_arguments(&field.arguments) } else { Ok(()) @@ -345,9 +333,10 @@ impl<'schema, 'writer, W: Write> Printer<'schema, 'writer, W> { ) -> FmtResult { write!(self.writer, "...")?; if let Some(type_condition) = field.type_condition { + self.print_optional_space()?; write!( self.writer, - " on {}", + "on {}", self.schema.get_type_name(type_condition).lookup(), )?; }; @@ -369,7 +358,7 @@ impl<'schema, 'writer, W: Write> Printer<'schema, 'writer, W> { if is_first_selection { is_first_selection = false; } else { - self.next_line()?; + self.print_new_line(false)?; } self.print_selection(selection, Some(accum_conditions.iter().rev().collect()))?; maybe_current_condition = None; @@ -399,16 +388,17 @@ impl<'schema, 'writer, W: Write> Printer<'schema, 'writer, W> { } fn print_directive(&mut self, directive: &Directive) -> FmtResult { - write!(self.writer, " @{}", directive.name.item.0)?; + self.print_optional_space()?; + write!(self.writer, "@{}", directive.name.item)?; self.print_arguments(&directive.arguments)?; if self.options.debug_directive_data { if let Some(data) = &directive.data { for debug_line in format!("{:#?}", data).lines() { - self.next_line()?; + self.print_new_line(false)?; write!(self.writer, "# {}", debug_line)?; } - self.next_line()?; + self.print_new_line(false)?; } } @@ -417,23 +407,23 @@ impl<'schema, 'writer, W: Write> Printer<'schema, 'writer, W> { fn print_condition_directives(&mut self, conditions: Vec<&Condition>) -> FmtResult { for condition in conditions { + self.print_optional_space()?; write!( self.writer, - " @{}", + "@{}", if condition.passing_value { "include" } else { "skip" } )?; + write!(self.writer, "(if")?; + self.print_colon_separator()?; match &condition.value { - ConditionValue::Constant(value) => { - write!(self.writer, "(if: {})", value)?; - } - ConditionValue::Variable(variable) => { - write!(self.writer, "(if: ${})", variable.name.item)?; - } - } + ConditionValue::Constant(value) => write!(self.writer, "{}", value), + ConditionValue::Variable(variable) => self.print_variable(variable.name.item), + }?; + write!(self.writer, ")")?; } Ok(()) } @@ -445,23 +435,17 @@ impl<'schema, 'writer, W: Write> Printer<'schema, 'writer, W> { if !variable_definitions.is_empty() { write!(self.writer, "(")?; self.indentation += 1; - for var_def in variable_definitions.iter() { - self.next_line()?; - let type_name = self.schema.get_type_string(&var_def.type_); - write!(self.writer, "${}: {}", var_def.name.item, type_name)?; - - match &var_def.default_value { - None => {} - Some(default_value) => { - write!(self.writer, " = ")?; - self.print_constant_value(&default_value.item)?; - } + for (i, var_def) in variable_definitions.iter().enumerate() { + if i == 0 { + self.print_new_line(true)?; + } else { + self.print_item_separator()?; } - - self.print_directives(&var_def.directives, None, None)?; + self.print_variable_definition(var_def)?; } self.indentation -= 1; - write!(self.writer, "\n)")?; + self.print_new_line(true)?; + write!(self.writer, ")")?; } Ok(()) } @@ -471,25 +455,33 @@ impl<'schema, 'writer, W: Write> Printer<'schema, 'writer, W> { argument_definitions: &[VariableDefinition], ) -> FmtResult { if !argument_definitions.is_empty() { - write!(self.writer, " @argumentDefinitions(")?; + self.print_optional_space()?; + write!(self.writer, "@argumentDefinitions(")?; self.indentation += 1; - for arg_def in argument_definitions.iter() { - self.next_line()?; - let type_name = self.schema.get_type_string(&arg_def.type_); - write!( - self.writer, - "{}: {{type: \"{}\"", - arg_def.name.item, type_name - )?; - + for (i, arg_def) in argument_definitions.iter().enumerate() { + if i == 0 { + self.print_new_line(true)?; + } else { + self.print_item_separator()?; + } + write!(self.writer, "{}", arg_def.name.item)?; + self.print_colon_separator()?; + write!(self.writer, "{{type")?; + self.print_colon_separator()?; + write!(self.writer, "\"")?; + self.print_type(&arg_def.type_)?; + write!(self.writer, "\"")?; if let Some(default_value) = &arg_def.default_value { - write!(self.writer, ", defaultValue: ")?; + self.print_comma_separator()?; + write!(self.writer, "defaultValue")?; + self.print_colon_separator()?; self.print_constant_value(&default_value.item)?; } write!(self.writer, "}}")?; } self.indentation -= 1; - write!(self.writer, "\n)")?; + self.print_new_line(true)?; + write!(self.writer, ")")?; } Ok(()) } @@ -498,7 +490,6 @@ impl<'schema, 'writer, W: Write> Printer<'schema, 'writer, W> { if arguments.is_empty() { Ok(()) } else { - write!(self.writer, "(")?; let sorted_arguments = if self.options.sort_keys { let mut sorted_arguments = arguments.to_vec(); sorted_arguments.sort_by_key(|arg| arg.name()); @@ -509,29 +500,22 @@ impl<'schema, 'writer, W: Write> Printer<'schema, 'writer, W> { let maybe_sorted_arguments = sorted_arguments .as_ref() .map_or(arguments, |v| v.as_slice()); - for (i, argument) in maybe_sorted_arguments.iter().enumerate() { - write!(self.writer, "{}:", argument.name.item)?; - if !self.options.compact { - write!(self.writer, " ")?; - } - self.print_value(&argument.value.item)?; + write!(self.writer, "(")?; + for (i, argument) in maybe_sorted_arguments.iter().enumerate() { + self.print_argument(argument)?; if i != arguments.len() - 1 { - write!(self.writer, ",")?; - if !self.options.compact { - write!(self.writer, " ")?; - } + self.print_comma_separator()?; } } - write!(self.writer, ")")?; - Ok(()) + write!(self.writer, ")") } } fn print_value(&mut self, val: &Value) -> FmtResult { match val { Value::Constant(constant_val) => self.print_constant_value(constant_val), - Value::Variable(variable_val) => write!(self.writer, "${}", variable_val.name.item), + Value::Variable(variable_val) => self.print_variable(variable_val.name.item), Value::Object(object) => { write!(self.writer, "{{")?; let mut first = true; @@ -552,23 +536,17 @@ impl<'schema, 'writer, W: Write> Printer<'schema, 'writer, W> { if first { first = false; } else { - write!(self.writer, ",")?; - if !self.options.compact { - write!(self.writer, " ")?; - } + self.print_comma_separator()?; } if self.options.json_format { write!(self.writer, "\"{}\":", arg.name.item)?; } else { write!(self.writer, "{}:", arg.name.item)?; } - if !self.options.compact { - write!(self.writer, " ")?; - } + self.print_optional_space()?; self.print_value(&arg.value.item)?; } - write!(self.writer, "}}")?; - Ok(()) + write!(self.writer, "}}") } Value::List(list) => { write!(self.writer, "[")?; @@ -580,15 +558,11 @@ impl<'schema, 'writer, W: Write> Printer<'schema, 'writer, W> { if first { first = false; } else { - write!(self.writer, ",")?; - if !self.options.compact { - write!(self.writer, " ")?; - } + self.print_comma_separator()?; } self.print_value(value)?; } - write!(self.writer, "]")?; - Ok(()) + write!(self.writer, "]") } } } @@ -624,23 +598,11 @@ impl<'schema, 'writer, W: Write> Printer<'schema, 'writer, W> { if first { first = false; } else { - write!(self.writer, ",")?; - if !self.options.compact { - write!(self.writer, " ")?; - } + self.print_comma_separator()?; } - if self.options.json_format { - write!(self.writer, "\"{}\":", arg.name.item)?; - } else { - write!(self.writer, "{}:", arg.name.item)?; - } - if !self.options.compact { - write!(self.writer, " ")?; - } - self.print_constant_value(&arg.value.item)?; + self.print_constant_object_field(arg)?; } - write!(self.writer, "}}")?; - Ok(()) + write!(self.writer, "}}") } ConstantValue::List(list) => { write!(self.writer, "[")?; @@ -649,15 +611,11 @@ impl<'schema, 'writer, W: Write> Printer<'schema, 'writer, W> { if first { first = false; } else { - write!(self.writer, ",")?; - if !self.options.compact { - write!(self.writer, " ")?; - } + self.print_comma_separator()?; } self.print_constant_value(value)?; } - write!(self.writer, "]")?; - Ok(()) + write!(self.writer, "]") } } } @@ -669,17 +627,89 @@ impl<'schema, 'writer, W: Write> Printer<'schema, 'writer, W> { ) -> FmtResult { if let Some(alias) = alias { if alias.item != name { - write!(self.writer, "{}: ", alias.item)?; + write!(self.writer, "{}", alias.item)?; + self.print_colon_separator()?; } } write!(self.writer, "{}", name) } - fn next_line(&mut self) -> FmtResult { + fn print_argument(&mut self, argument: &Argument) -> FmtResult { + write!(self.writer, "{}", argument.name.item)?; + self.print_colon_separator()?; + self.print_value(&argument.value.item) + } + + fn print_variable_definition(&mut self, var_def: &VariableDefinition) -> FmtResult { + self.print_variable(var_def.name.item)?; + self.print_colon_separator()?; + self.print_type(&var_def.type_)?; + + if let Some(default_value) = &var_def.default_value { + self.print_default_value(&default_value.item)?; + } + + self.print_directives(&var_def.directives, None, None) + } + + fn print_constant_object_field(&mut self, arg: &ConstantArgument) -> FmtResult { + if self.options.json_format { + write!(self.writer, "\"{}\"", arg.name.item)?; + } else { + write!(self.writer, "{}", arg.name.item)?; + } + self.print_colon_separator()?; + self.print_constant_value(&arg.value.item) + } + + fn print_variable(&mut self, variable: VariableName) -> FmtResult { + write!(self.writer, "${}", variable) + } + + fn print_default_value(&mut self, value: &ConstantValue) -> FmtResult { + self.print_optional_space()?; + write!(self.writer, "=")?; + self.print_optional_space()?; + self.print_constant_value(value) + } + + fn print_type(&mut self, type_: &TypeReference) -> FmtResult { + write!(self.writer, "{}", self.schema.get_type_string(type_)) + } + + fn print_new_line(&mut self, can_skip_from_this_location: bool) -> FmtResult { + if self.options.compact && can_skip_from_this_location { + return Ok(()); + } writeln!(self.writer)?; for _ in 0..self.indentation { write!(self.writer, " ")?; } Ok(()) } + + fn print_optional_space(&mut self) -> FmtResult { + if self.options.compact { + return Ok(()); + } + write!(self.writer, " ") + } + + fn print_comma_separator(&mut self) -> FmtResult { + write!(self.writer, ",")?; + self.print_optional_space() + } + + fn print_colon_separator(&mut self) -> FmtResult { + write!(self.writer, ":")?; + self.print_optional_space() + } + + fn print_item_separator(&mut self) -> FmtResult { + if self.options.compact { + write!(self.writer, ",") + } else { + self.print_new_line(false) + } + } } diff --git a/compiler/crates/graphql-text-printer/tests/compact/fixtures/basic_directives.expected b/compiler/crates/graphql-text-printer/tests/compact/fixtures/basic_directives.expected new file mode 100644 index 0000000000000..22cec7aba6504 --- /dev/null +++ b/compiler/crates/graphql-text-printer/tests/compact/fixtures/basic_directives.expected @@ -0,0 +1,21 @@ +==================================== INPUT ==================================== +query MyQuery($id: ID, $cond: Boolean!) { + my_alias: node(id: $id) { + id + ... on User @include(if: $cond) { + name + } + ...UserFragment @include(if: $cond) + } +} + +fragment UserFragment on User { + id + name @include(if: $cond) + otherName: name @customDirective(level: 3) @skip(if: $cond) + address @skip(if: true) { + city + } +} +==================================== OUTPUT =================================== +query MyQuery($id:ID,$cond:Boolean!){my_alias:node(id:$id){id,...on User@include(if:$cond){name},...UserFragment@include(if:$cond)}}fragment UserFragment on User{id,name@include(if:$cond),otherName:name@skip(if:$cond)@customDirective(level:3),address@skip(if:true){city}} diff --git a/compiler/crates/graphql-text-printer/tests/compact/fixtures/basic_directives.graphql b/compiler/crates/graphql-text-printer/tests/compact/fixtures/basic_directives.graphql new file mode 100644 index 0000000000000..0cc0451e7b34f --- /dev/null +++ b/compiler/crates/graphql-text-printer/tests/compact/fixtures/basic_directives.graphql @@ -0,0 +1,18 @@ +query MyQuery($id: ID, $cond: Boolean!) { + my_alias: node(id: $id) { + id + ... on User @include(if: $cond) { + name + } + ...UserFragment @include(if: $cond) + } +} + +fragment UserFragment on User { + id + name @include(if: $cond) + otherName: name @customDirective(level: 3) @skip(if: $cond) + address @skip(if: true) { + city + } +} diff --git a/compiler/crates/graphql-text-printer/tests/compact/fixtures/basic_query.expected b/compiler/crates/graphql-text-printer/tests/compact/fixtures/basic_query.expected new file mode 100644 index 0000000000000..38254ef3e34a1 --- /dev/null +++ b/compiler/crates/graphql-text-printer/tests/compact/fixtures/basic_query.expected @@ -0,0 +1,14 @@ +==================================== INPUT ==================================== +query MyQuery($id: ID) { + my_alias: node(id: $id) { + id + ... on User { + name + likers(first: 5) { + count + } + } + } +} +==================================== OUTPUT =================================== +query MyQuery($id:ID){my_alias:node(id:$id){id,...on User{name,likers(first:5){count}}}} diff --git a/compiler/crates/graphql-text-printer/tests/compact/fixtures/basic_query.graphql b/compiler/crates/graphql-text-printer/tests/compact/fixtures/basic_query.graphql new file mode 100644 index 0000000000000..12f6b9f3b6d70 --- /dev/null +++ b/compiler/crates/graphql-text-printer/tests/compact/fixtures/basic_query.graphql @@ -0,0 +1,11 @@ +query MyQuery($id: ID) { + my_alias: node(id: $id) { + id + ... on User { + name + likers(first: 5) { + count + } + } + } +} diff --git a/compiler/crates/graphql-text-printer/tests/compact/fixtures/basic_var_defs.expected b/compiler/crates/graphql-text-printer/tests/compact/fixtures/basic_var_defs.expected new file mode 100644 index 0000000000000..7b9c7d1286d6d --- /dev/null +++ b/compiler/crates/graphql-text-printer/tests/compact/fixtures/basic_var_defs.expected @@ -0,0 +1,17 @@ +==================================== INPUT ==================================== +query MyQuery($id: ID, $count: Int! = 5, $envs: [Environment!]! = [WEB]) { + my_alias: node(id: $id) { + id + ... on User { + name + likers(first: $count) { + count + } + checkins(environments: $envs) { + query + } + } + } +} +==================================== OUTPUT =================================== +query MyQuery($id:ID,$count:Int!=5,$envs:[Environment!]!=[WEB]){my_alias:node(id:$id){id,...on User{name,likers(first:$count){count},checkins(environments:$envs){query}}}} diff --git a/compiler/crates/graphql-text-printer/tests/compact/fixtures/basic_var_defs.graphql b/compiler/crates/graphql-text-printer/tests/compact/fixtures/basic_var_defs.graphql new file mode 100644 index 0000000000000..0e9dfbba909d1 --- /dev/null +++ b/compiler/crates/graphql-text-printer/tests/compact/fixtures/basic_var_defs.graphql @@ -0,0 +1,14 @@ +query MyQuery($id: ID, $count: Int! = 5, $envs: [Environment!]! = [WEB]) { + my_alias: node(id: $id) { + id + ... on User { + name + likers(first: $count) { + count + } + checkins(environments: $envs) { + query + } + } + } +} diff --git a/compiler/crates/graphql-text-printer/tests/compact/fixtures/compact_test.expected b/compiler/crates/graphql-text-printer/tests/compact/fixtures/compact_test.expected new file mode 100644 index 0000000000000..4312a756af411 --- /dev/null +++ b/compiler/crates/graphql-text-printer/tests/compact/fixtures/compact_test.expected @@ -0,0 +1,17 @@ +==================================== INPUT ==================================== +query Test { + #comments go away + aliasOfFoo : username(name : "val1" ) @customDirective(level: 2) { # and this comment as well + emailAddresses + firstName(if: false, unless: false) + } + ...FX +} + +fragment FX on Query { + aliased : username(name : "argVal") { + thin: emailAddresses + } +} +==================================== OUTPUT =================================== +query Test{aliasOfFoo:username(name:"val1")@customDirective(level:2){emailAddresses,firstName(if:false,unless:false)},...FX}fragment FX on Query{aliased:username(name:"argVal"){thin:emailAddresses}} diff --git a/compiler/crates/graphql-text-printer/tests/compact/fixtures/compact_test.graphql b/compiler/crates/graphql-text-printer/tests/compact/fixtures/compact_test.graphql new file mode 100644 index 0000000000000..18d8423837749 --- /dev/null +++ b/compiler/crates/graphql-text-printer/tests/compact/fixtures/compact_test.graphql @@ -0,0 +1,14 @@ +query Test { + #comments go away + aliasOfFoo : username(name : "val1" ) @customDirective(level: 2) { # and this comment as well + emailAddresses + firstName(if: false, unless: false) + } + ...FX +} + +fragment FX on Query { + aliased : username(name : "argVal") { + thin: emailAddresses + } +} diff --git a/compiler/crates/graphql-text-printer/tests/compact/fixtures/empty_args.expected b/compiler/crates/graphql-text-printer/tests/compact/fixtures/empty_args.expected new file mode 100644 index 0000000000000..4fd4224e3eaa8 --- /dev/null +++ b/compiler/crates/graphql-text-printer/tests/compact/fixtures/empty_args.expected @@ -0,0 +1,11 @@ +==================================== INPUT ==================================== +query EmptyArgs { + route(waypoints: []) { + __typename + } + items(filter: {}) { + __typename + } +} +==================================== OUTPUT =================================== +query EmptyArgs{route(waypoints:[]){__typename},items(filter:{}){__typename}} diff --git a/compiler/crates/graphql-text-printer/tests/compact/fixtures/empty_args.graphql b/compiler/crates/graphql-text-printer/tests/compact/fixtures/empty_args.graphql new file mode 100644 index 0000000000000..9c92aec663e24 --- /dev/null +++ b/compiler/crates/graphql-text-printer/tests/compact/fixtures/empty_args.graphql @@ -0,0 +1,8 @@ +query EmptyArgs { + route(waypoints: []) { + __typename + } + items(filter: {}) { + __typename + } +} diff --git a/compiler/crates/graphql-text-printer/tests/compact/fixtures/kitchen-sink.expected b/compiler/crates/graphql-text-printer/tests/compact/fixtures/kitchen-sink.expected new file mode 100644 index 0000000000000..25c5fa433ffa3 --- /dev/null +++ b/compiler/crates/graphql-text-printer/tests/compact/fixtures/kitchen-sink.expected @@ -0,0 +1,79 @@ +==================================== INPUT ==================================== +query NodeQuery( + $cond: Boolean! = false + $id: ID! + $size: [Int] = [32] + $query: CheckinSearchInput! +) { + node(id: $id) { + id + ... on User @include(if: $cond) { + name + } + ...UserFragment @include(if: $cond) @arguments(size: $size) + } + checkinSearchQuery(query: $query) { + query + } +} + +fragment UserFragment on User + @argumentDefinitions( + after: {type: "ID"} + first: {type: "Int", defaultValue: 5} + size: {type: "[Int]"} + storyType: {type: "StoryType", defaultValue: DIRECTED} + # storyType_invalid_string: {type: "StoryType", defaultValue: "DIRECTED"} + # storyType_invalid_lowercase: {type: "StoryType", defaultValue: directed} + ) { + id + __typename + checkins(environments: [WEB]) { + __typename + } + nakedEnum: checkins(environments: WEB) { + __typename + } + friends(after: $after, first: $first, traits: [HELPFUL]) { + count + } + secondFriends: friends(first: 10) { + count + } + name @include(if: $cond) + otherName: name @customDirective(level: 3) + thumbnail: profilePicture2( + size: 32 + cropPosition: CENTER + fileExtension: PNG + # TODO add support for custom scalars + # additionalParameters: {filter: "Boston"} + options: {newName: null} + ) { + height + width + src: uri + } + profilePicture(size: $size) @include(if: $cond) @skip(if: $foo) { + height + width + src: uri + } + storySearch(query: {type: DIRECTED}) { + id + } + ... @include(if: $cond) @skip(if: $foo) { + address { + city + } + alternate_name + } + + ... @include(if: $cond) { + address { + city + } + } +} +==================================== OUTPUT =================================== +query NodeQuery($cond:Boolean!=false,$id:ID!,$size:[Int]=[32],$query:CheckinSearchInput!){node(id:$id){id,...on User@include(if:$cond){name},...UserFragment@include(if:$cond)@arguments(size:$size)},checkinSearchQuery(query:$query){query}}fragment UserFragment on User@argumentDefinitions(after:{type:"ID"},first:{type:"Int",defaultValue:5},size:{type:"[Int]"},storyType:{type:"StoryType",defaultValue:DIRECTED}){id,__typename,checkins(environments:[WEB]){__typename},nakedEnum:checkins(environments:WEB){__typename},friends(after:$after,first:$first,traits:[HELPFUL]){count},secondFriends:friends(first:10){count},name@include(if:$cond),otherName:name@customDirective(level:3),thumbnail:profilePicture2(size:32,cropPosition:CENTER,fileExtension:PNG,options:{newName:null}){height,width,src:uri},profilePicture(size:$size)@include(if:$cond)@skip(if:$foo){height,width,src:uri},storySearch(query:{type:DIRECTED}){id},...@include(if:$cond)@skip(if:$foo){address{city},alternate_name},...@include(if:$cond){address{city}}} diff --git a/compiler/crates/graphql-text-printer/tests/compact/fixtures/kitchen-sink.graphql b/compiler/crates/graphql-text-printer/tests/compact/fixtures/kitchen-sink.graphql new file mode 100644 index 0000000000000..3adfe1c5eabdb --- /dev/null +++ b/compiler/crates/graphql-text-printer/tests/compact/fixtures/kitchen-sink.graphql @@ -0,0 +1,76 @@ +query NodeQuery( + $cond: Boolean! = false + $id: ID! + $size: [Int] = [32] + $query: CheckinSearchInput! +) { + node(id: $id) { + id + ... on User @include(if: $cond) { + name + } + ...UserFragment @include(if: $cond) @arguments(size: $size) + } + checkinSearchQuery(query: $query) { + query + } +} + +fragment UserFragment on User + @argumentDefinitions( + after: {type: "ID"} + first: {type: "Int", defaultValue: 5} + size: {type: "[Int]"} + storyType: {type: "StoryType", defaultValue: DIRECTED} + # storyType_invalid_string: {type: "StoryType", defaultValue: "DIRECTED"} + # storyType_invalid_lowercase: {type: "StoryType", defaultValue: directed} + ) { + id + __typename + checkins(environments: [WEB]) { + __typename + } + nakedEnum: checkins(environments: WEB) { + __typename + } + friends(after: $after, first: $first, traits: [HELPFUL]) { + count + } + secondFriends: friends(first: 10) { + count + } + name @include(if: $cond) + otherName: name @customDirective(level: 3) + thumbnail: profilePicture2( + size: 32 + cropPosition: CENTER + fileExtension: PNG + # TODO add support for custom scalars + # additionalParameters: {filter: "Boston"} + options: {newName: null} + ) { + height + width + src: uri + } + profilePicture(size: $size) @include(if: $cond) @skip(if: $foo) { + height + width + src: uri + } + storySearch(query: {type: DIRECTED}) { + id + } + ... @include(if: $cond) @skip(if: $foo) { + address { + city + } + alternate_name + } + + ... @include(if: $cond) { + address { + city + } + } +} diff --git a/compiler/crates/graphql-text-printer/tests/compact/fixtures/single-value-array-of-objects.expected b/compiler/crates/graphql-text-printer/tests/compact/fixtures/single-value-array-of-objects.expected new file mode 100644 index 0000000000000..08905768f58c8 --- /dev/null +++ b/compiler/crates/graphql-text-printer/tests/compact/fixtures/single-value-array-of-objects.expected @@ -0,0 +1,11 @@ +==================================== INPUT ==================================== +query SingleValueArrayQuery { + route(waypoints: {lat: "123", lon: "456"}) { + steps { + lat + lon + } + } +} +==================================== OUTPUT =================================== +query SingleValueArrayQuery{route(waypoints:{lat:"123",lon:"456"}){steps{lat,lon}}} diff --git a/compiler/crates/graphql-text-printer/tests/compact/fixtures/single-value-array-of-objects.graphql b/compiler/crates/graphql-text-printer/tests/compact/fixtures/single-value-array-of-objects.graphql new file mode 100644 index 0000000000000..2ef9bcc746dfd --- /dev/null +++ b/compiler/crates/graphql-text-printer/tests/compact/fixtures/single-value-array-of-objects.graphql @@ -0,0 +1,8 @@ +query SingleValueArrayQuery { + route(waypoints: {lat: "123", lon: "456"}) { + steps { + lat + lon + } + } +} diff --git a/compiler/crates/graphql-text-printer/tests/compact/mod.rs b/compiler/crates/graphql-text-printer/tests/compact/mod.rs new file mode 100644 index 0000000000000..68869747f158c --- /dev/null +++ b/compiler/crates/graphql-text-printer/tests/compact/mod.rs @@ -0,0 +1,52 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +use std::sync::Arc; + +use common::SourceLocationKey; +use fixture_tests::Fixture; +use graphql_ir::build; +use graphql_ir::node_identifier::LocationAgnosticPartialEq; +use graphql_ir::ExecutableDefinition; +use graphql_ir::Program; +use graphql_syntax::parse_executable; +use graphql_text_printer::print_full_operation; +use graphql_text_printer::PrinterOptions; +use relay_test_schema::TEST_SCHEMA; +use relay_transforms::RelayLocationAgnosticBehavior; + +pub fn transform_fixture(fixture: &Fixture<'_>) -> Result { + let source_location = SourceLocationKey::standalone(fixture.file_name); + let initial_ast = parse_executable(fixture.content, source_location).unwrap(); + let initial_ir = build(&TEST_SCHEMA, &initial_ast.definitions).unwrap(); + let initial_ir_copy = initial_ir.clone(); + let program = Program::from_definitions(Arc::clone(&TEST_SCHEMA), initial_ir.clone()); + let options = PrinterOptions { + compact: true, + ..Default::default() + }; + + // Print the IR into a GraphQL string for the fixture + let output = initial_ir + .into_iter() + .filter_map(|definition| match definition { + ExecutableDefinition::Operation(operation) => Some(operation), + _ => None, + }) + .map(|operation| print_full_operation(&program, &operation, options)) + .collect::>() + .join("\n\n"); + + // Roundtrip the output back into an IR + let roundtrip_ast = parse_executable(output.as_str(), SourceLocationKey::Generated).unwrap(); + let roundtrip_ir = build(&TEST_SCHEMA, &roundtrip_ast.definitions).unwrap(); + + // Check the roundtripped IR matches the initial IR to ensure we printed a valid schema + assert!(roundtrip_ir.location_agnostic_eq::(&initial_ir_copy)); + + Ok(output) +} diff --git a/compiler/crates/graphql-text-printer/tests/compact_test.rs b/compiler/crates/graphql-text-printer/tests/compact_test.rs new file mode 100644 index 0000000000000..490f1db1e6ed6 --- /dev/null +++ b/compiler/crates/graphql-text-printer/tests/compact_test.rs @@ -0,0 +1,62 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @generated SignedSource<<6d1db34f88e5cb87dff2002fd51965c4>> + */ + +mod compact; + +use compact::transform_fixture; +use fixture_tests::test_fixture; + +#[test] +fn basic_directives() { + let input = include_str!("compact/fixtures/basic_directives.graphql"); + let expected = include_str!("compact/fixtures/basic_directives.expected"); + test_fixture(transform_fixture, "basic_directives.graphql", "compact/fixtures/basic_directives.expected", input, expected); +} + +#[test] +fn basic_query() { + let input = include_str!("compact/fixtures/basic_query.graphql"); + let expected = include_str!("compact/fixtures/basic_query.expected"); + test_fixture(transform_fixture, "basic_query.graphql", "compact/fixtures/basic_query.expected", input, expected); +} + +#[test] +fn basic_var_defs() { + let input = include_str!("compact/fixtures/basic_var_defs.graphql"); + let expected = include_str!("compact/fixtures/basic_var_defs.expected"); + test_fixture(transform_fixture, "basic_var_defs.graphql", "compact/fixtures/basic_var_defs.expected", input, expected); +} + +#[test] +fn compact_test() { + let input = include_str!("compact/fixtures/compact_test.graphql"); + let expected = include_str!("compact/fixtures/compact_test.expected"); + test_fixture(transform_fixture, "compact_test.graphql", "compact/fixtures/compact_test.expected", input, expected); +} + +#[test] +fn empty_args() { + let input = include_str!("compact/fixtures/empty_args.graphql"); + let expected = include_str!("compact/fixtures/empty_args.expected"); + test_fixture(transform_fixture, "empty_args.graphql", "compact/fixtures/empty_args.expected", input, expected); +} + +#[test] +fn kitchen_sink() { + let input = include_str!("compact/fixtures/kitchen-sink.graphql"); + let expected = include_str!("compact/fixtures/kitchen-sink.expected"); + test_fixture(transform_fixture, "kitchen-sink.graphql", "compact/fixtures/kitchen-sink.expected", input, expected); +} + +#[test] +fn single_value_array_of_objects() { + let input = include_str!("compact/fixtures/single-value-array-of-objects.graphql"); + let expected = include_str!("compact/fixtures/single-value-array-of-objects.expected"); + test_fixture(transform_fixture, "single-value-array-of-objects.graphql", "compact/fixtures/single-value-array-of-objects.expected", input, expected); +} diff --git a/compiler/crates/graphql-text-printer/tests/operation_printer/mod.rs b/compiler/crates/graphql-text-printer/tests/operation_printer/mod.rs index 9eccd43ec9172..88ae4bfbc91b6 100644 --- a/compiler/crates/graphql-text-printer/tests/operation_printer/mod.rs +++ b/compiler/crates/graphql-text-printer/tests/operation_printer/mod.rs @@ -28,7 +28,11 @@ pub fn transform_fixture(fixture: &Fixture<'_>) -> Result { .into_iter() .filter_map(|definition| { if let ExecutableDefinition::Operation(operation) = definition { - Some(print_full_operation(&program, &operation)) + Some(print_full_operation( + &program, + &operation, + Default::default(), + )) } else { None } diff --git a/compiler/crates/relay-compiler/src/build_project/generate_artifacts.rs b/compiler/crates/relay-compiler/src/build_project/generate_artifacts.rs index c6add4e3518c9..987b73d2d1be6 100644 --- a/compiler/crates/relay-compiler/src/build_project/generate_artifacts.rs +++ b/compiler/crates/relay-compiler/src/build_project/generate_artifacts.rs @@ -14,6 +14,7 @@ use fnv::FnvHashMap; use graphql_ir::FragmentDefinition; use graphql_ir::OperationDefinition; use graphql_text_printer::OperationPrinter; +use graphql_text_printer::PrinterOptions; use intern::string_key::StringKey; use relay_transforms::ClientEdgeGeneratedQueryMetadataDirective; use relay_transforms::Programs; @@ -45,7 +46,14 @@ pub fn generate_artifacts( programs: &Programs, source_hashes: Arc, ) -> Vec { - let mut operation_printer = OperationPrinter::new(&programs.operation_text); + let printer_options = PrinterOptions { + compact: project_config + .feature_flags + .compact_query_text + .is_fully_enabled(), + ..Default::default() + }; + let mut operation_printer = OperationPrinter::new(&programs.operation_text, printer_options); return group_operations(programs) .into_iter() .map(|(_, operations)| -> Artifact { diff --git a/compiler/crates/relay-compiler/tests/compile_relay_artifacts/mod.rs b/compiler/crates/relay-compiler/tests/compile_relay_artifacts/mod.rs index b554a0d19a896..4e322bb3c9e32 100644 --- a/compiler/crates/relay-compiler/tests/compile_relay_artifacts/mod.rs +++ b/compiler/crates/relay-compiler/tests/compile_relay_artifacts/mod.rs @@ -116,6 +116,7 @@ pub fn transform_fixture(fixture: &Fixture<'_>) -> Result { enable_client_edges: FeatureFlag::Enabled, skip_printing_nulls: FeatureFlag::Disabled, enable_fragment_aliases: FeatureFlag::Enabled, + compact_query_text: FeatureFlag::Disabled, }; let default_project_config = ProjectConfig { @@ -230,7 +231,11 @@ pub fn transform_fixture(fixture: &Fixture<'_>) -> Result { let text = print_operation_node.map_or_else( || "Query Text is Empty.".to_string(), |print_operation_node| { - print_full_operation(&programs.operation_text, print_operation_node) + print_full_operation( + &programs.operation_text, + print_operation_node, + Default::default(), + ) }, ); diff --git a/compiler/crates/relay-compiler/tests/compile_relay_artifacts_with_custom_id/mod.rs b/compiler/crates/relay-compiler/tests/compile_relay_artifacts_with_custom_id/mod.rs index dc4489a60bb7e..a6d76518a8dbb 100644 --- a/compiler/crates/relay-compiler/tests/compile_relay_artifacts_with_custom_id/mod.rs +++ b/compiler/crates/relay-compiler/tests/compile_relay_artifacts_with_custom_id/mod.rs @@ -86,6 +86,7 @@ pub fn transform_fixture(fixture: &Fixture<'_>) -> Result { enable_client_edges: FeatureFlag::Enabled, skip_printing_nulls: FeatureFlag::Disabled, enable_fragment_aliases: FeatureFlag::Enabled, + compact_query_text: FeatureFlag::Disabled, }; let project_config = ProjectConfig { @@ -136,7 +137,11 @@ pub fn transform_fixture(fixture: &Fixture<'_>) -> Result { let text = print_operation_node.map_or_else( || "Query Text is Empty.".to_string(), |print_operation_node| { - print_full_operation(&programs.operation_text, print_operation_node) + print_full_operation( + &programs.operation_text, + print_operation_node, + Default::default(), + ) }, ); diff --git a/compiler/crates/relay-lsp/src/graphql_tools/mod.rs b/compiler/crates/relay-lsp/src/graphql_tools/mod.rs index 2fa9828c9ff8c..978c5517754c9 100644 --- a/compiler/crates/relay-lsp/src/graphql_tools/mod.rs +++ b/compiler/crates/relay-lsp/src/graphql_tools/mod.rs @@ -159,6 +159,7 @@ fn print_full_operation_text(programs: Programs, operation_name: StringKey) -> O Some(print_full_operation( &programs.operation_text, print_operation_node, + Default::default(), )) }