From 92d8df8960904fcb98eaafa2b3ef1dbe03826687 Mon Sep 17 00:00:00 2001 From: Robert Mosolgo Date: Fri, 28 Oct 2016 19:35:12 -0400 Subject: [PATCH 1/9] feat(GraphQL::Compatibility) add ExecutionSpec --- lib/graphql/compatibility.rb | 15 + lib/graphql/compatibility/execution_spec.rb | 336 ++++++++++++++++++++ spec/graphql/execution_spec.rb | 3 + 3 files changed, 354 insertions(+) create mode 100644 lib/graphql/compatibility.rb create mode 100644 lib/graphql/compatibility/execution_spec.rb create mode 100644 spec/graphql/execution_spec.rb diff --git a/lib/graphql/compatibility.rb b/lib/graphql/compatibility.rb new file mode 100644 index 0000000000..7aa1bf93a0 --- /dev/null +++ b/lib/graphql/compatibility.rb @@ -0,0 +1,15 @@ +require "graphql/compatibility/execution_spec" + + +module GraphQL + # This module contains specifications for third-party GraphQL extensions. + # It's not loaded by default. To use any members of this module, add: + # + # ``` + # require "graphql/compatibility" + # ``` + # + # to your project. + module Compatibility + end +end diff --git a/lib/graphql/compatibility/execution_spec.rb b/lib/graphql/compatibility/execution_spec.rb new file mode 100644 index 0000000000..4dac8bd8ad --- /dev/null +++ b/lib/graphql/compatibility/execution_spec.rb @@ -0,0 +1,336 @@ +module GraphQL + module Compatibility + # Test an execution strategy. This spec is not meant as a development aid. + # Rather, when the strategy _works_, run it here to see if it has any differences + # from the built-in strategy. + # + # - Custom scalar input / output + # - Null propagation + # - Query-level masking + # - Directive support + # - Typecasting + # - Error handling (raise / return GraphQL::ExecutionError) + # - Provides Irep & AST node to resolve fn + # + # Some things are explicitly _not_ tested here, because they're handled + # by other parts of the system: + # + # - Schema definition (including types and fields) + # - Parsing & parse errors + # - AST -> IRep transformation (eg, fragment merging) + # - Query validation and analysis + # - Relay features + # + module ExecutionSpec + DATA = { + "1001" => OpenStruct.new({ + name: "Fannie Lou Hamer", + birthdate: Time.new(1917, 10, 6), + organization_ids: [], + }), + "1002" => OpenStruct.new({ + name: "John Lewis", + birthdate: Time.new(1940, 2, 21), + organization_ids: ["2001"], + }), + "1003" => OpenStruct.new({ + name: "Diane Nash", + birthdate: Time.new(1938, 5, 15), + organization_ids: ["2001", "2002"], + }), + "1004" => OpenStruct.new({ + name: "Ralph Abernathy", + birthdate: Time.new(1926, 3, 11), + organization_ids: ["2002"], + }), + "2001" => OpenStruct.new({ + name: "SNCC", + leader_id: nil, # fail on purpose + }), + "2002" => OpenStruct.new({ + name: "SCLC", + leader_id: "1004", + }), + } + + # Make a minitest suite for this execution strategy, making sure it + # fulfills all the requirements of this library. + # @param execution_strategy [<#new, #execute>] An execution strategy class + # @return [Class] A test suite for this execution strategy + def self.build_suite(execution_strategy) + Class.new(Minitest::Test) do + def self.build_schema(execution_strategy) + organization_type = nil + + timestamp_type = GraphQL::ScalarType.define do + name "Timestamp" + coerce_input ->(value) { Time.at(value.to_i) } + coerce_result ->(value) { value.to_i } + end + + named_entity_interface_type = GraphQL::InterfaceType.define do + name "NamedEntity" + field :name, !types.String + end + + person_type = GraphQL::ObjectType.define do + name "Person" + interfaces [named_entity_interface_type] + field :name, !types.String + field :birthdate, timestamp_type + field :age, types.Int do + argument :on, !timestamp_type + resolve ->(obj, args, ctx) { + if obj.birthdate.nil? + nil + else + age_on = args[:on] + age_years = age_on.year - obj.birthdate.year + this_year_birthday = Time.new(age_on.year, obj.birthdate.month, obj.birthdate.day) + if this_year_birthday > age_on + age_years -= 1 + end + end + age_years + } + end + field :organizations, types[organization_type] do + resolve ->(obj, args, ctx) { + obj.organization_ids.map { |id| DATA[id] } + } + end + end + + organization_type = GraphQL::ObjectType.define do + name "Organization" + interfaces [named_entity_interface_type] + field :name, !types.String + field :leader, !person_type do + resolve ->(obj, args, ctx) { + DATA[obj.leader_id] + } + end + field :returnedError, types.String do + resolve ->(o, a, c) { + GraphQL::ExecutionError.new("This error was returned") + } + end + field :raisedError, types.String do + resolve ->(o, a, c) { + raise GraphQL::ExecutionError.new("This error was raised") + } + end + + field :nodePresence, !types[!types.Boolean] do + resolve ->(o, a, ctx) { + [ + ctx.irep_node.is_a?(GraphQL::InternalRepresentation::Node), + ctx.ast_node.is_a?(GraphQL::Language::Nodes::AbstractNode), + false, # just testing + ] + } + end + end + + node_union_type = GraphQL::UnionType.define do + name "Node" + possible_types [person_type, organization_type] + end + + query_type = GraphQL::ObjectType.define do + name "Query" + field :node, node_union_type do + argument :id, !types.ID + resolve ->(obj, args, ctx) { + obj[args[:id]] + } + end + + field :organization, !organization_type do + argument :id, !types.ID + resolve ->(obj, args, ctx) { + args[:id].start_with?("2") && obj[args[:id]] + } + end + end + + GraphQL::Schema.define do + query_execution_strategy execution_strategy + query query_type + + resolve_type ->(obj, ctx) { + obj.respond_to?(:birthdate) ? person_type : organization_type + } + end + end + + @@schema = build_schema(execution_strategy) + + def execute_query(query_string, **kwargs) + kwargs[:root_value] = DATA + @@schema.execute(query_string, **kwargs) + end + + def test_it_fetches_data + query_string = %| + query getData($nodeId: ID = "1001") { + flh: node(id: $nodeId) { + __typename + ... on Person { + name @include(if: true) + skippedName: name @skip(if: true) + birthdate + age(on: 1477660133) + } + + ... on NamedEntity { + ne_tn: __typename + ne_n: name + } + + ... on Organization { + org_n: name + } + } + } + | + res = execute_query(query_string) + + assert_equal nil, res["errors"], "It doesn't have an errors key" + + flh = res["data"]["flh"] + assert_equal "Fannie Lou Hamer", flh["name"], "It returns values" + assert_equal Time.new(1917, 10, 6).to_i, flh["birthdate"], "It returns custom scalars" + assert_equal 99, flh["age"], "It runs resolve functions" + assert_equal "Person", flh["__typename"], "It serves __typename" + assert_equal "Person", flh["ne_tn"], "It serves __typename on interfaces" + assert_equal "Fannie Lou Hamer", flh["ne_n"], "It serves interface fields" + assert_equal false, flh.key?("skippedName"), "It obeys @skip" + assert_equal false, flh.key?("org_n"), "It doesn't apply other type fields" + end + + def test_it_propagates_nulls_to_field + query_string = %| + query getOrg($id: ID = "2001"){ + failure: node(id: $id) { + ... on Organization { + name + leader { name } + } + } + success: node(id: $id) { + ... on Organization { + name + } + } + } + | + res = execute_query(query_string) + + failure = res["data"]["failure"] + success = res["data"]["success"] + + assert_equal nil, failure, "It propagates nulls to the next nullable field" + assert_equal "SNCC", success["name"], "It serves the same object if no invalid null is encountered" + assert_equal 1, res["errors"].length , "It returns an error for the invalid null" + end + + def test_it_propages_nulls_to_operation + query_string = %| + { + foundOrg: organization(id: "2001") { + name + } + organization(id: "2999") { + name + } + } + | + + res = execute_query(query_string) + assert_equal nil, res["data"] + assert_equal 1, res["errors"].length + end + + def test_it_exposes_raised_and_returned_user_execution_errors + query_string = %| + { + organization(id: "2001") { + name + returnedError + raisedError + } + } + | + + res = execute_query(query_string) + + assert_equal "SNCC", res["data"]["organization"]["name"], "It runs the rest of the query" + + expected_returned_error = { + "message"=>"This error was returned", + "locations"=>[{"line"=>5, "column"=>19}], + "path"=>["organization", "returnedError"] + } + assert_includes res["errors"], expected_returned_error, "It turns returned errors into response errors" + + expected_raised_error = { + "message"=>"This error was raised", + "locations"=>[{"line"=>6, "column"=>19}], + "path"=>["organization", "raisedError"] + } + assert_includes res["errors"], expected_raised_error, "It turns raised errors into response errors" + end + + def test_it_applies_masking + no_org = ->(member) { member.name == "Organization" } + query_string = %| + { + node(id: "2001") { + __typename + } + }| + + assert_raises(GraphQL::UnresolvedTypeError) { + execute_query(query_string, except: no_org) + } + + query_string = %| + { + organization(id: "2001") { name } + }| + + res = execute_query(query_string, except: no_org) + + assert_equal nil, res["data"] + assert_equal 1, res["errors"].length + + query_string = %| + { + __type(name: "Organization") { name } + }| + + res = execute_query(query_string, except: no_org) + + assert_equal nil, res["data"]["__type"] + assert_equal nil, res["errors"] + end + + def test_it_provides_nodes_to_resolve + query_string = %| + { + organization(id: "2001") { + name + nodePresence + } + }| + + res = execute_query(query_string) + assert_equal "SNCC", res["data"]["organization"]["name"] + assert_equal [true, true, false], res["data"]["organization"]["nodePresence"] + end + end + end + end + end +end diff --git a/spec/graphql/execution_spec.rb b/spec/graphql/execution_spec.rb new file mode 100644 index 0000000000..b82eed1fea --- /dev/null +++ b/spec/graphql/execution_spec.rb @@ -0,0 +1,3 @@ +require "spec_helper" +require "graphql/compatibility" +SerialExecutionSuite = GraphQL::Compatibility::ExecutionSpec.build_suite(GraphQL::Query::SerialExecution) From ddb75018c3caba6df8d03f8972fd2223c9ea267a Mon Sep 17 00:00:00 2001 From: Robert Mosolgo Date: Sat, 29 Oct 2016 21:35:08 -0400 Subject: [PATCH 2/9] feat(Compatibility) introduce QueryParserSpec --- Rakefile | 2 +- lib/graphql.rb | 1 + lib/graphql/compatibility.rb | 16 +- .../compatibility/query_parser_spec.rb | 126 +++++ .../parse_error_specification.rb | 78 ++++ .../query_parser_spec/query_assertions.rb | 78 ++++ .../compatibility/schema_parser_spec.rb | 102 ++++ lib/graphql/language/parser_tests.rb | 440 ------------------ .../execution_spec_spec.rb} | 2 +- .../compatibility/query_parser_spec_spec.rb | 5 + .../compatibility/schema_parser_spec_spec.rb | 5 + 11 files changed, 399 insertions(+), 456 deletions(-) create mode 100644 lib/graphql/compatibility/query_parser_spec.rb create mode 100644 lib/graphql/compatibility/query_parser_spec/parse_error_specification.rb create mode 100644 lib/graphql/compatibility/query_parser_spec/query_assertions.rb create mode 100644 lib/graphql/compatibility/schema_parser_spec.rb rename spec/graphql/{execution_spec.rb => compatibility/execution_spec_spec.rb} (80%) create mode 100644 spec/graphql/compatibility/query_parser_spec_spec.rb create mode 100644 spec/graphql/compatibility/schema_parser_spec_spec.rb diff --git a/Rakefile b/Rakefile index 06ea60b417..3b635cdc6f 100644 --- a/Rakefile +++ b/Rakefile @@ -20,7 +20,7 @@ task(default: [:test, :rubocop]) desc "Use Racc & Ragel to regenerate parser.rb & lexer.rb from configuration files" task :build_parser do - `rm lib/graphql/language/parser.rb lib/graphql/language/lexer.rb ` + `rm -f lib/graphql/language/parser.rb lib/graphql/language/lexer.rb ` `racc lib/graphql/language/parser.y -o lib/graphql/language/parser.rb` `ragel -R lib/graphql/language/lexer.rl` end diff --git a/lib/graphql.rb b/lib/graphql.rb index 7457ca3bb6..ed42b2c6ac 100644 --- a/lib/graphql.rb +++ b/lib/graphql.rb @@ -82,3 +82,4 @@ def self.scan_with_ragel(query_string) require "graphql/version" require "graphql/relay" require "graphql/execution" +require "graphql/compatibility" diff --git a/lib/graphql/compatibility.rb b/lib/graphql/compatibility.rb index 7aa1bf93a0..653628bef0 100644 --- a/lib/graphql/compatibility.rb +++ b/lib/graphql/compatibility.rb @@ -1,15 +1,3 @@ require "graphql/compatibility/execution_spec" - - -module GraphQL - # This module contains specifications for third-party GraphQL extensions. - # It's not loaded by default. To use any members of this module, add: - # - # ``` - # require "graphql/compatibility" - # ``` - # - # to your project. - module Compatibility - end -end +require "graphql/compatibility/query_parser_spec" +require "graphql/compatibility/schema_parser_spec" diff --git a/lib/graphql/compatibility/query_parser_spec.rb b/lib/graphql/compatibility/query_parser_spec.rb new file mode 100644 index 0000000000..b1807cb8ee --- /dev/null +++ b/lib/graphql/compatibility/query_parser_spec.rb @@ -0,0 +1,126 @@ +require "graphql/compatibility/query_parser_spec/query_assertions" +require "graphql/compatibility/query_parser_spec/parse_error_specification" + +module GraphQL + module Compatibility + # This asserts that a given parse function turns a string into + # the proper tree of {{GraphQL::Language::Nodes}}. + module QueryParserSpec + # @yieldparam query_string [String] A query string to parse + # @yieldreturn [GraphQL::Language::Nodes::Document] + # @return [Class] A test suite for this parse function + def self.build_suite(&block) + Class.new(Minitest::Test) do + include QueryAssertions + include ParseErrorSpecification + + @@parse_fn = block + + def parse(query_string) + @@parse_fn.call(query_string) + end + + def test_it_parses_queries + document = parse(QUERY_STRING) + query = document.definitions.first + assert_valid_query(query) + assert_valid_fragment(document.definitions.last) + assert_valid_variable(query.variables.first) + field = query.selections.first + assert_valid_field(field) + assert_valid_variable_argument(field.arguments.first) + assert_valid_literal_argument(field.arguments.last) + assert_valid_directive(field.directives.first) + fragment_spread = query.selections[1].selections.last + assert_valid_fragment_spread(fragment_spread) + assert_valid_typed_inline_fragment(query.selections[2]) + assert_valid_typeless_inline_fragment(query.selections[3]) + end + + def test_it_parses_empty_arguments + strings = [ + "{ field { } }", + "{ field() }", + ] + strings.each do |query_str| + doc = parse(query_str) + field = doc.definitions.first.selections.first + assert_equal 0, field.arguments.length + assert_equal 0, field.selections.length + end + end + + def test_it_parses_unnamed_queries + document = parse("{ name, age, height }") + operation = document.definitions.first + assert_equal 1, document.definitions.length + assert_equal "query", operation.operation_type + assert_equal nil, operation.name + assert_equal 3, operation.selections.length + end + + def test_it_parses_inputs + query_string = %| + { + field( + int: 3, + float: 4.7e-24, + bool: false, + string: "β˜€οΈŽπŸ†\\n escaped \\" unicode \\u00b6 /", + enum: ENUM_NAME, + array: [7, 8, 9] + object: {a: [1,2,3], b: {c: "4"}} + unicode_bom: "\xef\xbb\xbfquery" + keywordEnum: on + ) + } + | + document = parse(query_string) + inputs = document.definitions.first.selections.first.arguments + assert_equal 3, inputs[0].value, "Integers" + assert_equal 0.47e-23, inputs[1].value, "Floats" + assert_equal false, inputs[2].value, "Booleans" + assert_equal %|β˜€οΈŽπŸ†\n escaped " unicode ΒΆ /|, inputs[3].value, "Strings" + assert_instance_of GraphQL::Language::Nodes::Enum, inputs[4].value + assert_equal "ENUM_NAME", inputs[4].value.name, "Enums" + assert_equal [7,8,9], inputs[5].value, "Lists" + + obj = inputs[6].value + assert_equal "a", obj.arguments[0].name + assert_equal [1,2,3], obj.arguments[0].value + assert_equal "b", obj.arguments[1].name + assert_equal "c", obj.arguments[1].value.arguments[0].name + assert_equal "4", obj.arguments[1].value.arguments[0].value + + assert_equal %|\xef\xbb\xbfquery|, inputs[7].value, "Unicode BOM" + assert_equal "on", inputs[8].value.name, "Enum value 'on'" + end + end + end + + QUERY_STRING = %| + query getStuff($someVar: Int = 1, $anotherVar: [String!] ) @skip(if: false) { + myField: someField(someArg: $someVar, ok: 1.4) @skip(if: $anotherVar) @thing(or: "Whatever") + + anotherField(someArg: [1,2,3]) { + nestedField + ... moreNestedFields @skip(if: true) + } + + ... on OtherType @include(unless: false){ + field(arg: [{key: "value", anotherKey: 0.9, anotherAnotherKey: WHATEVER}]) + anotherField + } + + ... { + id + } + } + + fragment moreNestedFields on NestedType @or(something: "ok") { + anotherNestedField @enum(directive: true) + } + | + end + end +end diff --git a/lib/graphql/compatibility/query_parser_spec/parse_error_specification.rb b/lib/graphql/compatibility/query_parser_spec/parse_error_specification.rb new file mode 100644 index 0000000000..0aae96725b --- /dev/null +++ b/lib/graphql/compatibility/query_parser_spec/parse_error_specification.rb @@ -0,0 +1,78 @@ +module GraphQL + module Compatibility + # Include me into a minitest class + # to add assertions about parse errors + module ParseErrorSpecification + def assert_raises_parse_error(query_string) + assert_raises(GraphQL::ParseError) { + parse(query_string) + } + end + + def test_it_includes_line_and_column + err = assert_raises_parse_error(" + query getCoupons { + allCoupons: {data{id}} + } + ") + + assert_includes(err.message, '"{"') + assert_equal(3, err.line) + assert_equal(25, err.col) + end + + def test_it_rejects_unterminated_strings + assert_raises_parse_error('{ " }') + assert_raises_parse_error(%|{ "\n" }|) + end + + def test_it_rejects_unexpected_ends + assert_raises_parse_error("query { stuff { thing }") + end + + def assert_rejects_character(char) + err = assert_raises_parse_error("{ field#{char} }") + assert_includes(err.message, char.inspect, "The message includes the invalid character") + end + + def test_it_rejects_invalid_characters + assert_rejects_character(";") + assert_rejects_character("\a") + assert_rejects_character("\xef") + assert_rejects_character("\v") + assert_rejects_character("\f") + assert_rejects_character("\xa0") + end + + def test_it_rejects_bad_unicode + assert_raises_parse_error(%|{ field(arg:"\\x") }|) + assert_raises_parse_error(%|{ field(arg:"\\u1") }|) + assert_raises_parse_error(%|{ field(arg:"\\u0XX1") }|) + assert_raises_parse_error(%|{ field(arg:"\\uXXXX") }|) + assert_raises_parse_error(%|{ field(arg:"\\uFXXX") }|) + assert_raises_parse_error(%|{ field(arg:"\\uXXXF") }|) + end + + def assert_empty_document(query_string) + doc = parse(query_string) + assert_equal 0, doc.definitions.length + end + + def test_it_parses_blank_queries + assert_empty_document("") + assert_empty_document(" ") + assert_empty_document("\t \t") + end + + def test_it_restricts_on + assert_raises_parse_error("{ ...on }") + assert_raises_parse_error("fragment on on Type { field }") + end + + def test_it_rejects_null + err = assert_raises_parse_error("{ field(input: null) }") + assert_includes(err.message, "null") + end + end + end +end diff --git a/lib/graphql/compatibility/query_parser_spec/query_assertions.rb b/lib/graphql/compatibility/query_parser_spec/query_assertions.rb new file mode 100644 index 0000000000..3e3849524a --- /dev/null +++ b/lib/graphql/compatibility/query_parser_spec/query_assertions.rb @@ -0,0 +1,78 @@ +module GraphQL + module Compatibility + module QueryParserSpec + module QueryAssertions + def assert_valid_query(query) + assert query.is_a?(GraphQL::Language::Nodes::OperationDefinition) + assert_equal "getStuff", query.name + assert_equal "query", query.operation_type + assert_equal 2, query.variables.length + assert_equal 4, query.selections.length + assert_equal 1, query.directives.length + assert_equal [2, 13], [query.line, query.col] + end + + def assert_valid_fragment(fragment_def) + assert fragment_def.is_a?(GraphQL::Language::Nodes::FragmentDefinition) + assert_equal "moreNestedFields", fragment_def.name + assert_equal 1, fragment_def.selections.length + assert_equal "NestedType", fragment_def.type.name + assert_equal 1, fragment_def.directives.length + assert_equal [20, 13], fragment_def.position + end + + def assert_valid_variable(variable) + assert_equal "someVar", variable.name + assert_equal "Int", variable.type.name + assert_equal 1, variable.default_value + assert_equal [2, 28], variable.position + end + + def assert_valid_field(field) + assert_equal "someField", field.name + assert_equal "myField", field.alias + assert_equal 2, field.directives.length + assert_equal 2, field.arguments.length + assert_equal 0, field.selections.length + assert_equal [3, 15], field.position + end + + def assert_valid_literal_argument(argument) + assert_equal "ok", argument.name + assert_equal 1.4, argument.value + end + + def assert_valid_variable_argument(argument) + assert_equal "someArg", argument.name + assert_equal "someVar", argument.value.name + end + + def assert_valid_fragment_spread(fragment_spread) + assert_equal "moreNestedFields", fragment_spread.name + assert_equal 1, fragment_spread.directives.length + assert_equal [7, 17], fragment_spread.position + end + + def assert_valid_directive(directive) + assert_equal "skip", directive.name + assert_equal "if", directive.arguments.first.name + assert_equal 1, directive.arguments.length + assert_equal [3, 62], directive.position + end + + def assert_valid_typed_inline_fragment(inline_fragment) + assert_equal "OtherType", inline_fragment.type.name + assert_equal 2, inline_fragment.selections.length + assert_equal 1, inline_fragment.directives.length + assert_equal [10, 15], inline_fragment.position + end + + def assert_valid_typeless_inline_fragment(inline_fragment) + assert_equal nil, inline_fragment.type + assert_equal 1, inline_fragment.selections.length + assert_equal 0, inline_fragment.directives.length + end + end + end + end +end diff --git a/lib/graphql/compatibility/schema_parser_spec.rb b/lib/graphql/compatibility/schema_parser_spec.rb new file mode 100644 index 0000000000..20b91b2ed5 --- /dev/null +++ b/lib/graphql/compatibility/schema_parser_spec.rb @@ -0,0 +1,102 @@ +module GraphQL + module Compatibility + # This asserts that a given parse function turns a string into + # the proper tree of {{GraphQL::Language::Nodes}}. + module SchemaParserSpec + # @yieldparam query_string [String] A query string to parse + # @yieldreturn [GraphQL::Language::Nodes::Document] + # @return [Class] A test suite for this parse function + def self.build_suite(&block) + Class.new(Minitest::Test) do + @@parse_fn = block + + def parse(query_string) + @@parse_fn.call(query_string) + end + + def test_it_parses_schema_definition + document = parse(SCHEMA_DEFINITION_STRING) + + assert_equal 6, document.definitions.size + + schema_definition = document.definitions.shift + assert_equal GraphQL::Language::Nodes::SchemaDefinition, schema_definition.class + + directive_definition = document.definitions.shift + assert_equal GraphQL::Language::Nodes::DirectiveDefinition, directive_definition.class + assert_equal 'This is a directive', directive_definition.description + + enum_type_definition = document.definitions.shift + assert_equal GraphQL::Language::Nodes::EnumTypeDefinition, enum_type_definition.class + assert_equal "Multiline comment\n\nWith an enum", enum_type_definition.description + + assert_nil enum_type_definition.values[0].description + assert_equal 'Not a creative color', enum_type_definition.values[1].description + + object_type_definition = document.definitions.shift + assert_equal GraphQL::Language::Nodes::ObjectTypeDefinition, object_type_definition.class + assert_equal 'Comment without preceding space', object_type_definition.description + assert_equal 'And a field to boot', object_type_definition.fields[0].description + + input_object_type_definition = document.definitions.shift + assert_equal GraphQL::Language::Nodes::InputObjectTypeDefinition, input_object_type_definition.class + assert_equal 'Comment for input object types', input_object_type_definition.description + assert_equal 'Color of the car', input_object_type_definition.fields[0].description + + interface_type_definition = document.definitions.shift + assert_equal GraphQL::Language::Nodes::InterfaceTypeDefinition, interface_type_definition.class + assert_equal 'Comment for interface definitions', interface_type_definition.description + assert_equal 'Amount of wheels', interface_type_definition.fields[0].description + end + end + end + + SCHEMA_DEFINITION_STRING = %| + # Schema at beginning of file + + schema { + query: Hello + } + + # Comment between two definitions are omitted + + # This is a directive + directive @foo( + # It has an argument + arg: Int + ) on FIELD + + # Multiline comment + # + # With an enum + enum Color { + RED + + # Not a creative color + GREEN + BLUE + } + + #Comment without preceding space + type Hello { + # And a field to boot + str: String + } + + # Comment for input object types + input Car { + # Color of the car + color: String! + } + + # Comment for interface definitions + interface Vehicle { + # Amount of wheels + wheels: Int! + } + + # Comment at the end of schema + | + end + end +end diff --git a/lib/graphql/language/parser_tests.rb b/lib/graphql/language/parser_tests.rb index 66fc8104a2..3d06007617 100644 --- a/lib/graphql/language/parser_tests.rb +++ b/lib/graphql/language/parser_tests.rb @@ -13,268 +13,8 @@ module ParserTests def self.included(test) test.send(:describe, "Parser Tests") do let(:document) { subject.parse(query_string) } - let(:query_string) {%| - query getStuff($someVar: Int = 1, $anotherVar: [String!] ) @skip(if: false) { - myField: someField(someArg: $someVar, ok: 1.4) @skip(if: $anotherVar) @thing(or: "Whatever") - - anotherField(someArg: [1,2,3]) { - nestedField - ... moreNestedFields @skip(if: true) - } - - ... on OtherType @include(unless: false){ - field(arg: [{key: "value", anotherKey: 0.9, anotherAnotherKey: WHATEVER}]) - anotherField - } - - ... { - id - } - } - - fragment moreNestedFields on NestedType @or(something: "ok") { - anotherNestedField @enum(directive: true) - } - |} describe ".parse" do - it "parses queries" do - assert document - end - - describe "visited nodes" do - let(:query) { document.definitions.first } - let(:fragment_def) { document.definitions.last } - - it "creates a valid document" do - assert document.is_a?(GraphQL::Language::Nodes::Document) - assert_equal 2, document.definitions.length - end - - it "creates a valid operation" do - assert query.is_a?(GraphQL::Language::Nodes::OperationDefinition) - assert_equal "getStuff", query.name - assert_equal "query", query.operation_type - assert_equal 2, query.variables.length - assert_equal 4, query.selections.length - assert_equal 1, query.directives.length - assert_equal [2, 13], [query.line, query.col] - end - - it "creates a valid fragment definition" do - assert fragment_def.is_a?(GraphQL::Language::Nodes::FragmentDefinition) - assert_equal "moreNestedFields", fragment_def.name - assert_equal 1, fragment_def.selections.length - assert_equal "NestedType", fragment_def.type.name - assert_equal 1, fragment_def.directives.length - assert_equal [20, 13], fragment_def.position - end - - describe "variable definitions" do - let(:optional_var) { query.variables.first } - it "gets name and type" do - assert_equal "someVar", optional_var.name - assert_equal "Int", optional_var.type.name - end - - it "gets default value" do - assert_equal 1, optional_var.default_value - end - - it "gets position info" do - assert_equal [2, 28], optional_var.position - end - end - - describe "fields" do - let(:leaf_field) { query.selections.first } - let(:parent_field) { query.selections[1] } - - it "gets name, alias, arguments and directives" do - assert_equal "someField", leaf_field.name - assert_equal "myField", leaf_field.alias - assert_equal 2, leaf_field.directives.length - assert_equal 2, leaf_field.arguments.length - end - - it "gets nested fields" do - assert_equal 2, parent_field.selections.length - end - - it "gets location info" do - assert_equal [3 ,15], leaf_field.position - end - - describe "when the arguments list is empty" do - let(:query_string) { "{ field() }"} - let(:field) { query.selections.first } - it "has zero arguments" do - assert_equal 0, field.arguments.length - end - end - - describe "when selections are empty" do - let(:query_string) { "{ field { } }"} - let(:field) { query.selections.first } - it "has zero selections" do - assert_equal 0, field.selections.length - end - end - end - - describe "arguments" do - let(:literal_argument) { query.selections.first.arguments.last } - let(:variable_argument) { query.selections.first.arguments.first } - - it "gets name and literal value" do - assert_equal "ok", literal_argument.name - assert_equal 1.4, literal_argument.value - end - - it "gets name and variable value" do - assert_equal "someArg", variable_argument.name - assert_equal "someVar", variable_argument.value.name - end - - - it "gets position info" do - assert_equal [3, 34], variable_argument.position - end - end - - describe "fragment spreads" do - let(:fragment_spread) { query.selections[1].selections.last } - it "gets the name and directives" do - assert_equal "moreNestedFields", fragment_spread.name - assert_equal 1, fragment_spread.directives.length - end - - it "gets position info" do - assert_equal [7, 17], fragment_spread.position - end - end - - describe "directives" do - let(:variable_directive) { query.selections.first.directives.first } - - it "gets the name and arguments" do - assert_equal "skip", variable_directive.name - assert_equal "if", variable_directive.arguments.first.name - assert_equal 1, variable_directive.arguments.length - end - - it "gets position info" do - assert_equal [3, 62], variable_directive.position - end - end - - describe "inline fragments" do - let(:inline_fragment) { query.selections[2] } - let(:typeless_inline_fragment) { query.selections[3] } - - it "gets the type and directives" do - assert_equal "OtherType", inline_fragment.type.name - assert_equal 2, inline_fragment.selections.length - assert_equal 1, inline_fragment.directives.length - end - - it "gets inline fragments without type conditions" do - assert_equal nil, typeless_inline_fragment.type - assert_equal 1, typeless_inline_fragment.selections.length - assert_equal 0, typeless_inline_fragment.directives.length - end - - it "gets position info" do - assert_equal [10, 15], inline_fragment.position - end - end - - describe "inputs" do - let(:query_string) {%| - { - field( - int: 3, - float: 4.7e-24, - bool: false, - string: "β˜€οΈŽπŸ†\\n escaped \\" unicode \\u00b6 /", - enum: ENUM_NAME, - array: [7, 8, 9] - object: {a: [1,2,3], b: {c: "4"}} - unicode_bom: "\xef\xbb\xbfquery" - keywordEnum: on - ) - } - |} - - let(:inputs) { document.definitions.first.selections.first.arguments } - - it "parses ints" do - assert_equal 3, inputs[0].value - end - - it "parses floats" do - assert_equal 0.47e-23, inputs[1].value - end - - it "parses booleans" do - assert_equal false, inputs[2].value - end - - it "parses UTF-8 strings" do - assert_equal %|β˜€οΈŽπŸ†\n escaped " unicode ΒΆ /|, inputs[3].value - end - - it "parses enums" do - assert_instance_of GraphQL::Language::Nodes::Enum, inputs[4].value - assert_equal "ENUM_NAME", inputs[4].value.name - end - - it "parses arrays" do - assert_equal [7,8,9], inputs[5].value - end - - it "parses objects" do - obj = inputs[6].value - assert_equal "a", obj.arguments[0].name - assert_equal [1,2,3], obj.arguments[0].value - assert_equal "b", obj.arguments[1].name - assert_equal "c", obj.arguments[1].value.arguments[0].name - assert_equal "4", obj.arguments[1].value.arguments[0].value - end - - it "parses unicode bom" do - obj = inputs[7].value - assert_equal %|\xef\xbb\xbfquery|, inputs[7].value - end - - it "parses enum 'on''" do - assert_equal "on", inputs[8].value.name - end - end - end - - describe "unnamed queries" do - let(:query_string) {%| - { name, age, height } - |} - let(:operation) { document.definitions.first } - - it "parses unnamed queries" do - assert_equal 1, document.definitions.length - assert_equal "query", operation.operation_type - assert_equal nil, operation.name - assert_equal 3, operation.selections.length - end - end - - describe "introspection query" do - let(:query_string) { GraphQL::Introspection::INTROSPECTION_QUERY } - - it "parses a big ol' query" do - assert(document) - end - end - describe "schema with comments" do let(:query_string) {%| # Schema at beginning of file @@ -622,186 +362,6 @@ def self.included(test) end end end - - describe "errors" do - let(:query_string) {%| query doSomething { bogus { } |} - it "raises a parse error" do - err = assert_raises(GraphQL::ParseError) { document } - end - - it "correctly identifies parse error location and content" do - e = assert_raises(GraphQL::ParseError) do - GraphQL.parse(" - query getCoupons { - allCoupons: {data{id}} - } - ") - end - assert_includes(e.message, '"{"') - assert_includes(e.message, "LCURLY") - assert_equal(3, e.line) - assert_equal(33, e.col) - end - - it "handles unexpected ends" do - err = assert_raises(GraphQL::ParseError) { GraphQL.parse("{ ") } - assert_equal "Unexpected end of document", err.message - end - - it "rejects unsupported characters" do - e = assert_raises(GraphQL::ParseError) do - GraphQL.parse("{ field; }") - end - - assert_includes(e.message, "Parse error on \";\"") - end - - it "rejects control characters" do - e = assert_raises(GraphQL::ParseError) do - GraphQL.parse("{ \afield }") - end - - assert_includes(e.message, "Parse error on \"\\a\"") - end - - it "rejects partial BOM" do - e = assert_raises(GraphQL::ParseError) do - GraphQL.parse("{ \xeffield }") - end - - assert_includes(e.message, "Parse error on \"\\xEF\"") - end - - it "rejects vertical tabs" do - e = assert_raises(GraphQL::ParseError) do - GraphQL.parse("{ \vfield }") - end - - assert_includes(e.message, "Parse error on \"\\v\"") - end - - it "rejects form feed" do - e = assert_raises(GraphQL::ParseError) do - GraphQL.parse("{ \ffield }") - end - - assert_includes(e.message, "Parse error on \"\\f\"") - end - - it "rejects no break space" do - e = assert_raises(GraphQL::ParseError) do - GraphQL.parse("{ \xa0field }") - end - - assert_includes(e.message, "Parse error on \"\\xA0\"") - end - - it "rejects unterminated strings" do - e = assert_raises(GraphQL::ParseError) do - GraphQL.parse("\"") - end - - assert_includes(e.message, "Parse error on \"\\\"\"") - - e = assert_raises(GraphQL::ParseError) do - GraphQL.parse("\"\n\"") - end - - assert_includes(e.message, "Parse error on \"\\n\"") - end - - it "rejects bad escape sequence in strings" do - e = assert_raises(GraphQL::ParseError) do - GraphQL.parse("{ field(arg:\"\\x\") }") - end - - assert_includes(e.message, "Parse error on bad Unicode escape sequence") - end - - it "rejects incomplete escape sequence in strings" do - e = assert_raises(GraphQL::ParseError) do - GraphQL.parse("{ field(arg:\"\\u1\") }") - end - - assert_includes(e.message, "bad Unicode escape sequence") - end - - it "rejects unicode escape with bad chars" do - e = assert_raises(GraphQL::ParseError) do - GraphQL.parse("{ field(arg:\"\\u0XX1\") }") - end - - assert_includes(e.message, "bad Unicode escape sequence") - - e = assert_raises(GraphQL::ParseError) do - GraphQL.parse("{ field(arg:\"\\uXXXX\") }") - end - - assert_includes(e.message, "bad Unicode escape sequence") - - - e = assert_raises(GraphQL::ParseError) do - GraphQL.parse("{ field(arg:\"\\uFXXX\") }") - end - - assert_includes(e.message, "bad Unicode escape sequence") - - - e = assert_raises(GraphQL::ParseError) do - GraphQL.parse("{ field(arg:\"\\uXXXF\") }") - end - - assert_includes(e.message, "bad Unicode escape sequence") - end - - it "rejects fragments named 'on'" do - e = assert_raises(GraphQL::ParseError) do - GraphQL.parse("fragment on on on { on }") - end - - assert_includes(e.message, "Parse error on \"on\"") - end - - it "rejects fragment spread of 'on'" do - e = assert_raises(GraphQL::ParseError) do - GraphQL.parse("{ ...on }") - end - - assert_includes(e.message, "Parse error on \"}\"") - end - - it "rejects null value" do - e = assert_raises(GraphQL::ParseError) do - GraphQL.parse("{ fieldWithNullableStringInput(input: null) }") - end - - assert_includes(e.message, "Parse error on \"null\"") - end - end - - - describe "whitespace" do - describe "whitespace-only queries" do - let(:query_string) { " " } - it "doesn't blow up" do - assert_equal [], document.definitions - end - end - - describe "empty string queries" do - let(:query_string) { "" } - it "doesn't blow up" do - assert_equal [], document.definitions - end - end - - describe "using tabs as whitespace" do - let(:query_string) { "\t{\t\tid, \tname}"} - it "parses the query" do - assert_equal 1, document.definitions.length - end - end - end end end end diff --git a/spec/graphql/execution_spec.rb b/spec/graphql/compatibility/execution_spec_spec.rb similarity index 80% rename from spec/graphql/execution_spec.rb rename to spec/graphql/compatibility/execution_spec_spec.rb index b82eed1fea..96f88e2be9 100644 --- a/spec/graphql/execution_spec.rb +++ b/spec/graphql/compatibility/execution_spec_spec.rb @@ -1,3 +1,3 @@ require "spec_helper" -require "graphql/compatibility" + SerialExecutionSuite = GraphQL::Compatibility::ExecutionSpec.build_suite(GraphQL::Query::SerialExecution) diff --git a/spec/graphql/compatibility/query_parser_spec_spec.rb b/spec/graphql/compatibility/query_parser_spec_spec.rb new file mode 100644 index 0000000000..a7d7920598 --- /dev/null +++ b/spec/graphql/compatibility/query_parser_spec_spec.rb @@ -0,0 +1,5 @@ +require "spec_helper" + +BuiltInParserSuite = GraphQL::Compatibility::QueryParserSpec.build_suite do |query_string| + GraphQL::Language::Parser.parse(query_string) +end diff --git a/spec/graphql/compatibility/schema_parser_spec_spec.rb b/spec/graphql/compatibility/schema_parser_spec_spec.rb new file mode 100644 index 0000000000..ee0b6779ce --- /dev/null +++ b/spec/graphql/compatibility/schema_parser_spec_spec.rb @@ -0,0 +1,5 @@ +require "spec_helper" + +BuiltInParserSuite = GraphQL::Compatibility::SchemaParserSpec.build_suite do |query_string| + GraphQL::Language::Parser.parse(query_string) +end From 99723da1ec2c087379f4da72b0de592aa7db3661 Mon Sep 17 00:00:00 2001 From: Robert Mosolgo Date: Sat, 29 Oct 2016 21:40:14 -0400 Subject: [PATCH 3/9] refactor(Compatibility) rename spec -> specification --- lib/graphql/compatibility.rb | 6 +- ...ion_spec.rb => execution_specification.rb} | 2 +- .../parse_error_specification.rb | 78 ------------------- ..._spec.rb => query_parser_specification.rb} | 6 +- .../parse_error_specification.rb | 78 +++++++++++++++++++ .../query_assertions.rb | 2 +- ...spec.rb => schema_parser_specification.rb} | 39 +++++++++- .../compatibility/execution_spec_spec.rb | 3 - .../execution_specification_spec.rb | 3 + .../compatibility/query_parser_spec_spec.rb | 5 -- .../query_parser_specification_spec.rb | 5 ++ .../compatibility/schema_parser_spec_spec.rb | 5 -- .../schema_parser_specification_spec.rb | 5 ++ 13 files changed, 137 insertions(+), 100 deletions(-) rename lib/graphql/compatibility/{execution_spec.rb => execution_specification.rb} (99%) delete mode 100644 lib/graphql/compatibility/query_parser_spec/parse_error_specification.rb rename lib/graphql/compatibility/{query_parser_spec.rb => query_parser_specification.rb} (95%) create mode 100644 lib/graphql/compatibility/query_parser_specification/parse_error_specification.rb rename lib/graphql/compatibility/{query_parser_spec => query_parser_specification}/query_assertions.rb (98%) rename lib/graphql/compatibility/{schema_parser_spec.rb => schema_parser_specification.rb} (70%) delete mode 100644 spec/graphql/compatibility/execution_spec_spec.rb create mode 100644 spec/graphql/compatibility/execution_specification_spec.rb delete mode 100644 spec/graphql/compatibility/query_parser_spec_spec.rb create mode 100644 spec/graphql/compatibility/query_parser_specification_spec.rb delete mode 100644 spec/graphql/compatibility/schema_parser_spec_spec.rb create mode 100644 spec/graphql/compatibility/schema_parser_specification_spec.rb diff --git a/lib/graphql/compatibility.rb b/lib/graphql/compatibility.rb index 653628bef0..4068dc71a7 100644 --- a/lib/graphql/compatibility.rb +++ b/lib/graphql/compatibility.rb @@ -1,3 +1,3 @@ -require "graphql/compatibility/execution_spec" -require "graphql/compatibility/query_parser_spec" -require "graphql/compatibility/schema_parser_spec" +require "graphql/compatibility/execution_specification" +require "graphql/compatibility/query_parser_specification" +require "graphql/compatibility/schema_parser_specification" diff --git a/lib/graphql/compatibility/execution_spec.rb b/lib/graphql/compatibility/execution_specification.rb similarity index 99% rename from lib/graphql/compatibility/execution_spec.rb rename to lib/graphql/compatibility/execution_specification.rb index 4dac8bd8ad..6431cdc774 100644 --- a/lib/graphql/compatibility/execution_spec.rb +++ b/lib/graphql/compatibility/execution_specification.rb @@ -21,7 +21,7 @@ module Compatibility # - Query validation and analysis # - Relay features # - module ExecutionSpec + module ExecutionSpecification DATA = { "1001" => OpenStruct.new({ name: "Fannie Lou Hamer", diff --git a/lib/graphql/compatibility/query_parser_spec/parse_error_specification.rb b/lib/graphql/compatibility/query_parser_spec/parse_error_specification.rb deleted file mode 100644 index 0aae96725b..0000000000 --- a/lib/graphql/compatibility/query_parser_spec/parse_error_specification.rb +++ /dev/null @@ -1,78 +0,0 @@ -module GraphQL - module Compatibility - # Include me into a minitest class - # to add assertions about parse errors - module ParseErrorSpecification - def assert_raises_parse_error(query_string) - assert_raises(GraphQL::ParseError) { - parse(query_string) - } - end - - def test_it_includes_line_and_column - err = assert_raises_parse_error(" - query getCoupons { - allCoupons: {data{id}} - } - ") - - assert_includes(err.message, '"{"') - assert_equal(3, err.line) - assert_equal(25, err.col) - end - - def test_it_rejects_unterminated_strings - assert_raises_parse_error('{ " }') - assert_raises_parse_error(%|{ "\n" }|) - end - - def test_it_rejects_unexpected_ends - assert_raises_parse_error("query { stuff { thing }") - end - - def assert_rejects_character(char) - err = assert_raises_parse_error("{ field#{char} }") - assert_includes(err.message, char.inspect, "The message includes the invalid character") - end - - def test_it_rejects_invalid_characters - assert_rejects_character(";") - assert_rejects_character("\a") - assert_rejects_character("\xef") - assert_rejects_character("\v") - assert_rejects_character("\f") - assert_rejects_character("\xa0") - end - - def test_it_rejects_bad_unicode - assert_raises_parse_error(%|{ field(arg:"\\x") }|) - assert_raises_parse_error(%|{ field(arg:"\\u1") }|) - assert_raises_parse_error(%|{ field(arg:"\\u0XX1") }|) - assert_raises_parse_error(%|{ field(arg:"\\uXXXX") }|) - assert_raises_parse_error(%|{ field(arg:"\\uFXXX") }|) - assert_raises_parse_error(%|{ field(arg:"\\uXXXF") }|) - end - - def assert_empty_document(query_string) - doc = parse(query_string) - assert_equal 0, doc.definitions.length - end - - def test_it_parses_blank_queries - assert_empty_document("") - assert_empty_document(" ") - assert_empty_document("\t \t") - end - - def test_it_restricts_on - assert_raises_parse_error("{ ...on }") - assert_raises_parse_error("fragment on on Type { field }") - end - - def test_it_rejects_null - err = assert_raises_parse_error("{ field(input: null) }") - assert_includes(err.message, "null") - end - end - end -end diff --git a/lib/graphql/compatibility/query_parser_spec.rb b/lib/graphql/compatibility/query_parser_specification.rb similarity index 95% rename from lib/graphql/compatibility/query_parser_spec.rb rename to lib/graphql/compatibility/query_parser_specification.rb index b1807cb8ee..5190052e7e 100644 --- a/lib/graphql/compatibility/query_parser_spec.rb +++ b/lib/graphql/compatibility/query_parser_specification.rb @@ -1,11 +1,11 @@ -require "graphql/compatibility/query_parser_spec/query_assertions" -require "graphql/compatibility/query_parser_spec/parse_error_specification" +require "graphql/compatibility/query_parser_specification/query_assertions" +require "graphql/compatibility/query_parser_specification/parse_error_specification" module GraphQL module Compatibility # This asserts that a given parse function turns a string into # the proper tree of {{GraphQL::Language::Nodes}}. - module QueryParserSpec + module QueryParserSpecification # @yieldparam query_string [String] A query string to parse # @yieldreturn [GraphQL::Language::Nodes::Document] # @return [Class] A test suite for this parse function diff --git a/lib/graphql/compatibility/query_parser_specification/parse_error_specification.rb b/lib/graphql/compatibility/query_parser_specification/parse_error_specification.rb new file mode 100644 index 0000000000..f2bc80ec39 --- /dev/null +++ b/lib/graphql/compatibility/query_parser_specification/parse_error_specification.rb @@ -0,0 +1,78 @@ +module GraphQL + module Compatibility + module QueryParserSpecification + module ParseErrorSpecification + def assert_raises_parse_error(query_string) + assert_raises(GraphQL::ParseError) { + parse(query_string) + } + end + + def test_it_includes_line_and_column + err = assert_raises_parse_error(" + query getCoupons { + allCoupons: {data{id}} + } + ") + + assert_includes(err.message, '"{"') + assert_equal(3, err.line) + assert_equal(27, err.col) + end + + def test_it_rejects_unterminated_strings + assert_raises_parse_error('{ " }') + assert_raises_parse_error(%|{ "\n" }|) + end + + def test_it_rejects_unexpected_ends + assert_raises_parse_error("query { stuff { thing }") + end + + def assert_rejects_character(char) + err = assert_raises_parse_error("{ field#{char} }") + assert_includes(err.message, char.inspect, "The message includes the invalid character") + end + + def test_it_rejects_invalid_characters + assert_rejects_character(";") + assert_rejects_character("\a") + assert_rejects_character("\xef") + assert_rejects_character("\v") + assert_rejects_character("\f") + assert_rejects_character("\xa0") + end + + def test_it_rejects_bad_unicode + assert_raises_parse_error(%|{ field(arg:"\\x") }|) + assert_raises_parse_error(%|{ field(arg:"\\u1") }|) + assert_raises_parse_error(%|{ field(arg:"\\u0XX1") }|) + assert_raises_parse_error(%|{ field(arg:"\\uXXXX") }|) + assert_raises_parse_error(%|{ field(arg:"\\uFXXX") }|) + assert_raises_parse_error(%|{ field(arg:"\\uXXXF") }|) + end + + def assert_empty_document(query_string) + doc = parse(query_string) + assert_equal 0, doc.definitions.length + end + + def test_it_parses_blank_queries + assert_empty_document("") + assert_empty_document(" ") + assert_empty_document("\t \t") + end + + def test_it_restricts_on + assert_raises_parse_error("{ ...on }") + assert_raises_parse_error("fragment on on Type { field }") + end + + def test_it_rejects_null + err = assert_raises_parse_error("{ field(input: null) }") + assert_includes(err.message, "null") + end + end + end + end +end diff --git a/lib/graphql/compatibility/query_parser_spec/query_assertions.rb b/lib/graphql/compatibility/query_parser_specification/query_assertions.rb similarity index 98% rename from lib/graphql/compatibility/query_parser_spec/query_assertions.rb rename to lib/graphql/compatibility/query_parser_specification/query_assertions.rb index 3e3849524a..5f63f0222f 100644 --- a/lib/graphql/compatibility/query_parser_spec/query_assertions.rb +++ b/lib/graphql/compatibility/query_parser_specification/query_assertions.rb @@ -1,6 +1,6 @@ module GraphQL module Compatibility - module QueryParserSpec + module QueryParserSpecification module QueryAssertions def assert_valid_query(query) assert query.is_a?(GraphQL::Language::Nodes::OperationDefinition) diff --git a/lib/graphql/compatibility/schema_parser_spec.rb b/lib/graphql/compatibility/schema_parser_specification.rb similarity index 70% rename from lib/graphql/compatibility/schema_parser_spec.rb rename to lib/graphql/compatibility/schema_parser_specification.rb index 20b91b2ed5..81b00f0f0b 100644 --- a/lib/graphql/compatibility/schema_parser_spec.rb +++ b/lib/graphql/compatibility/schema_parser_specification.rb @@ -2,7 +2,7 @@ module GraphQL module Compatibility # This asserts that a given parse function turns a string into # the proper tree of {{GraphQL::Language::Nodes}}. - module SchemaParserSpec + module SchemaParserSpecification # @yieldparam query_string [String] A query string to parse # @yieldreturn [GraphQL::Language::Nodes::Document] # @return [Class] A test suite for this parse function @@ -14,7 +14,44 @@ def parse(query_string) @@parse_fn.call(query_string) end + def test_it_parses_object_types + document = parse(' + type Comment implements Node @deprecated(reason: "No longer supported") { + id: ID! + } + ') + + type = document.definitions.first + assert_equal GraphQL::Language::Nodes::ObjectTypeDefinition, type.class + assert_equal 'Comment', type.name + assert_equal ['Node'], type.interfaces.map(&:name) + assert_equal ['id'], type.fields.map(&:name) + assert_equal [], type.fields[0].arguments + assert_equal 'ID', type.fields[0].type.of_type.name + assert_equal 1, type.directives.length + + deprecated_directive = type.directives[0] + assert_equal 'deprecated', deprecated_directive.name + assert_equal 'reason', deprecated_directive.arguments[0].name + assert_equal 'No longer supported', deprecated_directive.arguments[0].value + end + def test_it_parses_schema_definition + document = parse(' + schema { + query: QueryRoot + mutation: MutationRoot + subscription: SubscriptionRoot + } + ') + + schema = document.definitions.first + assert_equal 'QueryRoot', schema.query + assert_equal 'MutationRoot', schema.mutation + assert_equal 'SubscriptionRoot', schema.subscription + end + + def test_it_parses_whole_definition_with_descriptions document = parse(SCHEMA_DEFINITION_STRING) assert_equal 6, document.definitions.size diff --git a/spec/graphql/compatibility/execution_spec_spec.rb b/spec/graphql/compatibility/execution_spec_spec.rb deleted file mode 100644 index 96f88e2be9..0000000000 --- a/spec/graphql/compatibility/execution_spec_spec.rb +++ /dev/null @@ -1,3 +0,0 @@ -require "spec_helper" - -SerialExecutionSuite = GraphQL::Compatibility::ExecutionSpec.build_suite(GraphQL::Query::SerialExecution) diff --git a/spec/graphql/compatibility/execution_specification_spec.rb b/spec/graphql/compatibility/execution_specification_spec.rb new file mode 100644 index 0000000000..5b8ae3c6ad --- /dev/null +++ b/spec/graphql/compatibility/execution_specification_spec.rb @@ -0,0 +1,3 @@ +require "spec_helper" + +SerialExecutionSuite = GraphQL::Compatibility::ExecutionSpecification.build_suite(GraphQL::Query::SerialExecution) diff --git a/spec/graphql/compatibility/query_parser_spec_spec.rb b/spec/graphql/compatibility/query_parser_spec_spec.rb deleted file mode 100644 index a7d7920598..0000000000 --- a/spec/graphql/compatibility/query_parser_spec_spec.rb +++ /dev/null @@ -1,5 +0,0 @@ -require "spec_helper" - -BuiltInParserSuite = GraphQL::Compatibility::QueryParserSpec.build_suite do |query_string| - GraphQL::Language::Parser.parse(query_string) -end diff --git a/spec/graphql/compatibility/query_parser_specification_spec.rb b/spec/graphql/compatibility/query_parser_specification_spec.rb new file mode 100644 index 0000000000..6734065417 --- /dev/null +++ b/spec/graphql/compatibility/query_parser_specification_spec.rb @@ -0,0 +1,5 @@ +require "spec_helper" + +BuiltInParserSuite = GraphQL::Compatibility::QueryParserSpecification.build_suite do |query_string| + GraphQL::Language::Parser.parse(query_string) +end diff --git a/spec/graphql/compatibility/schema_parser_spec_spec.rb b/spec/graphql/compatibility/schema_parser_spec_spec.rb deleted file mode 100644 index ee0b6779ce..0000000000 --- a/spec/graphql/compatibility/schema_parser_spec_spec.rb +++ /dev/null @@ -1,5 +0,0 @@ -require "spec_helper" - -BuiltInParserSuite = GraphQL::Compatibility::SchemaParserSpec.build_suite do |query_string| - GraphQL::Language::Parser.parse(query_string) -end diff --git a/spec/graphql/compatibility/schema_parser_specification_spec.rb b/spec/graphql/compatibility/schema_parser_specification_spec.rb new file mode 100644 index 0000000000..90d79d545f --- /dev/null +++ b/spec/graphql/compatibility/schema_parser_specification_spec.rb @@ -0,0 +1,5 @@ +require "spec_helper" + +BuiltInParserSuite = GraphQL::Compatibility::SchemaParserSpecification.build_suite do |query_string| + GraphQL::Language::Parser.parse(query_string) +end From 66bb7e65bd5fec694342b2a83308068e814517d3 Mon Sep 17 00:00:00 2001 From: Robert Mosolgo Date: Sat, 29 Oct 2016 21:57:05 -0400 Subject: [PATCH 4/9] feat(Compatibility) finish SchemaParserSpecification --- .../schema_parser_specification.rb | 100 +++++ lib/graphql/language/parser_tests.rb | 369 ------------------ .../query_parser_specification_spec.rb | 2 +- .../schema_parser_specification_spec.rb | 2 +- spec/graphql/language/parser_spec.rb | 11 +- 5 files changed, 111 insertions(+), 373 deletions(-) delete mode 100644 lib/graphql/language/parser_tests.rb diff --git a/lib/graphql/compatibility/schema_parser_specification.rb b/lib/graphql/compatibility/schema_parser_specification.rb index 81b00f0f0b..2c4013552a 100644 --- a/lib/graphql/compatibility/schema_parser_specification.rb +++ b/lib/graphql/compatibility/schema_parser_specification.rb @@ -16,6 +16,8 @@ def parse(query_string) def test_it_parses_object_types document = parse(' + # This is what + # somebody said about something type Comment implements Node @deprecated(reason: "No longer supported") { id: ID! } @@ -24,6 +26,7 @@ def test_it_parses_object_types type = document.definitions.first assert_equal GraphQL::Language::Nodes::ObjectTypeDefinition, type.class assert_equal 'Comment', type.name + assert_equal "This is what\nsomebody said about something", type.description assert_equal ['Node'], type.interfaces.map(&:name) assert_equal ['id'], type.fields.map(&:name) assert_equal [], type.fields[0].arguments @@ -36,6 +39,103 @@ def test_it_parses_object_types assert_equal 'No longer supported', deprecated_directive.arguments[0].value end + def test_it_parses_scalars + document = parse('scalar DateTime') + + type = document.definitions.first + assert_equal GraphQL::Language::Nodes::ScalarTypeDefinition, type.class + assert_equal 'DateTime', type.name + end + + def test_it_parses_enum_types + document = parse(' + enum DogCommand { + # Good dog + SIT + DOWN @deprecated(reason: "No longer supported") + HEEL + } + ') + + type = document.definitions.first + assert_equal GraphQL::Language::Nodes::EnumTypeDefinition, type.class + assert_equal 'DogCommand', type.name + assert_equal 3, type.values.length + + assert_equal 'SIT', type.values[0].name + assert_equal [], type.values[0].directives + assert_equal "Good dog", type.values[0].description + + assert_equal 'DOWN', type.values[1].name + assert_equal 1, type.values[1].directives.length + deprecated_directive = type.values[1].directives[0] + assert_equal 'deprecated', deprecated_directive.name + assert_equal 'reason', deprecated_directive.arguments[0].name + assert_equal 'No longer supported', deprecated_directive.arguments[0].value + + assert_equal 'HEEL', type.values[2].name + assert_equal [], type.values[2].directives + end + + def test_it_parses_input_types + document = parse(' + input EmptyMutationInput { + clientMutationId: String + } + ') + + type = document.definitions.first + assert_equal GraphQL::Language::Nodes::InputObjectTypeDefinition, type.class + assert_equal 'EmptyMutationInput', type.name + assert_equal ['clientMutationId'], type.fields.map(&:name) + assert_equal 'String', type.fields[0].type.name + assert_equal nil, type.fields[0].default_value + end + + def test_it_parses_directives + document = parse(' + directive @include(if: Boolean!) + on FIELD + | FRAGMENT_SPREAD + | INLINE_FRAGMENT + ') + + type = document.definitions.first + assert_equal GraphQL::Language::Nodes::DirectiveDefinition, type.class + assert_equal 'include', type.name + + assert_equal 1, type.arguments.length + assert_equal 'if', type.arguments[0].name + assert_equal 'Boolean', type.arguments[0].type.of_type.name + + assert_equal ['FIELD', 'FRAGMENT_SPREAD', 'INLINE_FRAGMENT'], type.locations + end + + def test_it_parses_field_arguments + document = parse(' + type Mutation { + post( + id: ID! @deprecated(reason: "Not used"), + # This is what goes in the post + data: String + ): Post + } + ') + + field = document.definitions.first.fields.first + assert_equal ['id', 'data'], field.arguments.map(&:name) + id_arg = field.arguments[0] + + deprecated_directive = id_arg.directives[0] + assert_equal 'deprecated', deprecated_directive.name + assert_equal 'reason', deprecated_directive.arguments[0].name + assert_equal 'Not used', deprecated_directive.arguments[0].value + + data_arg = field.arguments[1] + assert_equal "data", data_arg.name + assert_equal "This is what goes in the post", data_arg.description + end + def test_it_parses_schema_definition document = parse(' schema { diff --git a/lib/graphql/language/parser_tests.rb b/lib/graphql/language/parser_tests.rb deleted file mode 100644 index 3d06007617..0000000000 --- a/lib/graphql/language/parser_tests.rb +++ /dev/null @@ -1,369 +0,0 @@ -module GraphQL - module Language - # If you create your own GraphQL parser, can verify it using these tests. - # - # @example Include these tests in a Minitest suite - # require 'graphql/language/parser_tests' - # - # describe MyParser do - # include GraphQL::Language::ParserTests - # subject { MyParser } - # end - module ParserTests - def self.included(test) - test.send(:describe, "Parser Tests") do - let(:document) { subject.parse(query_string) } - - describe ".parse" do - describe "schema with comments" do - let(:query_string) {%| - # Schema at beginning of file - - schema { - query: Hello - } - - # Comment between two definitions are omitted - - # This is a directive - directive @foo( - # It has an argument - arg: Int - ) on FIELD - - # Multiline comment - # - # With an enum - enum Color { - RED - - # Not a creative color - GREEN - BLUE - } - - #Comment without preceding space - type Hello { - # And a field to boot - str: String - } - - # Comment for input object types - input Car { - # Color of the car - color: String! - } - - # Comment for interface definitions - interface Vehicle { - # Amount of wheels - wheels: Int! - } - - # Comment at the end of schema - |} - - it "parses successfully" do - document = subject.parse(query_string) - - assert_equal 6, document.definitions.size - - schema_definition = document.definitions.shift - assert_equal GraphQL::Language::Nodes::SchemaDefinition, schema_definition.class - - directive_definition = document.definitions.shift - assert_equal GraphQL::Language::Nodes::DirectiveDefinition, directive_definition.class - assert_equal 'This is a directive', directive_definition.description - - enum_type_definition = document.definitions.shift - assert_equal GraphQL::Language::Nodes::EnumTypeDefinition, enum_type_definition.class - assert_equal "Multiline comment\n\nWith an enum", enum_type_definition.description - - assert_nil enum_type_definition.values[0].description - assert_equal 'Not a creative color', enum_type_definition.values[1].description - - object_type_definition = document.definitions.shift - assert_equal GraphQL::Language::Nodes::ObjectTypeDefinition, object_type_definition.class - assert_equal 'Comment without preceding space', object_type_definition.description - assert_equal 'And a field to boot', object_type_definition.fields[0].description - - input_object_type_definition = document.definitions.shift - assert_equal GraphQL::Language::Nodes::InputObjectTypeDefinition, input_object_type_definition.class - assert_equal 'Comment for input object types', input_object_type_definition.description - assert_equal 'Color of the car', input_object_type_definition.fields[0].description - - interface_type_definition = document.definitions.shift - assert_equal GraphQL::Language::Nodes::InterfaceTypeDefinition, interface_type_definition.class - assert_equal 'Comment for interface definitions', interface_type_definition.description - assert_equal 'Amount of wheels', interface_type_definition.fields[0].description - end - end - - describe "schema" do - it "parses the test schema" do - schema = DummySchema - schema_string = GraphQL::Schema::Printer.print_schema(schema) - - document = subject.parse(schema_string) - - assert_equal schema_string, document.to_query_string - end - - it "parses mimal schema definition" do - document = subject.parse('schema { query: QueryRoot }') - - schema = document.definitions.first - assert_equal 'QueryRoot', schema.query - assert_equal nil, schema.mutation - assert_equal nil, schema.subscription - end - - it "parses full schema definitions" do - document = subject.parse(' - schema { - query: QueryRoot - mutation: MutationRoot - subscription: SubscriptionRoot - } - ') - - schema = document.definitions.first - assert_equal 'QueryRoot', schema.query - assert_equal 'MutationRoot', schema.mutation - assert_equal 'SubscriptionRoot', schema.subscription - end - - it "parses object types" do - document = subject.parse(' - type Comment implements Node { - id: ID! - } - ') - - type = document.definitions.first - assert_equal GraphQL::Language::Nodes::ObjectTypeDefinition, type.class - assert_equal 'Comment', type.name - assert_equal ['Node'], type.interfaces.map(&:name) - assert_equal ['id'], type.fields.map(&:name) - assert_equal [], type.fields[0].arguments - assert_equal 'ID', type.fields[0].type.of_type.name - end - - it "parses object types with directives" do - document = subject.parse(' - type Comment implements Node @deprecated(reason: "No longer supported") { - id: ID! - } - ') - - type = document.definitions.first - assert_equal GraphQL::Language::Nodes::ObjectTypeDefinition, type.class - assert_equal 'Comment', type.name - assert_equal ['Node'], type.interfaces.map(&:name) - assert_equal ['id'], type.fields.map(&:name) - assert_equal [], type.fields[0].arguments - assert_equal 'ID', type.fields[0].type.of_type.name - assert_equal 1, type.directives.length - - deprecated_directive = type.directives[0] - assert_equal 'deprecated', deprecated_directive.name - assert_equal 'reason', deprecated_directive.arguments[0].name - assert_equal 'No longer supported', deprecated_directive.arguments[0].value - end - - it "parses field arguments" do - document = subject.parse(' - type Mutation { - post(id: ID!, data: PostData = { message: "First!1!", type: BLOG, tags: ["Test", "Annoying"] }): Post - } - ') - - field = document.definitions.first.fields.first - assert_equal ['id', 'data'], field.arguments.map(&:name) - data_arg = field.arguments[1] - assert_equal 'PostData', data_arg.type.name - assert_equal ['message', 'type', 'tags'], data_arg.default_value.arguments.map(&:name) - tags_arg = data_arg.default_value.arguments[2] - assert_equal ['Test', 'Annoying'], tags_arg.value - end - - it "parses field arguments with directives" do - document = subject.parse(' - type Mutation { - post(id: ID! @deprecated(reason: "No longer supported"), data: String): Post - } - ') - - field = document.definitions.first.fields.first - assert_equal ['id', 'data'], field.arguments.map(&:name) - id_arg = field.arguments[0] - - deprecated_directive = id_arg.directives[0] - assert_equal 'deprecated', deprecated_directive.name - assert_equal 'reason', deprecated_directive.arguments[0].name - assert_equal 'No longer supported', deprecated_directive.arguments[0].value - end - - it "parses directive definition" do - document = subject.parse(' - directive @include(if: Boolean!) - on FIELD - | FRAGMENT_SPREAD - | INLINE_FRAGMENT - ') - - type = document.definitions.first - assert_equal GraphQL::Language::Nodes::DirectiveDefinition, type.class - assert_equal 'include', type.name - - assert_equal 1, type.arguments.length - assert_equal 'if', type.arguments[0].name - assert_equal 'Boolean', type.arguments[0].type.of_type.name - - assert_equal ['FIELD', 'FRAGMENT_SPREAD', 'INLINE_FRAGMENT'], type.locations - end - - it "parses scalar types" do - document = subject.parse('scalar DateTime') - - type = document.definitions.first - assert_equal GraphQL::Language::Nodes::ScalarTypeDefinition, type.class - assert_equal 'DateTime', type.name - end - - it "parses scalar types with directives" do - document = subject.parse('scalar DateTime @deprecated(reason: "No longer supported")') - - type = document.definitions.first - assert_equal GraphQL::Language::Nodes::ScalarTypeDefinition, type.class - assert_equal 'DateTime', type.name - assert_equal 1, type.directives.length - - deprecated_directive = type.directives[0] - assert_equal 'deprecated', deprecated_directive.name - assert_equal 'reason', deprecated_directive.arguments[0].name - assert_equal 'No longer supported', deprecated_directive.arguments[0].value - end - - it "parses interface types" do - document = subject.parse(' - interface Node { - id: ID! - } - ') - - type = document.definitions.first - assert_equal GraphQL::Language::Nodes::InterfaceTypeDefinition, type.class - assert_equal 'Node', type.name - assert_equal ['id'], type.fields.map(&:name) - assert_equal [], type.fields[0].arguments - assert_equal 'ID', type.fields[0].type.of_type.name - end - - it "parses interface types with directives" do - document = subject.parse(' - interface Node @deprecated(reason: "No longer supported") { - id: ID! - } - ') - - type = document.definitions.first - assert_equal GraphQL::Language::Nodes::InterfaceTypeDefinition, type.class - assert_equal 'Node', type.name - assert_equal 1, type.directives.length - - deprecated_directive = type.directives[0] - assert_equal 'deprecated', deprecated_directive.name - assert_equal 'reason', deprecated_directive.arguments[0].name - assert_equal 'No longer supported', deprecated_directive.arguments[0].value - end - - it "parses enum types" do - document = subject.parse(' - enum DogCommand { - SIT - DOWN @deprecated(reason: "No longer supported") - HEEL - } - ') - - type = document.definitions.first - assert_equal GraphQL::Language::Nodes::EnumTypeDefinition, type.class - assert_equal 'DogCommand', type.name - assert_equal 3, type.values.length - - assert_equal 'SIT', type.values[0].name - assert_equal [], type.values[0].directives - - assert_equal 'DOWN', type.values[1].name - assert_equal 1, type.values[1].directives.length - deprecated_directive = type.values[1].directives[0] - assert_equal 'deprecated', deprecated_directive.name - assert_equal 'reason', deprecated_directive.arguments[0].name - assert_equal 'No longer supported', deprecated_directive.arguments[0].value - - assert_equal 'HEEL', type.values[2].name - assert_equal [], type.values[2].directives - end - - it "parses enum types with directives" do - document = subject.parse(' - enum DogCommand @deprecated(reason: "No longer supported") { - SIT - } - ') - - type = document.definitions.first - assert_equal GraphQL::Language::Nodes::EnumTypeDefinition, type.class - assert_equal 'DogCommand', type.name - assert_equal 1, type.directives.length - - deprecated_directive = type.directives[0] - assert_equal 'deprecated', deprecated_directive.name - assert_equal 'reason', deprecated_directive.arguments[0].name - assert_equal 'No longer supported', deprecated_directive.arguments[0].value - end - - it "parses input object types" do - document = subject.parse(' - input EmptyMutationInput { - clientMutationId: String - } - ') - - type = document.definitions.first - assert_equal GraphQL::Language::Nodes::InputObjectTypeDefinition, type.class - assert_equal 'EmptyMutationInput', type.name - assert_equal ['clientMutationId'], type.fields.map(&:name) - assert_equal 'String', type.fields[0].type.name - assert_equal nil, type.fields[0].default_value - end - - it "parses input object types with directives" do - document = subject.parse(' - input EmptyMutationInput @deprecated(reason: "No longer supported") { - clientMutationId: String - } - ') - - type = document.definitions.first - assert_equal GraphQL::Language::Nodes::InputObjectTypeDefinition, type.class - assert_equal 'EmptyMutationInput', type.name - assert_equal ['clientMutationId'], type.fields.map(&:name) - assert_equal 'String', type.fields[0].type.name - assert_equal nil, type.fields[0].default_value - assert_equal 1, type.directives.length - - deprecated_directive = type.directives[0] - assert_equal 'deprecated', deprecated_directive.name - assert_equal 'reason', deprecated_directive.arguments[0].name - assert_equal 'No longer supported', deprecated_directive.arguments[0].value - - end - end - end - end - end - end - end -end diff --git a/spec/graphql/compatibility/query_parser_specification_spec.rb b/spec/graphql/compatibility/query_parser_specification_spec.rb index 6734065417..1fdd4a486d 100644 --- a/spec/graphql/compatibility/query_parser_specification_spec.rb +++ b/spec/graphql/compatibility/query_parser_specification_spec.rb @@ -1,5 +1,5 @@ require "spec_helper" -BuiltInParserSuite = GraphQL::Compatibility::QueryParserSpecification.build_suite do |query_string| +BuiltInQueryParserSuite = GraphQL::Compatibility::QueryParserSpecification.build_suite do |query_string| GraphQL::Language::Parser.parse(query_string) end diff --git a/spec/graphql/compatibility/schema_parser_specification_spec.rb b/spec/graphql/compatibility/schema_parser_specification_spec.rb index 90d79d545f..2a0c660b63 100644 --- a/spec/graphql/compatibility/schema_parser_specification_spec.rb +++ b/spec/graphql/compatibility/schema_parser_specification_spec.rb @@ -1,5 +1,5 @@ require "spec_helper" -BuiltInParserSuite = GraphQL::Compatibility::SchemaParserSpecification.build_suite do |query_string| +BuiltInSchemaParserSuite = GraphQL::Compatibility::SchemaParserSpecification.build_suite do |query_string| GraphQL::Language::Parser.parse(query_string) end diff --git a/spec/graphql/language/parser_spec.rb b/spec/graphql/language/parser_spec.rb index 632454ec22..585b535b00 100644 --- a/spec/graphql/language/parser_spec.rb +++ b/spec/graphql/language/parser_spec.rb @@ -1,8 +1,6 @@ require "spec_helper" -require 'graphql/language/parser_tests' describe GraphQL::Language::Parser do - include GraphQL::Language::ParserTests subject { GraphQL::Language::Parser } describe "anonymous fragment extension" do @@ -30,6 +28,15 @@ assert_equal [2, 7], fragment.position end end + + it "parses the test schema" do + schema = DummySchema + schema_string = GraphQL::Schema::Printer.print_schema(schema) + + document = subject.parse(schema_string) + + assert_equal schema_string, document.to_query_string + end end end end From e1829e4b17ef7b58724491d5df4add4c575a2b5b Mon Sep 17 00:00:00 2001 From: Robert Mosolgo Date: Sat, 29 Oct 2016 22:26:41 -0400 Subject: [PATCH 5/9] fix(QueryParserSpecification) update to match ligraphqlparser --- .../query_parser_specification.rb | 19 ++----- .../parse_error_specification.rb | 7 ++- spec/graphql/language/parser_spec.rb | 53 ++++++++++--------- 3 files changed, 38 insertions(+), 41 deletions(-) diff --git a/lib/graphql/compatibility/query_parser_specification.rb b/lib/graphql/compatibility/query_parser_specification.rb index 5190052e7e..cc3ad2e1f7 100644 --- a/lib/graphql/compatibility/query_parser_specification.rb +++ b/lib/graphql/compatibility/query_parser_specification.rb @@ -37,19 +37,6 @@ def test_it_parses_queries assert_valid_typeless_inline_fragment(query.selections[3]) end - def test_it_parses_empty_arguments - strings = [ - "{ field { } }", - "{ field() }", - ] - strings.each do |query_str| - doc = parse(query_str) - field = doc.definitions.first.selections.first - assert_equal 0, field.arguments.length - assert_equal 0, field.selections.length - end - end - def test_it_parses_unnamed_queries document = parse("{ name, age, height }") operation = document.definitions.first @@ -59,6 +46,10 @@ def test_it_parses_unnamed_queries assert_equal 3, operation.selections.length end + def test_it_parses_the_introspection_query + parse(GraphQL::Introspection::INTROSPECTION_QUERY) + end + def test_it_parses_inputs query_string = %| { @@ -66,7 +57,7 @@ def test_it_parses_inputs int: 3, float: 4.7e-24, bool: false, - string: "β˜€οΈŽπŸ†\\n escaped \\" unicode \\u00b6 /", + string: "β˜€οΈŽπŸ†\\n escaped \\" unicode \u00b6 /", enum: ENUM_NAME, array: [7, 8, 9] object: {a: [1,2,3], b: {c: "4"}} diff --git a/lib/graphql/compatibility/query_parser_specification/parse_error_specification.rb b/lib/graphql/compatibility/query_parser_specification/parse_error_specification.rb index f2bc80ec39..bba29edfbc 100644 --- a/lib/graphql/compatibility/query_parser_specification/parse_error_specification.rb +++ b/lib/graphql/compatibility/query_parser_specification/parse_error_specification.rb @@ -15,7 +15,7 @@ def test_it_includes_line_and_column } ") - assert_includes(err.message, '"{"') + assert_includes(err.message, '{') assert_equal(3, err.line) assert_equal(27, err.col) end @@ -31,7 +31,10 @@ def test_it_rejects_unexpected_ends def assert_rejects_character(char) err = assert_raises_parse_error("{ field#{char} }") - assert_includes(err.message, char.inspect, "The message includes the invalid character") + expected_char = char.inspect.gsub('"', '').downcase + msg_downcase = err.message.downcase + # Case-insensitive for UTF-8 printing + assert_includes(msg_downcase, expected_char, "The message includes the invalid character") end def test_it_rejects_invalid_characters diff --git a/spec/graphql/language/parser_spec.rb b/spec/graphql/language/parser_spec.rb index 585b535b00..27bc35d865 100644 --- a/spec/graphql/language/parser_spec.rb +++ b/spec/graphql/language/parser_spec.rb @@ -11,32 +11,35 @@ } |} - describe ".parse" do - it "parses queries" do - assert document - end - - describe "visited nodes" do - let(:fragment) { document.definitions.first } - - it "creates an anonymous fragment definition" do - assert fragment.is_a?(GraphQL::Language::Nodes::FragmentDefinition) - assert_equal nil, fragment.name - assert_equal 1, fragment.selections.length - assert_equal "NestedType", fragment.type.name - assert_equal 1, fragment.directives.length - assert_equal [2, 7], fragment.position - end - end - - it "parses the test schema" do - schema = DummySchema - schema_string = GraphQL::Schema::Printer.print_schema(schema) - - document = subject.parse(schema_string) + let(:fragment) { document.definitions.first } + + it "creates an anonymous fragment definition" do + assert fragment.is_a?(GraphQL::Language::Nodes::FragmentDefinition) + assert_equal nil, fragment.name + assert_equal 1, fragment.selections.length + assert_equal "NestedType", fragment.type.name + assert_equal 1, fragment.directives.length + assert_equal [2, 7], fragment.position + end + end - assert_equal schema_string, document.to_query_string - end + it "parses empty arguments" do + strings = [ + "{ field { inner } }", + "{ field() { inner }}", + ] + strings.each do |query_str| + doc = subject.parse(query_str) + field = doc.definitions.first.selections.first + assert_equal 0, field.arguments.length + assert_equal 1, field.selections.length end end + + it "parses the test schema" do + schema = DummySchema + schema_string = GraphQL::Schema::Printer.print_schema(schema) + document = subject.parse(schema_string) + assert_equal schema_string, document.to_query_string + end end From 3fc54dbeec45e26c798c479af0a704e4aa2f0e7a Mon Sep 17 00:00:00 2001 From: Robert Mosolgo Date: Sun, 30 Oct 2016 19:30:09 -0400 Subject: [PATCH 6/9] fix(Lexer) don't replace backslash-u with unicode --- lib/graphql/compatibility/query_parser_specification.rb | 4 ++-- lib/graphql/language/lexer.rl | 4 ---- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/lib/graphql/compatibility/query_parser_specification.rb b/lib/graphql/compatibility/query_parser_specification.rb index cc3ad2e1f7..ef334516f1 100644 --- a/lib/graphql/compatibility/query_parser_specification.rb +++ b/lib/graphql/compatibility/query_parser_specification.rb @@ -57,7 +57,7 @@ def test_it_parses_inputs int: 3, float: 4.7e-24, bool: false, - string: "β˜€οΈŽπŸ†\\n escaped \\" unicode \u00b6 /", + string: "β˜€οΈŽπŸ†\\n \\" \u00b6 /", enum: ENUM_NAME, array: [7, 8, 9] object: {a: [1,2,3], b: {c: "4"}} @@ -71,7 +71,7 @@ def test_it_parses_inputs assert_equal 3, inputs[0].value, "Integers" assert_equal 0.47e-23, inputs[1].value, "Floats" assert_equal false, inputs[2].value, "Booleans" - assert_equal %|β˜€οΈŽπŸ†\n escaped " unicode ΒΆ /|, inputs[3].value, "Strings" + assert_equal %|β˜€οΈŽπŸ†\n " ΒΆ /|, inputs[3].value, "Strings" assert_instance_of GraphQL::Language::Nodes::Enum, inputs[4].value assert_equal "ENUM_NAME", inputs[4].value.name, "Enums" assert_equal [7,8,9], inputs[5].value, "Lists" diff --git a/lib/graphql/language/lexer.rl b/lib/graphql/language/lexer.rl index 9128b860ba..af36c2f54d 100644 --- a/lib/graphql/language/lexer.rl +++ b/lib/graphql/language/lexer.rl @@ -109,7 +109,6 @@ module GraphQL # To avoid allocating more strings, this modifies the string passed into it def self.replace_escaped_characters_in_place(raw_string) raw_string.gsub!(ESCAPES, ESCAPES_REPLACE) - raw_string.gsub!(UTF_8, &UTF_8_REPLACE) nil end @@ -178,9 +177,6 @@ module GraphQL "\\t" => "\t", } - UTF_8 = /\\u[\dAa-f]{4}/i - UTF_8_REPLACE = ->(m) { [m[-4..-1].to_i(16)].pack('U'.freeze) } - def self.emit_string(ts, te, meta) value = meta[:data][ts...te].pack("c*").force_encoding("UTF-8") if value =~ /\\u|\\./ && value !~ ESCAPES From bf7b9b6750a5ce90cfd681bf12938c9251e77338 Mon Sep 17 00:00:00 2001 From: Robert Mosolgo Date: Sun, 30 Oct 2016 21:39:10 -0400 Subject: [PATCH 7/9] Run the introspection query for good measure --- lib/graphql/compatibility/execution_specification.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/graphql/compatibility/execution_specification.rb b/lib/graphql/compatibility/execution_specification.rb index 6431cdc774..b78eee2f83 100644 --- a/lib/graphql/compatibility/execution_specification.rb +++ b/lib/graphql/compatibility/execution_specification.rb @@ -329,6 +329,10 @@ def test_it_provides_nodes_to_resolve assert_equal "SNCC", res["data"]["organization"]["name"] assert_equal [true, true, false], res["data"]["organization"]["nodePresence"] end + + def test_it_runs_the_introspection_query + execute_query(GraphQL::Introspection::INTROSPECTION_QUERY) + end end end end From 0bad071e7e228f4ffd764a010d63be0cb0fa352c Mon Sep 17 00:00:00 2001 From: Robert Mosolgo Date: Mon, 31 Oct 2016 09:04:26 -0400 Subject: [PATCH 8/9] feat(Compatibility) scaffold more execution tests --- .../compatibility/execution_specification.rb | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/lib/graphql/compatibility/execution_specification.rb b/lib/graphql/compatibility/execution_specification.rb index b78eee2f83..d982ad1725 100644 --- a/lib/graphql/compatibility/execution_specification.rb +++ b/lib/graphql/compatibility/execution_specification.rb @@ -231,7 +231,7 @@ def test_it_propagates_nulls_to_field success = res["data"]["success"] assert_equal nil, failure, "It propagates nulls to the next nullable field" - assert_equal "SNCC", success["name"], "It serves the same object if no invalid null is encountered" + assert_equal({"name" => "SNCC"}, success, "It serves the same object if no invalid null is encountered") assert_equal 1, res["errors"].length , "It returns an error for the invalid null" end @@ -333,6 +333,22 @@ def test_it_provides_nodes_to_resolve def test_it_runs_the_introspection_query execute_query(GraphQL::Introspection::INTROSPECTION_QUERY) end + + def test_it_propagates_deeply_nested_nulls + skip + end + + def test_it_doesnt_add_errors_for_invalid_nulls_from_execution_errors + skip + end + + def test_it_passes_invalid_nulls_to_schema + skip + end + + def test_it_includes_path_and_index_in_error_path + skip + end end end end From 33370e78ec2e511c1160c7508b55498b690ae2a6 Mon Sep 17 00:00:00 2001 From: Robert Mosolgo Date: Mon, 31 Oct 2016 22:40:24 -0400 Subject: [PATCH 9/9] feat(Compatibilty) more null handling specifications --- .../compatibility/execution_specification.rb | 106 ++++++++++++++---- 1 file changed, 82 insertions(+), 24 deletions(-) diff --git a/lib/graphql/compatibility/execution_specification.rb b/lib/graphql/compatibility/execution_specification.rb index d982ad1725..ab16db1d3c 100644 --- a/lib/graphql/compatibility/execution_specification.rb +++ b/lib/graphql/compatibility/execution_specification.rb @@ -99,6 +99,11 @@ def self.build_schema(execution_strategy) obj.organization_ids.map { |id| DATA[id] } } end + field :first_organization, !organization_type do + resolve ->(obj, args, ctx) { + DATA[obj.organization_ids.first] + } + end end organization_type = GraphQL::ObjectType.define do @@ -107,7 +112,7 @@ def self.build_schema(execution_strategy) field :name, !types.String field :leader, !person_type do resolve ->(obj, args, ctx) { - DATA[obj.leader_id] + DATA[obj.leader_id] || (ctx[:return_error] ? ExecutionError.new("Error on Nullable") : nil) } end field :returnedError, types.String do @@ -152,6 +157,12 @@ def self.build_schema(execution_strategy) args[:id].start_with?("2") && obj[args[:id]] } end + + field :organizations, types[organization_type] do + resolve ->(obj, args, ctx) { + [obj["2001"], obj["2002"]] + } + end end GraphQL::Schema.define do @@ -260,6 +271,10 @@ def test_it_exposes_raised_and_returned_user_execution_errors returnedError raisedError } + organizations { + returnedError + raisedError + } } | @@ -267,19 +282,42 @@ def test_it_exposes_raised_and_returned_user_execution_errors assert_equal "SNCC", res["data"]["organization"]["name"], "It runs the rest of the query" - expected_returned_error = { - "message"=>"This error was returned", - "locations"=>[{"line"=>5, "column"=>19}], - "path"=>["organization", "returnedError"] - } - assert_includes res["errors"], expected_returned_error, "It turns returned errors into response errors" - - expected_raised_error = { - "message"=>"This error was raised", - "locations"=>[{"line"=>6, "column"=>19}], - "path"=>["organization", "raisedError"] - } - assert_includes res["errors"], expected_raised_error, "It turns raised errors into response errors" + expected_errors = [ + { + "message"=>"This error was returned", + "locations"=>[{"line"=>5, "column"=>19}], + "path"=>["organization", "returnedError"] + }, + { + "message"=>"This error was raised", + "locations"=>[{"line"=>6, "column"=>19}], + "path"=>["organization", "raisedError"] + }, + { + "message"=>"This error was raised", + "locations"=>[{"line"=>10, "column"=>19}], + "path"=>["organizations", 0, "raisedError"] + }, + { + "message"=>"This error was raised", + "locations"=>[{"line"=>10, "column"=>19}], + "path"=>["organizations", 1, "raisedError"] + }, + { + "message"=>"This error was returned", + "locations"=>[{"line"=>9, "column"=>19}], + "path"=>["organizations", 0, "returnedError"] + }, + { + "message"=>"This error was returned", + "locations"=>[{"line"=>9, "column"=>19}], + "path"=>["organizations", 1, "returnedError"] + }, + ] + + expected_errors.each do |expected_err| + assert_includes res["errors"], expected_err + end end def test_it_applies_masking @@ -335,19 +373,39 @@ def test_it_runs_the_introspection_query end def test_it_propagates_deeply_nested_nulls - skip + query_string = %| + { + node(id: "1001") { + ... on Person { + name + first_organization { + leader { + name + } + } + } + } + } + | + res = execute_query(query_string) + assert_equal nil, res["data"]["node"] + assert_equal 1, res["errors"].length end def test_it_doesnt_add_errors_for_invalid_nulls_from_execution_errors - skip - end - - def test_it_passes_invalid_nulls_to_schema - skip - end - - def test_it_includes_path_and_index_in_error_path - skip + query_string = %| + query getOrg($id: ID = "2001"){ + failure: node(id: $id) { + ... on Organization { + name + leader { name } + } + } + } + | + res = execute_query(query_string, context: {return_error: true}) + error_messages = res["errors"].map { |e| e["message"] } + assert_equal ["Error on Nullable"], error_messages end end end