From 02007a739b15a3e2d0deb631ab102ae0e9b40853 Mon Sep 17 00:00:00 2001 From: Tom Gasson Date: Tue, 21 Jun 2022 15:19:18 +1000 Subject: [PATCH 1/2] Compact GraphQL query text output Wires the pre-existing `compact` printing into a feature flag. Additionally disables indenting when printing in compact form --- compiler/Cargo.lock | 5 +- compiler/crates/common/src/feature_flags.rs | 4 + .../crates/graphql-ir/src/node_identifier.rs | 156 ++++++++- .../crates/graphql-text-printer/Cargo.toml | 1 + .../src/print_full_operation.rs | 18 +- .../graphql-text-printer/src/print_to_text.rs | 308 ++++++++++-------- .../fixtures/basic_directives.expected | 23 ++ .../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 | 19 ++ .../compact/fixtures/compact_test.graphql | 14 + .../compact/fixtures/empty_args.expected | 11 + .../tests/compact/fixtures/empty_args.graphql | 8 + .../compact/fixtures/kitchen-sink.expected | 81 +++++ .../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 | 49 +++ .../tests/compact_test.rs | 63 ++++ .../tests/operation_printer/mod.rs | 6 +- .../src/build_project/generate_artifacts.rs | 11 +- .../tests/compile_relay_artifacts/mod.rs | 7 +- .../mod.rs | 7 +- .../crates/relay-lsp/src/graphql_tools/mod.rs | 1 + 27 files changed, 812 insertions(+), 149 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/compiler/Cargo.lock b/compiler/Cargo.lock index 59f5e0e979517..e627138c0dedb 100644 --- a/compiler/Cargo.lock +++ b/compiler/Cargo.lock @@ -693,6 +693,7 @@ dependencies = [ "graphql-syntax", "intern", "relay-test-schema", + "relay-transforms", "schema", ] @@ -1144,9 +1145,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.8.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" +checksum = "7709cef83f0c1f58f666e746a08b21e0085f7440fa6a29cc194d68aac97a4225" [[package]] name = "opaque-debug" diff --git a/compiler/crates/common/src/feature_flags.rs b/compiler/crates/common/src/feature_flags.rs index d036cf1d2a27a..ce9efef4512db 100644 --- a/compiler/crates/common/src/feature_flags.rs +++ b/compiler/crates/common/src/feature_flags.rs @@ -53,6 +53,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 ef216d295ab14..e00fd3c377de8 100644 --- a/compiler/crates/graphql-ir/src/node_identifier.rs +++ b/compiler/crates/graphql-ir/src/node_identifier.rs @@ -7,8 +7,9 @@ use crate::*; use common::WithLocation; +use graphql_syntax::OperationKind; use intern::string_key::StringKey; -use schema::SDLSchema; +use schema::{FieldID, SDLSchema, Type}; use std::{ fmt, hash::{Hash, Hasher}, @@ -275,7 +276,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), @@ -302,6 +303,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 { @@ -405,6 +409,16 @@ impl LocationAgnosticPartialEq for Argument { } } +impl LocationAgnosticPartialEq for Option<&Argument> { + fn location_agnostic_eq(&self, other: &Self) -> bool { + match (self, other) { + (Some(l), Some(r)) => l.location_agnostic_eq::(r), + (None, None) => true, + _ => false, + } + } +} + impl LocationAgnosticHash for Value { fn location_agnostic_hash(&self, state: &mut H) { match self { @@ -517,3 +531,141 @@ impl LocationAgnosticPartialEq for ConditionValue { } } } + +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_ == 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.kind.location_agnostic_eq::(&other.kind) + && self + .variable_definitions + .location_agnostic_eq::(&other.variable_definitions) + && self.directives.location_agnostic_eq::(&other.directives) + && self.selections.location_agnostic_eq::(&other.selections) + } +} + +impl LocationAgnosticPartialEq for OperationKind { + fn location_agnostic_eq(&self, other: &Self) -> bool { + self == other + } +} +impl LocationAgnosticPartialEq for Type { + 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 == 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) + } +} + +impl LocationAgnosticPartialEq for FieldID { + fn location_agnostic_eq(&self, other: &Self) -> bool { + self == other + } +} 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 90c46ff6d373c..9cf673f5b6e74 100644 --- a/compiler/crates/graphql-text-printer/src/print_full_operation.rs +++ b/compiler/crates/graphql-text-printer/src/print_full_operation.rs @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -use crate::{print_fragment, print_operation}; +use crate::{print_fragment, print_operation, PrinterOptions}; use fnv::FnvHashMap; use graphql_ir::{ FragmentDefinition, FragmentSpread, OperationDefinition, Program, ScalarField, Visitor, @@ -13,8 +13,12 @@ use graphql_ir::{ use intern::string_key::StringKey; use std::sync::Arc; -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) } @@ -22,19 +26,21 @@ 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<(StringKey, Arc)> = self.reachable_fragments.drain().collect(); @@ -51,7 +57,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 fa8cf592ee034..6f57b7dfcf8e6 100644 --- a/compiler/crates/graphql-text-printer/src/print_to_text.rs +++ b/compiler/crates/graphql-text-printer/src/print_to_text.rs @@ -7,13 +7,13 @@ use common::{Named, NamedItem, WithLocation}; use graphql_ir::{ - Argument, Condition, ConditionValue, ConstantValue, Directive, ExecutableDefinition, - FragmentDefinition, FragmentSpread, InlineFragment, LinkedField, OperationDefinition, - ScalarField, Selection, Value, VariableDefinition, + Argument, Condition, ConditionValue, ConstantArgument, ConstantValue, Directive, + ExecutableDefinition, FragmentDefinition, FragmentSpread, InlineFragment, LinkedField, + OperationDefinition, ScalarField, Selection, Value, VariableDefinition, }; use graphql_syntax::OperationKind; use intern::string_key::{Intern, StringKey}; -use schema::{SDLSchema, Schema}; +use schema::{SDLSchema, Schema, Type, TypeReference}; use std::fmt::{Result as FmtResult, Write}; pub fn print_ir(schema: &SDLSchema, definitions: &[ExecutableDefinition]) -> Vec { @@ -193,17 +193,19 @@ impl<'schema, 'writer, W: Write> Printer<'schema, 'writer, W> { OperationKind::Mutation => "mutation", OperationKind::Subscription => "subscription", }; - let operation_name = operation.name.item; - write!(self.writer, "{} {}", operation_kind, operation_name)?; + write!(self.writer, "{}", operation_kind)?; + self.space(false)?; + write!(self.writer, "{}", operation.name.item)?; self.print_variable_definitions(&operation.variable_definitions)?; self.print_directives(&operation.directives, None, None)?; self.print_selections(&operation.selections) } fn print_fragment(mut self, fragment: &FragmentDefinition) -> FmtResult { - let fragment_name = fragment.name.item; - let type_condition_name = self.schema.get_type_name(fragment.type_condition); - write!(self.writer, "fragment {}", fragment_name)?; + self.text("fragment")?; + self.space(false)?; + write!(self.writer, "{}", fragment.name.item)?; + if fragment .directives .named("argumentDefinitions".intern()) @@ -211,8 +213,9 @@ impl<'schema, 'writer, W: Write> Printer<'schema, 'writer, W> { { self.print_variable_definitions(&fragment.variable_definitions)?; } - write!(self.writer, " on {}", type_condition_name)?; + self.space(false)?; + self.print_type_condition(fragment.type_condition)?; self.print_directives( &fragment.directives, None, @@ -224,20 +227,21 @@ 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.space(true)?; + self.text("{")?; self.indentation += 1; - self.next_line()?; + self.next_line(true)?; for (i, selection) in selections.iter().enumerate() { self.print_selection(selection, None)?; if i != len - 1 { - self.next_line()?; + self.item_separator()?; } } self.indentation -= 1; - self.next_line()?; - write!(self.writer, "}}")?; + self.next_line(true)?; + self.text("}")?; } else { panic!( "Cannot print empty selections. Please, check transforms that may produce invalid selections." @@ -252,23 +256,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( @@ -304,7 +297,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.space(true)?; + self.text("@arguments")?; self.print_arguments(&field.arguments) } else { Ok(()) @@ -316,13 +310,10 @@ impl<'schema, 'writer, W: Write> Printer<'schema, 'writer, W> { field: &InlineFragment, conditions: Option>, ) -> FmtResult { - write!(self.writer, "...")?; + self.text("...")?; if let Some(type_condition) = field.type_condition { - write!( - self.writer, - " on {}", - self.schema.get_type_name(type_condition).lookup(), - )?; + self.space(true)?; + self.print_type_condition(type_condition)?; }; self.print_directives(&field.directives, conditions, None)?; self.print_selections(&field.selections) @@ -342,7 +333,7 @@ impl<'schema, 'writer, W: Write> Printer<'schema, 'writer, W> { if is_first_selection { is_first_selection = false; } else { - self.next_line()?; + self.next_line(false)?; } self.print_selection(selection, Some(accum_conditions.iter().rev().collect()))?; maybe_current_condition = None; @@ -372,16 +363,17 @@ impl<'schema, 'writer, W: Write> Printer<'schema, 'writer, W> { } fn print_directive(&mut self, directive: &Directive) -> FmtResult { - write!(self.writer, " @{}", directive.name.item)?; + self.space(true)?; + 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.next_line(false)?; write!(self.writer, "# {}", debug_line)?; } - self.next_line()?; + self.next_line(false)?; } } @@ -390,23 +382,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.space(true)?; write!( self.writer, - " @{}", + "@{}", if condition.passing_value { "include" } else { "skip" } )?; + self.text("(if")?; + self.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), + }?; + self.text(")")?; } Ok(()) } @@ -416,25 +408,19 @@ impl<'schema, 'writer, W: Write> Printer<'schema, 'writer, W> { variable_definitions: &[VariableDefinition], ) -> FmtResult { if !variable_definitions.is_empty() { - write!(self.writer, "(")?; + self.text("(")?; 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.next_line(true)?; + } else { + self.item_separator()?; } - - self.print_directives(&var_def.directives, None, None)?; + self.print_variable_definition(var_def)?; } self.indentation -= 1; - write!(self.writer, "\n)")?; + self.next_line(true)?; + self.text(")")?; } Ok(()) } @@ -444,25 +430,33 @@ impl<'schema, 'writer, W: Write> Printer<'schema, 'writer, W> { argument_definitions: &[VariableDefinition], ) -> FmtResult { if !argument_definitions.is_empty() { - write!(self.writer, " @argumentDefinitions(")?; + self.space(true)?; + self.text("@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.next_line(true)?; + } else { + self.item_separator()?; + } + write!(self.writer, "{}", arg_def.name.item)?; + self.colon_separator()?; + self.text("{type")?; + self.colon_separator()?; + self.text("\"")?; + self.print_type(&arg_def.type_)?; + self.text("\"")?; if let Some(default_value) = &arg_def.default_value { - write!(self.writer, ", defaultValue: ")?; + self.comma_separator()?; + self.text("defaultValue")?; + self.colon_separator()?; self.print_constant_value(&default_value.item)?; } - write!(self.writer, "}}")?; + self.text("}")?; } self.indentation -= 1; - write!(self.writer, "\n)")?; + self.next_line(true)?; + self.text(")")?; } Ok(()) } @@ -471,7 +465,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()); @@ -482,31 +475,24 @@ 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)?; + self.text("(")?; + 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.comma_separator()?; } } - write!(self.writer, ")")?; - Ok(()) + self.text(")") } } 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, "{{")?; + self.text("{")?; let mut first = true; let sorted_object = if self.options.sort_keys { let mut maybe_sorted_object = object.clone(); @@ -525,19 +511,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.comma_separator()?; } - write!(self.writer, "{}:", arg.name.item)?; - if !self.options.compact { - write!(self.writer, " ")?; - } - self.print_value(&arg.value.item)?; + self.print_argument(arg)?; } - write!(self.writer, "}}")?; - Ok(()) + self.text("}") } Value::List(list) => { write!(self.writer, "[")?; @@ -549,15 +527,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.comma_separator()?; } self.print_value(value)?; } - write!(self.writer, "]")?; - Ok(()) + write!(self.writer, "]") } } } @@ -577,7 +551,7 @@ impl<'schema, 'writer, W: Write> Printer<'schema, 'writer, W> { } } ConstantValue::Object(object) => { - write!(self.writer, "{{")?; + self.text("{")?; let mut first = true; let sorted_object = if self.options.sort_keys { let mut maybe_sorted_object = object.clone(); @@ -593,23 +567,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, " ")?; - } - } - 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.comma_separator()?; } - self.print_constant_value(&arg.value.item)?; + self.print_object_field(arg)?; } - write!(self.writer, "}}")?; - Ok(()) + self.text("}") } ConstantValue::List(list) => { write!(self.writer, "[")?; @@ -618,15 +580,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.comma_separator()?; } self.print_constant_value(value)?; } - write!(self.writer, "]")?; - Ok(()) + write!(self.writer, "]") } } } @@ -638,17 +596,99 @@ 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.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.colon_separator()?; + self.print_value(&argument.value.item) + } + + fn print_type_condition(&mut self, type_condition: Type) -> FmtResult { + self.text("on")?; + self.space(false)?; + write!(self.writer, "{}", self.schema.get_type_name(type_condition)) + } + + fn print_variable_definition(&mut self, var_def: &VariableDefinition) -> FmtResult { + self.print_variable(&var_def.name.item)?; + self.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_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.colon_separator()?; + self.print_constant_value(&arg.value.item) + } + + fn print_variable(&mut self, variable: &StringKey) -> FmtResult { + write!(self.writer, "${}", variable) + } + + fn print_default_value(&mut self, value: &ConstantValue) -> FmtResult { + self.space(true)?; + self.text("=")?; + self.space(true)?; + self.print_constant_value(&value) + } + + fn print_type(&mut self, type_: &TypeReference) -> FmtResult { + write!(self.writer, "{}", self.schema.get_type_string(&type_)) + } + + fn next_line(&mut self, can_collapse: bool) -> FmtResult { + if self.options.compact && can_collapse { + return Ok(()); + } writeln!(self.writer)?; for _ in 0..self.indentation { write!(self.writer, " ")?; } Ok(()) } + + fn text(&mut self, text: &str) -> FmtResult { + write!(self.writer, "{}", text) + } + + fn space(&mut self, can_collapse: bool) -> FmtResult { + if self.options.compact && can_collapse { + return Ok(()); + } + write!(self.writer, " ") + } + + fn comma_separator(&mut self) -> FmtResult { + self.text(",")?; + self.space(true) + } + + fn colon_separator(&mut self) -> FmtResult { + self.text(":")?; + self.space(true) + } + + fn item_separator(&mut self) -> FmtResult { + if self.options.compact { + write!(self.writer, ",") + } else { + self.next_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..ff25b51a9187c --- /dev/null +++ b/compiler/crates/graphql-text-printer/tests/compact/fixtures/basic_directives.expected @@ -0,0 +1,23 @@ +==================================== 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..2fd2de38a17e4 --- /dev/null +++ b/compiler/crates/graphql-text-printer/tests/compact/fixtures/compact_test.expected @@ -0,0 +1,19 @@ +==================================== 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..92e893b7905ae --- /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 + } +} \ No newline at end of file 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..f2319d0bdee08 --- /dev/null +++ b/compiler/crates/graphql-text-printer/tests/compact/fixtures/kitchen-sink.expected @@ -0,0 +1,81 @@ +==================================== 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..6e5b7acdd719a --- /dev/null +++ b/compiler/crates/graphql-text-printer/tests/compact/mod.rs @@ -0,0 +1,49 @@ +/* + * 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 common::SourceLocationKey; +use fixture_tests::Fixture; +use graphql_ir::{ + build, node_identifier::LocationAgnosticPartialEq, ExecutableDefinition, Program, +}; +use graphql_syntax::parse_executable; +use graphql_text_printer::{print_full_operation, PrinterOptions}; +use relay_test_schema::TEST_SCHEMA; +use relay_transforms::RelayLocationAgnosticBehavior; +use std::sync::Arc; + +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..e60d3899e0a4f --- /dev/null +++ b/compiler/crates/graphql-text-printer/tests/compact_test.rs @@ -0,0 +1,63 @@ +/* + * 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<> + */ + +mod compact; + +use compact::transform_fixture; +use fixture_tests::test_fixture; + + +#[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 compact_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 compact_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 compact_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_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 compact_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 compact_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 6535c3d7e05de..6c4aec13a0b98 100644 --- a/compiler/crates/graphql-text-printer/tests/operation_printer/mod.rs +++ b/compiler/crates/graphql-text-printer/tests/operation_printer/mod.rs @@ -25,7 +25,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 38f365791104e..107f0094dba63 100644 --- a/compiler/crates/relay-compiler/src/build_project/generate_artifacts.rs +++ b/compiler/crates/relay-compiler/src/build_project/generate_artifacts.rs @@ -11,7 +11,7 @@ use crate::config::{Config, ProjectConfig}; use common::{NamedItem, SourceLocationKey}; use fnv::FnvHashMap; use graphql_ir::{FragmentDefinition, OperationDefinition}; -use graphql_text_printer::OperationPrinter; +use graphql_text_printer::{OperationPrinter, PrinterOptions}; use intern::string_key::StringKey; use relay_transforms::{ ClientEdgeGeneratedQueryMetadataDirective, Programs, RefetchableDerivedFromMetadata, @@ -37,7 +37,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 b5a330ea46789..bf8cd2d22ce69 100644 --- a/compiler/crates/relay-compiler/tests/compile_relay_artifacts/mod.rs +++ b/compiler/crates/relay-compiler/tests/compile_relay_artifacts/mod.rs @@ -97,6 +97,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 { @@ -191,7 +192,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 8a64618e15928..38842c9beed11 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 @@ -74,6 +74,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 { @@ -122,7 +123,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 72acbb29b28c2..61fe043e37f75 100644 --- a/compiler/crates/relay-lsp/src/graphql_tools/mod.rs +++ b/compiler/crates/relay-lsp/src/graphql_tools/mod.rs @@ -145,6 +145,7 @@ fn print_full_operation_text(programs: Programs, operation_name: StringKey) -> O Some(print_full_operation( &programs.operation_text, print_operation_node, + Default::default(), )) } From b125784c579023f266d8d6ab6fa5c7715d839ec4 Mon Sep 17 00:00:00 2001 From: Tom Gasson Date: Tue, 28 Jun 2022 20:44:14 +1000 Subject: [PATCH 2/2] Scratch implementation of PreloadableConcreteRequest --- compiler/crates/relay-codegen/src/lib.rs | 4 +- compiler/crates/relay-codegen/src/printer.rs | 31 ++++ .../relay-codegen/src/top_level_statements.rs | 2 +- .../src/artifact_content/mod.rs | 2 +- compiler/crates/relay-compiler/src/config.rs | 133 +++++++++++++++++- 5 files changed, 164 insertions(+), 8 deletions(-) diff --git a/compiler/crates/relay-codegen/src/lib.rs b/compiler/crates/relay-codegen/src/lib.rs index ee8890f4ccfef..d8c7ed3df6056 100644 --- a/compiler/crates/relay-codegen/src/lib.rs +++ b/compiler/crates/relay-codegen/src/lib.rs @@ -14,7 +14,7 @@ mod build_ast; mod constants; mod indentation; mod printer; -mod top_level_statements; +pub mod top_level_statements; mod utils; pub use ast::{AstBuilder, Primitive, QueryID, RequestParameters}; @@ -23,7 +23,7 @@ pub use build_ast::{ }; pub use constants::CODEGEN_CONSTANTS; pub use printer::{ - print_fragment, print_operation, print_request, print_request_params, JSONPrinter, Printer, + print_fragment, print_operation, print_request, print_request_params, print_request_params_fixed, JSONPrinter, Printer, }; pub use relay_config::JsModuleFormat; pub use top_level_statements::TopLevelStatement; diff --git a/compiler/crates/relay-codegen/src/printer.rs b/compiler/crates/relay-codegen/src/printer.rs index 85203a7a03503..cf3e37c4df88c 100644 --- a/compiler/crates/relay-codegen/src/printer.rs +++ b/compiler/crates/relay-codegen/src/printer.rs @@ -60,6 +60,37 @@ pub fn print_request( ) } + +pub fn print_request_params_fixed( + schema: &SDLSchema, + operation: &OperationDefinition, + query_id: &Option, + text: &Option, + project_config: &ProjectConfig, + top_level_statements: &mut TopLevelStatements, +) -> String { + let mut request_parameters = build_request_params(operation); + if query_id.is_some() { + request_parameters.id = query_id; + } else { + request_parameters.text = text.clone(); + }; + + + let mut builder = AstBuilder::default(); + let request_parameters_ast_key = build_request_params_ast_key( + schema, + request_parameters, + &mut builder, + operation, + top_level_statements, + operation.name, + project_config, + ); + let printer = JSONPrinter::new(&builder, project_config, top_level_statements); + printer.print(request_parameters_ast_key, false) +} + pub fn print_request_params( schema: &SDLSchema, operation: &OperationDefinition, diff --git a/compiler/crates/relay-codegen/src/top_level_statements.rs b/compiler/crates/relay-codegen/src/top_level_statements.rs index 2ee0f3a3f8cd7..fcd31f6b94636 100644 --- a/compiler/crates/relay-codegen/src/top_level_statements.rs +++ b/compiler/crates/relay-codegen/src/top_level_statements.rs @@ -10,7 +10,7 @@ use indexmap::IndexMap; use std::fmt::Result as FmtResult; #[derive(Default, Clone)] -pub struct TopLevelStatements(IndexMap); +pub struct TopLevelStatements(pub IndexMap); #[derive(Clone)] pub enum TopLevelStatement { ImportStatement { name: String, path: String }, diff --git a/compiler/crates/relay-compiler/src/artifact_content/mod.rs b/compiler/crates/relay-compiler/src/artifact_content/mod.rs index b5227e3d7c42d..5e41b9643afac 100644 --- a/compiler/crates/relay-compiler/src/artifact_content/mod.rs +++ b/compiler/crates/relay-compiler/src/artifact_content/mod.rs @@ -6,7 +6,7 @@ */ mod content; -mod content_section; +pub mod content_section; use crate::config::{Config, ProjectConfig}; use common::SourceLocationKey; diff --git a/compiler/crates/relay-compiler/src/config.rs b/compiler/crates/relay-compiler/src/config.rs index 607a574a3f3d6..ff6388f106dff 100644 --- a/compiler/crates/relay-compiler/src/config.rs +++ b/compiler/crates/relay-compiler/src/config.rs @@ -4,7 +4,9 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - +use crate::artifact_content::content_section::{ + ContentSection, ContentSections, DocblockSection, GenericSection, +}; use crate::build_project::generate_extra_artifacts::GenerateExtraArtifactsFn; use crate::build_project::{ artifact_writer::{ArtifactFileWriter, ArtifactWriter}, @@ -14,6 +16,8 @@ use crate::compiler_state::{ProjectName, ProjectSet}; use crate::errors::{ConfigValidationError, Error, Result}; use crate::saved_state::SavedStateLoader; use crate::status_reporter::{ConsoleStatusReporter, StatusReporter}; +use crate::Artifact; +use crate::ArtifactContent; use async_trait::async_trait; use common::{FeatureFlags, Rollout}; use fnv::{FnvBuildHasher, FnvHashSet}; @@ -25,6 +29,10 @@ use log::warn; use persist_query::PersistError; use rayon::prelude::*; use regex::Regex; +use relay_codegen::TopLevelStatement; +use relay_codegen::{ + build_request_params, print_request_params_fixed, top_level_statements::TopLevelStatements, +}; use relay_config::{ CustomScalarType, FlowTypegenConfig, JsModuleFormat, SchemaConfig, TypegenConfig, TypegenLanguage, @@ -32,13 +40,16 @@ use relay_config::{ pub use relay_config::{ LocalPersistConfig, PersistConfig, ProjectConfig, RemotePersistConfig, SchemaLocation, }; -use relay_transforms::CustomTransformsConfig; +use relay_transforms::{is_operation_preloadable, CustomTransformsConfig, Programs}; +use schema::SDLSchema; use serde::de::Error as DeError; use serde::{Deserialize, Deserializer, Serialize}; use serde_json::Value; use sha1::{Digest, Sha1}; +use signedsource::SIGNING_TOKEN; use std::env::current_dir; use std::ffi::OsStr; +use std::fmt::Write; use std::path::{Path, PathBuf}; use std::sync::Arc; use std::{fmt, vec}; @@ -359,7 +370,117 @@ Example file: header: config_file.header, codegen_command: config_file.codegen_command, load_saved_state_file: None, - generate_extra_artifacts: None, + generate_extra_artifacts: Some(Box::new( + |project_config: &ProjectConfig, + schema: &SDLSchema, + _programs: &Programs, + artifacts: &[Artifact]| { + let mut output = vec![]; + for artifact in artifacts { + if let ArtifactContent::Operation { + normalization_operation, + id_and_text_hash, + text, + .. + } = &artifact.content + { + if is_operation_preloadable(normalization_operation) { + let mut request_parameters = + build_request_params(&normalization_operation); + request_parameters.id = &id_and_text_hash; + if id_and_text_hash.is_some() { + request_parameters.id = id_and_text_hash; + } else { + request_parameters.text = text.clone(); + }; + + let index_map: IndexMap = + IndexMap::with_hasher(Default::default()); + let mut top_level_statements = TopLevelStatements(index_map); + + let out = print_request_params_fixed( + schema, + &normalization_operation, + &id_and_text_hash, + &text, + project_config, + &mut top_level_statements, + ); + + println!("Generating artifact\n"); + println!("path: {}", artifact.path.to_string_lossy()); + + println!("file: {}", artifact.source_file.path()); + for name in &artifact.source_definition_names { + println!("source: {}", name); + } + + let mut sections = ContentSections::default(); + let mut section = DocblockSection::default(); + writeln!(section, "{}", SIGNING_TOKEN).unwrap(); + writeln!(section, "@flow").unwrap(); + writeln!(section, "@lightSyntaxTransform").unwrap(); + writeln!(section, "@nogrep").unwrap(); + + sections.push(ContentSection::Docblock(section)); + + let mut section = GenericSection::default(); + writeln!(section, "/*::").unwrap(); + writeln!( + section, + "import type {{ {} }} from '{}';", + "PreloadableConcreteRequest", "react-relay" + ) + .unwrap(); + writeln!( + section, + "import type {{ {} }} from './{}.graphql';", + &normalization_operation.name.item, + &normalization_operation.name.item + ) + .unwrap(); + writeln!(section, "*/").unwrap(); + writeln!(section, "/* eslint-disable */").unwrap(); + writeln!(section, "'use strict';").unwrap(); + writeln!( + section, + "var node/*: PreloadableConcreteRequest<{}> */ = {{", + &normalization_operation.name.item + ) + .unwrap(); + writeln!(section, " \"kind\": \"PreloadableConcreteRequest\",") + .unwrap(); + writeln!(section, " \"params\": {}", out).unwrap(); + writeln!(section, "}};").unwrap(); + writeln!(section, "module.exports = node;").unwrap(); + sections.push(ContentSection::Generic(section)); + + let bytes = sections.into_signed_bytes().unwrap(); + + let mut params_path = artifact.path.clone(); + params_path.set_file_name(format!( + "{}$Parameters", + &normalization_operation.name.item + )); + params_path.set_extension("graphql.js"); + + let art = Artifact { + source_definition_names: artifact + .source_definition_names + .clone(), + path: params_path, + content: ArtifactContent::Generic { content: bytes }, + source_file: artifact.source_file, + }; + output.push(art); + } + } + } + + output + // -> Vec + Send + Sync>; + }, + )), generate_virtual_id_file_name: None, saved_state_config: config_file.saved_state_config, saved_state_loader: None, @@ -522,7 +643,11 @@ impl fmt::Debug for Config { } = self; fn option_fn_to_string(option: &Option) -> &'static str { - if option.is_some() { "Some(Fn)" } else { "None" } + if option.is_some() { + "Some(Fn)" + } else { + "None" + } } f.debug_struct("Config")