Skip to content

Commit ddb7501

Browse files
committed
feat(Compatibility) introduce QueryParserSpec
1 parent 92d8df8 commit ddb7501

File tree

11 files changed

+399
-456
lines changed

11 files changed

+399
-456
lines changed

Rakefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ task(default: [:test, :rubocop])
2020

2121
desc "Use Racc & Ragel to regenerate parser.rb & lexer.rb from configuration files"
2222
task :build_parser do
23-
`rm lib/graphql/language/parser.rb lib/graphql/language/lexer.rb `
23+
`rm -f lib/graphql/language/parser.rb lib/graphql/language/lexer.rb `
2424
`racc lib/graphql/language/parser.y -o lib/graphql/language/parser.rb`
2525
`ragel -R lib/graphql/language/lexer.rl`
2626
end

lib/graphql.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,3 +82,4 @@ def self.scan_with_ragel(query_string)
8282
require "graphql/version"
8383
require "graphql/relay"
8484
require "graphql/execution"
85+
require "graphql/compatibility"

lib/graphql/compatibility.rb

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,3 @@
11
require "graphql/compatibility/execution_spec"
2-
3-
4-
module GraphQL
5-
# This module contains specifications for third-party GraphQL extensions.
6-
# It's not loaded by default. To use any members of this module, add:
7-
#
8-
# ```
9-
# require "graphql/compatibility"
10-
# ```
11-
#
12-
# to your project.
13-
module Compatibility
14-
end
15-
end
2+
require "graphql/compatibility/query_parser_spec"
3+
require "graphql/compatibility/schema_parser_spec"
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
require "graphql/compatibility/query_parser_spec/query_assertions"
2+
require "graphql/compatibility/query_parser_spec/parse_error_specification"
3+
4+
module GraphQL
5+
module Compatibility
6+
# This asserts that a given parse function turns a string into
7+
# the proper tree of {{GraphQL::Language::Nodes}}.
8+
module QueryParserSpec
9+
# @yieldparam query_string [String] A query string to parse
10+
# @yieldreturn [GraphQL::Language::Nodes::Document]
11+
# @return [Class<Minitest::Test>] A test suite for this parse function
12+
def self.build_suite(&block)
13+
Class.new(Minitest::Test) do
14+
include QueryAssertions
15+
include ParseErrorSpecification
16+
17+
@@parse_fn = block
18+
19+
def parse(query_string)
20+
@@parse_fn.call(query_string)
21+
end
22+
23+
def test_it_parses_queries
24+
document = parse(QUERY_STRING)
25+
query = document.definitions.first
26+
assert_valid_query(query)
27+
assert_valid_fragment(document.definitions.last)
28+
assert_valid_variable(query.variables.first)
29+
field = query.selections.first
30+
assert_valid_field(field)
31+
assert_valid_variable_argument(field.arguments.first)
32+
assert_valid_literal_argument(field.arguments.last)
33+
assert_valid_directive(field.directives.first)
34+
fragment_spread = query.selections[1].selections.last
35+
assert_valid_fragment_spread(fragment_spread)
36+
assert_valid_typed_inline_fragment(query.selections[2])
37+
assert_valid_typeless_inline_fragment(query.selections[3])
38+
end
39+
40+
def test_it_parses_empty_arguments
41+
strings = [
42+
"{ field { } }",
43+
"{ field() }",
44+
]
45+
strings.each do |query_str|
46+
doc = parse(query_str)
47+
field = doc.definitions.first.selections.first
48+
assert_equal 0, field.arguments.length
49+
assert_equal 0, field.selections.length
50+
end
51+
end
52+
53+
def test_it_parses_unnamed_queries
54+
document = parse("{ name, age, height }")
55+
operation = document.definitions.first
56+
assert_equal 1, document.definitions.length
57+
assert_equal "query", operation.operation_type
58+
assert_equal nil, operation.name
59+
assert_equal 3, operation.selections.length
60+
end
61+
62+
def test_it_parses_inputs
63+
query_string = %|
64+
{
65+
field(
66+
int: 3,
67+
float: 4.7e-24,
68+
bool: false,
69+
string: "☀︎🏆\\n escaped \\" unicode \\u00b6 /",
70+
enum: ENUM_NAME,
71+
array: [7, 8, 9]
72+
object: {a: [1,2,3], b: {c: "4"}}
73+
unicode_bom: "\xef\xbb\xbfquery"
74+
keywordEnum: on
75+
)
76+
}
77+
|
78+
document = parse(query_string)
79+
inputs = document.definitions.first.selections.first.arguments
80+
assert_equal 3, inputs[0].value, "Integers"
81+
assert_equal 0.47e-23, inputs[1].value, "Floats"
82+
assert_equal false, inputs[2].value, "Booleans"
83+
assert_equal %|☀︎🏆\n escaped " unicode ¶ /|, inputs[3].value, "Strings"
84+
assert_instance_of GraphQL::Language::Nodes::Enum, inputs[4].value
85+
assert_equal "ENUM_NAME", inputs[4].value.name, "Enums"
86+
assert_equal [7,8,9], inputs[5].value, "Lists"
87+
88+
obj = inputs[6].value
89+
assert_equal "a", obj.arguments[0].name
90+
assert_equal [1,2,3], obj.arguments[0].value
91+
assert_equal "b", obj.arguments[1].name
92+
assert_equal "c", obj.arguments[1].value.arguments[0].name
93+
assert_equal "4", obj.arguments[1].value.arguments[0].value
94+
95+
assert_equal %|\xef\xbb\xbfquery|, inputs[7].value, "Unicode BOM"
96+
assert_equal "on", inputs[8].value.name, "Enum value 'on'"
97+
end
98+
end
99+
end
100+
101+
QUERY_STRING = %|
102+
query getStuff($someVar: Int = 1, $anotherVar: [String!] ) @skip(if: false) {
103+
myField: someField(someArg: $someVar, ok: 1.4) @skip(if: $anotherVar) @thing(or: "Whatever")
104+
105+
anotherField(someArg: [1,2,3]) {
106+
nestedField
107+
... moreNestedFields @skip(if: true)
108+
}
109+
110+
... on OtherType @include(unless: false){
111+
field(arg: [{key: "value", anotherKey: 0.9, anotherAnotherKey: WHATEVER}])
112+
anotherField
113+
}
114+
115+
... {
116+
id
117+
}
118+
}
119+
120+
fragment moreNestedFields on NestedType @or(something: "ok") {
121+
anotherNestedField @enum(directive: true)
122+
}
123+
|
124+
end
125+
end
126+
end
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
module GraphQL
2+
module Compatibility
3+
# Include me into a minitest class
4+
# to add assertions about parse errors
5+
module ParseErrorSpecification
6+
def assert_raises_parse_error(query_string)
7+
assert_raises(GraphQL::ParseError) {
8+
parse(query_string)
9+
}
10+
end
11+
12+
def test_it_includes_line_and_column
13+
err = assert_raises_parse_error("
14+
query getCoupons {
15+
allCoupons: {data{id}}
16+
}
17+
")
18+
19+
assert_includes(err.message, '"{"')
20+
assert_equal(3, err.line)
21+
assert_equal(25, err.col)
22+
end
23+
24+
def test_it_rejects_unterminated_strings
25+
assert_raises_parse_error('{ " }')
26+
assert_raises_parse_error(%|{ "\n" }|)
27+
end
28+
29+
def test_it_rejects_unexpected_ends
30+
assert_raises_parse_error("query { stuff { thing }")
31+
end
32+
33+
def assert_rejects_character(char)
34+
err = assert_raises_parse_error("{ field#{char} }")
35+
assert_includes(err.message, char.inspect, "The message includes the invalid character")
36+
end
37+
38+
def test_it_rejects_invalid_characters
39+
assert_rejects_character(";")
40+
assert_rejects_character("\a")
41+
assert_rejects_character("\xef")
42+
assert_rejects_character("\v")
43+
assert_rejects_character("\f")
44+
assert_rejects_character("\xa0")
45+
end
46+
47+
def test_it_rejects_bad_unicode
48+
assert_raises_parse_error(%|{ field(arg:"\\x") }|)
49+
assert_raises_parse_error(%|{ field(arg:"\\u1") }|)
50+
assert_raises_parse_error(%|{ field(arg:"\\u0XX1") }|)
51+
assert_raises_parse_error(%|{ field(arg:"\\uXXXX") }|)
52+
assert_raises_parse_error(%|{ field(arg:"\\uFXXX") }|)
53+
assert_raises_parse_error(%|{ field(arg:"\\uXXXF") }|)
54+
end
55+
56+
def assert_empty_document(query_string)
57+
doc = parse(query_string)
58+
assert_equal 0, doc.definitions.length
59+
end
60+
61+
def test_it_parses_blank_queries
62+
assert_empty_document("")
63+
assert_empty_document(" ")
64+
assert_empty_document("\t \t")
65+
end
66+
67+
def test_it_restricts_on
68+
assert_raises_parse_error("{ ...on }")
69+
assert_raises_parse_error("fragment on on Type { field }")
70+
end
71+
72+
def test_it_rejects_null
73+
err = assert_raises_parse_error("{ field(input: null) }")
74+
assert_includes(err.message, "null")
75+
end
76+
end
77+
end
78+
end
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
module GraphQL
2+
module Compatibility
3+
module QueryParserSpec
4+
module QueryAssertions
5+
def assert_valid_query(query)
6+
assert query.is_a?(GraphQL::Language::Nodes::OperationDefinition)
7+
assert_equal "getStuff", query.name
8+
assert_equal "query", query.operation_type
9+
assert_equal 2, query.variables.length
10+
assert_equal 4, query.selections.length
11+
assert_equal 1, query.directives.length
12+
assert_equal [2, 13], [query.line, query.col]
13+
end
14+
15+
def assert_valid_fragment(fragment_def)
16+
assert fragment_def.is_a?(GraphQL::Language::Nodes::FragmentDefinition)
17+
assert_equal "moreNestedFields", fragment_def.name
18+
assert_equal 1, fragment_def.selections.length
19+
assert_equal "NestedType", fragment_def.type.name
20+
assert_equal 1, fragment_def.directives.length
21+
assert_equal [20, 13], fragment_def.position
22+
end
23+
24+
def assert_valid_variable(variable)
25+
assert_equal "someVar", variable.name
26+
assert_equal "Int", variable.type.name
27+
assert_equal 1, variable.default_value
28+
assert_equal [2, 28], variable.position
29+
end
30+
31+
def assert_valid_field(field)
32+
assert_equal "someField", field.name
33+
assert_equal "myField", field.alias
34+
assert_equal 2, field.directives.length
35+
assert_equal 2, field.arguments.length
36+
assert_equal 0, field.selections.length
37+
assert_equal [3, 15], field.position
38+
end
39+
40+
def assert_valid_literal_argument(argument)
41+
assert_equal "ok", argument.name
42+
assert_equal 1.4, argument.value
43+
end
44+
45+
def assert_valid_variable_argument(argument)
46+
assert_equal "someArg", argument.name
47+
assert_equal "someVar", argument.value.name
48+
end
49+
50+
def assert_valid_fragment_spread(fragment_spread)
51+
assert_equal "moreNestedFields", fragment_spread.name
52+
assert_equal 1, fragment_spread.directives.length
53+
assert_equal [7, 17], fragment_spread.position
54+
end
55+
56+
def assert_valid_directive(directive)
57+
assert_equal "skip", directive.name
58+
assert_equal "if", directive.arguments.first.name
59+
assert_equal 1, directive.arguments.length
60+
assert_equal [3, 62], directive.position
61+
end
62+
63+
def assert_valid_typed_inline_fragment(inline_fragment)
64+
assert_equal "OtherType", inline_fragment.type.name
65+
assert_equal 2, inline_fragment.selections.length
66+
assert_equal 1, inline_fragment.directives.length
67+
assert_equal [10, 15], inline_fragment.position
68+
end
69+
70+
def assert_valid_typeless_inline_fragment(inline_fragment)
71+
assert_equal nil, inline_fragment.type
72+
assert_equal 1, inline_fragment.selections.length
73+
assert_equal 0, inline_fragment.directives.length
74+
end
75+
end
76+
end
77+
end
78+
end

0 commit comments

Comments
 (0)