graphql 0.15.3 → 0.16.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/lib/graphql.rb +4 -1
  3. data/lib/graphql/analysis.rb +5 -0
  4. data/lib/graphql/analysis/analyze_query.rb +73 -0
  5. data/lib/graphql/analysis/max_query_complexity.rb +25 -0
  6. data/lib/graphql/analysis/max_query_depth.rb +25 -0
  7. data/lib/graphql/analysis/query_complexity.rb +122 -0
  8. data/lib/graphql/analysis/query_depth.rb +54 -0
  9. data/lib/graphql/analysis_error.rb +4 -0
  10. data/lib/graphql/base_type.rb +7 -0
  11. data/lib/graphql/define/assign_object_field.rb +2 -1
  12. data/lib/graphql/field.rb +25 -3
  13. data/lib/graphql/input_object_type.rb +1 -1
  14. data/lib/graphql/internal_representation.rb +2 -0
  15. data/lib/graphql/internal_representation/node.rb +81 -0
  16. data/lib/graphql/internal_representation/rewrite.rb +177 -0
  17. data/lib/graphql/language/visitor.rb +15 -9
  18. data/lib/graphql/object_type.rb +1 -1
  19. data/lib/graphql/query.rb +66 -7
  20. data/lib/graphql/query/context.rb +10 -3
  21. data/lib/graphql/query/directive_resolution.rb +5 -5
  22. data/lib/graphql/query/serial_execution.rb +5 -3
  23. data/lib/graphql/query/serial_execution/field_resolution.rb +22 -15
  24. data/lib/graphql/query/serial_execution/operation_resolution.rb +7 -5
  25. data/lib/graphql/query/serial_execution/selection_resolution.rb +20 -105
  26. data/lib/graphql/query/serial_execution/value_resolution.rb +15 -12
  27. data/lib/graphql/schema.rb +7 -2
  28. data/lib/graphql/schema/timeout_middleware.rb +67 -0
  29. data/lib/graphql/static_validation/all_rules.rb +0 -1
  30. data/lib/graphql/static_validation/type_stack.rb +7 -11
  31. data/lib/graphql/static_validation/validation_context.rb +11 -1
  32. data/lib/graphql/static_validation/validator.rb +14 -4
  33. data/lib/graphql/version.rb +1 -1
  34. data/readme.md +10 -9
  35. data/spec/graphql/analysis/analyze_query_spec.rb +50 -0
  36. data/spec/graphql/analysis/max_query_complexity_spec.rb +62 -0
  37. data/spec/graphql/{static_validation/rules/document_does_not_exceed_max_depth_spec.rb → analysis/max_query_depth_spec.rb} +20 -21
  38. data/spec/graphql/analysis/query_complexity_spec.rb +235 -0
  39. data/spec/graphql/analysis/query_depth_spec.rb +80 -0
  40. data/spec/graphql/directive_spec.rb +1 -0
  41. data/spec/graphql/internal_representation/rewrite_spec.rb +120 -0
  42. data/spec/graphql/introspection/schema_type_spec.rb +1 -0
  43. data/spec/graphql/language/visitor_spec.rb +14 -4
  44. data/spec/graphql/non_null_type_spec.rb +31 -0
  45. data/spec/graphql/query/context_spec.rb +24 -1
  46. data/spec/graphql/query_spec.rb +6 -2
  47. data/spec/graphql/schema/timeout_middleware_spec.rb +180 -0
  48. data/spec/graphql/static_validation/rules/argument_literals_are_compatible_spec.rb +1 -1
  49. data/spec/graphql/static_validation/rules/arguments_are_defined_spec.rb +1 -1
  50. data/spec/graphql/static_validation/rules/directives_are_defined_spec.rb +1 -1
  51. data/spec/graphql/static_validation/rules/directives_are_in_valid_locations_spec.rb +1 -1
  52. data/spec/graphql/static_validation/rules/fields_are_defined_on_type_spec.rb +1 -1
  53. data/spec/graphql/static_validation/rules/fields_have_appropriate_selections_spec.rb +1 -1
  54. data/spec/graphql/static_validation/rules/fields_will_merge_spec.rb +1 -1
  55. data/spec/graphql/static_validation/rules/fragment_spreads_are_possible_spec.rb +1 -1
  56. data/spec/graphql/static_validation/rules/fragment_types_exist_spec.rb +1 -1
  57. data/spec/graphql/static_validation/rules/fragments_are_finite_spec.rb +1 -1
  58. data/spec/graphql/static_validation/rules/fragments_are_on_composite_types_spec.rb +1 -1
  59. data/spec/graphql/static_validation/rules/fragments_are_used_spec.rb +1 -1
  60. data/spec/graphql/static_validation/rules/required_arguments_are_present_spec.rb +1 -1
  61. data/spec/graphql/static_validation/rules/variable_default_values_are_correctly_typed_spec.rb +1 -1
  62. data/spec/graphql/static_validation/rules/variable_usages_are_allowed_spec.rb +1 -1
  63. data/spec/graphql/static_validation/rules/variables_are_input_types_spec.rb +1 -1
  64. data/spec/graphql/static_validation/rules/variables_are_used_and_defined_spec.rb +1 -1
  65. data/spec/graphql/static_validation/validator_spec.rb +1 -1
  66. data/spec/support/dairy_app.rb +22 -1
  67. metadata +29 -5
  68. data/lib/graphql/static_validation/rules/document_does_not_exceed_max_depth.rb +0 -79
@@ -23,7 +23,6 @@ module GraphQL
23
23
  GraphQL::StaticValidation::VariableDefaultValuesAreCorrectlyTyped,
24
24
  GraphQL::StaticValidation::VariablesAreUsedAndDefined,
25
25
  GraphQL::StaticValidation::VariableUsagesAreAllowed,
26
- GraphQL::StaticValidation::DocumentDoesNotExceedMaxDepth,
27
26
  ]
28
27
  end
29
28
  end
@@ -89,18 +89,14 @@ module GraphQL
89
89
  def push(stack, node)
90
90
  parent_type = stack.object_types.last
91
91
  parent_type = parent_type.unwrap
92
- if parent_type.kind.fields?
93
- field_class = stack.schema.get_field(parent_type, node.name)
94
- stack.field_definitions.push(field_class)
95
- if !field_class.nil?
96
- next_object_type = field_class.type
97
- stack.object_types.push(next_object_type)
98
- else
99
- stack.object_types.push(nil)
100
- end
92
+
93
+ field_definition = stack.schema.get_field(parent_type, node.name)
94
+ stack.field_definitions.push(field_definition)
95
+ if !field_definition.nil?
96
+ next_object_type = field_definition.type
97
+ stack.object_types.push(next_object_type)
101
98
  else
102
- stack.field_definitions.push(nil)
103
- stack.object_types.push(parent_type)
99
+ stack.object_types.push(nil)
104
100
  end
105
101
  end
106
102
 
@@ -29,7 +29,7 @@ module GraphQL
29
29
  end
30
30
 
31
31
  @errors = []
32
- @visitor = GraphQL::Language::Visitor.new
32
+ @visitor = GraphQL::Language::Visitor.new(document)
33
33
  @type_stack = GraphQL::StaticValidation::TypeStack.new(schema, visitor)
34
34
  end
35
35
 
@@ -37,6 +37,16 @@ module GraphQL
37
37
  @type_stack.object_types
38
38
  end
39
39
 
40
+ # @return [GraphQL::BaseType] The current object type
41
+ def type_definition
42
+ object_types.last
43
+ end
44
+
45
+ # @return [GraphQL::BaseType] The type which the current type came from
46
+ def parent_type_definition
47
+ object_types[-2]
48
+ end
49
+
40
50
  # @return [GraphQL::Field, nil] The most-recently-entered GraphQL::Field, if currently inside one
41
51
  def field_definition
42
52
  @type_stack.field_definitions.last
@@ -17,16 +17,26 @@ module GraphQL
17
17
  @rules = rules
18
18
  end
19
19
 
20
- # Validate `document` against the schema. Returns an array of message hashes.
21
- # @param document [GraphQL::Language::Nodes::Document]
20
+ # Validate `query` against the schema. Returns an array of message hashes.
21
+ # @param query [GraphQL::Query]
22
22
  # @return [Array<Hash>]
23
23
  def validate(query)
24
24
  context = GraphQL::StaticValidation::ValidationContext.new(query)
25
+ rewrite = GraphQL::InternalRepresentation::Rewrite.new
26
+
27
+ # Put this first so its enters and exits are always called
28
+ rewrite.validate(context)
25
29
  @rules.each do |rules|
26
30
  rules.new.validate(context)
27
31
  end
28
- context.visitor.visit(query.document)
29
- context.errors.map(&:to_h)
32
+
33
+ context.visitor.visit
34
+
35
+ {
36
+ errors: context.errors.map(&:to_h),
37
+ # If there were errors, the irep is garbage
38
+ irep: context.errors.none? ? rewrite.operations : nil,
39
+ }
30
40
  end
31
41
  end
32
42
  end
@@ -1,3 +1,3 @@
1
1
  module GraphQL
2
- VERSION = "0.15.3"
2
+ VERSION = "0.16.0"
3
3
  end
data/readme.md CHANGED
@@ -13,6 +13,9 @@ A Ruby implementation of [GraphQL](http://graphql.org/).
13
13
  - [Defining Your Schema](http://www.rubydoc.info/github/rmosolgo/graphql-ruby/file/guides/defining_your_schema.md)
14
14
  - [Executing Queries](http://www.rubydoc.info/github/rmosolgo/graphql-ruby/file/guides/executing_queries.md)
15
15
  - [Testing](http://www.rubydoc.info/github/rmosolgo/graphql-ruby/file/guides/testing.md)
16
+ - [Code Reuse](http://www.rubydoc.info/github/rmosolgo/graphql-ruby/file/guides/code_reuse.md)
17
+ - [Security](http://www.rubydoc.info/github/rmosolgo/graphql-ruby/file/guides/security.md)
18
+
16
19
 
17
20
  - [API Documentation](http://www.rubydoc.info/github/rmosolgo/graphql-ruby)
18
21
 
@@ -119,16 +122,14 @@ If you're building a backend for [Relay](http://facebook.github.io/relay/), you'
119
122
 
120
123
  ## To Do
121
124
 
122
- - __1.0 items:__
123
- - Non-nulls should _propagate_ to the next non-null field (all the way up to data, if need be)
124
- - Add docs for shared behaviors & DRY code
125
- - Subscriptions
126
- - Is there something to do at the graphql-ruby level to make this easier for specific implementations?
127
125
  - Accept type name as `type` argument?
128
126
  - Goal: accept `field :post, "Post"` to look up a type named `"Post"` in the schema
129
127
  - Problem: how does a field know which schema to look up the name from?
130
128
  - Problem: how can we load types in Rails without accessing the constant?
131
- - Customizable complexity validator
132
- - Types / fields can define their "weight" in a query
133
- - Queries can be executed with a "max weight", or Schema can have a default
134
- - During validation, we make sure the query doesn't exceed "max weight"
129
+ - Maybe support by third-party library? `type("Post!")` could implement "type_missing", keeps `graphql-ruby` very simple
130
+ - If we eval'd `define { ... }` blocks _lazily_, would that work around all circular dependency issues?
131
+ - `QueryComplexity` improvements:
132
+ - Better detection of union / interface possibilities? Right now they're summed
133
+ - Type check improvements:
134
+ - Use catch-all type/field/argument definitions instead of terminating traversal
135
+ - Reduce ad-hoc traversals?
@@ -0,0 +1,50 @@
1
+ require "spec_helper"
2
+
3
+ describe GraphQL::Analysis do
4
+ class TypeCollector
5
+ def initial_value(query)
6
+ []
7
+ end
8
+
9
+ def call(memo, visit_type, irep_node)
10
+ if visit_type == :enter
11
+ memo + [irep_node.return_type]
12
+ else
13
+ memo
14
+ end
15
+ end
16
+ end
17
+
18
+ describe ".analyze_query" do
19
+ let(:node_counter) {
20
+ -> (memo, visit_type, irep_node) {
21
+ memo ||= Hash.new { |h,k| h[k] = 0 }
22
+ visit_type == :enter && memo[irep_node.ast_node.class] += 1
23
+ memo
24
+ }
25
+ }
26
+ let(:type_collector) { TypeCollector.new }
27
+ let(:analyzers) { [type_collector, node_counter] }
28
+ let(:reduce_result) { GraphQL::Analysis.analyze_query(query, analyzers) }
29
+ let(:query) { GraphQL::Query.new(DummySchema, query_string) }
30
+ let(:query_string) {%|
31
+ {
32
+ cheese(id: 1) {
33
+ id
34
+ flavor
35
+ }
36
+ }
37
+ |}
38
+
39
+ it "calls the defined analyzers" do
40
+ collected_types, node_counts = reduce_result
41
+ expected_visited_types = [QueryType, CheeseType, GraphQL::INT_TYPE, GraphQL::STRING_TYPE]
42
+ assert_equal expected_visited_types, collected_types
43
+ expected_node_counts = {
44
+ GraphQL::Language::Nodes::OperationDefinition => 1,
45
+ GraphQL::Language::Nodes::Field => 3,
46
+ }
47
+ assert_equal expected_node_counts, node_counts
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,62 @@
1
+ require "spec_helper"
2
+
3
+ describe GraphQL::Analysis::MaxQueryComplexity do
4
+ before do
5
+ @prev_max_complexity = DummySchema.max_complexity
6
+ end
7
+
8
+ after do
9
+ DummySchema.max_complexity = @prev_max_complexity
10
+ end
11
+
12
+
13
+ let(:result) { DummySchema.execute(query_string) }
14
+ let(:query_string) {%|
15
+ {
16
+ a: cheese(id: 1) { id }
17
+ b: cheese(id: 1) { id }
18
+ c: cheese(id: 1) { id }
19
+ d: cheese(id: 1) { id }
20
+ e: cheese(id: 1) { id }
21
+ }
22
+ |}
23
+
24
+ describe "when a query goes over max complexity" do
25
+ before do
26
+ DummySchema.max_complexity = 9
27
+ end
28
+
29
+ it "returns an error" do
30
+ assert_equal "Query has complexity of 10, which exceeds max complexity of 9", result["errors"][0]["message"]
31
+ end
32
+ end
33
+
34
+ describe "when there is no max complexity" do
35
+ before do
36
+ DummySchema.max_complexity = nil
37
+ end
38
+ it "doesn't error" do
39
+ assert_equal nil, result["errors"]
40
+ end
41
+ end
42
+
43
+ describe "when the query is less than the max complexity" do
44
+ before do
45
+ DummySchema.max_complexity = 99
46
+ end
47
+ it "doesn't error" do
48
+ assert_equal nil, result["errors"]
49
+ end
50
+ end
51
+
52
+ describe "when complexity is overriden at query-level" do
53
+ before do
54
+ DummySchema.max_complexity = 100
55
+ end
56
+ let(:result) { DummySchema.execute(query_string, max_complexity: 7) }
57
+
58
+ it "is applied" do
59
+ assert_equal "Query has complexity of 10, which exceeds max complexity of 7", result["errors"][0]["message"]
60
+ end
61
+ end
62
+ end
@@ -1,11 +1,15 @@
1
1
  require "spec_helper"
2
2
 
3
- describe GraphQL::StaticValidation::DocumentDoesNotExceedMaxDepth do
4
- let(:rule) { GraphQL::StaticValidation::DocumentDoesNotExceedMaxDepth }
5
- let(:validator) { GraphQL::StaticValidation::Validator.new(schema: DummySchema, rules: [rule]) }
6
- let(:query) { GraphQL::Query.new(DummySchema, query_string) }
7
- let(:errors) { validator.validate(query) }
3
+ describe GraphQL::Analysis::MaxQueryDepth do
4
+ before do
5
+ @prev_max_depth = DummySchema.max_depth
6
+ end
7
+
8
+ after do
9
+ DummySchema.max_depth = @prev_max_depth
10
+ end
8
11
 
12
+ let(:result) { DummySchema.execute(query_string) }
9
13
  let(:query_string) { "
10
14
  {
11
15
  cheese(id: 1) {
@@ -26,48 +30,43 @@ describe GraphQL::StaticValidation::DocumentDoesNotExceedMaxDepth do
26
30
 
27
31
  describe "when the query is deeper than max depth" do
28
32
  it "adds an error message for a too-deep query" do
29
- assert_equal 1, errors.length
33
+ assert_equal "Query has depth of 7, which exceeds max depth of 5", result["errors"][0]["message"]
30
34
  end
31
35
  end
32
36
 
33
37
  describe "when the query specifies a different max_depth" do
34
- let(:query) { GraphQL::Query.new(DummySchema, query_string, max_depth: 100) }
38
+ let(:result) { DummySchema.execute(query_string, max_depth: 100) }
39
+
35
40
  it "obeys that max_depth" do
36
- assert_equal 0, errors.length
41
+ assert_equal nil, result["errors"]
37
42
  end
38
43
  end
39
44
 
40
45
  describe "When the query is not deeper than max_depth" do
41
46
  before do
42
- @prev_max_depth = DummySchema.max_depth
43
47
  DummySchema.max_depth = 100
44
48
  end
45
49
 
46
- after do
47
- DummySchema.max_depth = @prev_max_depth
48
- end
49
-
50
50
  it "doesn't add an error" do
51
- assert_equal 0, errors.length
51
+ assert_equal nil, result["errors"]
52
52
  end
53
53
  end
54
54
 
55
55
  describe "when the max depth isn't set" do
56
56
  before do
57
- @prev_max_depth = DummySchema.max_depth
58
57
  DummySchema.max_depth = nil
59
58
  end
60
59
 
61
- after do
62
- DummySchema.max_depth = @prev_max_depth
63
- end
64
-
65
60
  it "doesn't add an error message" do
66
- assert_equal 0, errors.length
61
+ assert_equal nil, result["errors"]
67
62
  end
68
63
  end
69
64
 
70
65
  describe "when a fragment exceeds max depth" do
66
+ before do
67
+ DummySchema.max_depth = 4
68
+ end
69
+
71
70
  let(:query_string) { "
72
71
  {
73
72
  cheese(id: 1) {
@@ -95,7 +94,7 @@ describe GraphQL::StaticValidation::DocumentDoesNotExceedMaxDepth do
95
94
  "}
96
95
 
97
96
  it "adds an error message for a too-deep query" do
98
- assert_equal 1, errors.length
97
+ assert_equal 1, result["errors"].length
99
98
  end
100
99
  end
101
100
  end
@@ -0,0 +1,235 @@
1
+ require "spec_helper"
2
+
3
+ describe GraphQL::Analysis::QueryComplexity do
4
+ let(:complexities) { [] }
5
+ let(:query_complexity) { GraphQL::Analysis::QueryComplexity.new { |this_query, complexity| complexities << this_query << complexity } }
6
+ let(:reduce_result) { GraphQL::Analysis.analyze_query(query, [query_complexity]) }
7
+ let(:variables) { {} }
8
+ let(:query) { GraphQL::Query.new(DummySchema, query_string, variables: variables) }
9
+
10
+ describe "simple queries" do
11
+ let(:query_string) {%|
12
+ query cheeses($isSkipped: Boolean = false){
13
+ # complexity of 3
14
+ cheese1: cheese(id: 1) {
15
+ id
16
+ flavor
17
+ }
18
+
19
+ # complexity of 4
20
+ cheese2: cheese(id: 2) @skip(if: $isSkipped) {
21
+ similarCheese(source: SHEEP) {
22
+ ... on Cheese {
23
+ similarCheese(source: SHEEP) {
24
+ id
25
+ }
26
+ }
27
+ }
28
+ }
29
+ }
30
+ |}
31
+
32
+ it "sums the complexity" do
33
+ reduce_result
34
+ assert_equal complexities, [query, 7]
35
+ end
36
+
37
+ describe "when skipped by directives" do
38
+ let(:variables) { { "isSkipped" => true } }
39
+ it "doesn't include skipped fields" do
40
+ reduce_result
41
+ assert_equal complexities, [query, 3]
42
+ end
43
+ end
44
+ end
45
+
46
+ describe "query with fragments" do
47
+ let(:query_string) {%|
48
+ {
49
+ # complexity of 3
50
+ cheese1: cheese(id: 1) {
51
+ id
52
+ flavor
53
+ }
54
+
55
+ # complexity of 7
56
+ cheese2: cheese(id: 2) {
57
+ ... cheeseFields1
58
+ ... cheeseFields2
59
+ }
60
+ }
61
+
62
+ fragment cheeseFields1 on Cheese {
63
+ similarCow: similarCheese(source: COW) {
64
+ id
65
+ ... cheeseFields2
66
+ }
67
+ }
68
+
69
+ fragment cheeseFields2 on Cheese {
70
+ similarSheep: similarCheese(source: SHEEP) {
71
+ id
72
+ }
73
+ }
74
+ |}
75
+
76
+ it "counts all fragment usages, not the definitions" do
77
+ reduce_result
78
+ assert_equal complexities, [query, 10]
79
+ end
80
+
81
+ describe "mutually exclusive types" do
82
+ let(:query_string) {%|
83
+ {
84
+ favoriteEdible {
85
+ # 1 for everybody
86
+ fatContent
87
+
88
+ # 1 for everybody
89
+ ... on Edible {
90
+ origin
91
+ }
92
+
93
+ # 1 for honey
94
+ ... on Sweetener {
95
+ sweetness
96
+ }
97
+
98
+ # 2 for milk
99
+ ... milkFields
100
+ # 1 for cheese
101
+ ... cheeseFields
102
+ # 1 for honey
103
+ ... honeyFields
104
+ # 1 for milk + cheese
105
+ ... dairyProductFields
106
+ }
107
+ }
108
+
109
+ fragment milkFields on Milk {
110
+ id
111
+ source
112
+ }
113
+
114
+ fragment cheeseFields on Cheese {
115
+ source
116
+ }
117
+
118
+ fragment honeyFields on Honey {
119
+ flowerType
120
+ }
121
+
122
+ fragment dairyProductFields on DairyProduct {
123
+ ... on Cheese {
124
+ flavor
125
+ }
126
+
127
+ ... on Milk {
128
+ flavors
129
+ }
130
+ }
131
+ |}
132
+
133
+ it "gets the max among options" do
134
+ reduce_result
135
+ assert_equal 5, complexities.last
136
+ end
137
+ end
138
+
139
+
140
+ describe "when there are no selections on any object types" do
141
+ let(:query_string) {%|
142
+ {
143
+ favoriteEdible {
144
+ # 1 for everybody
145
+ fatContent
146
+
147
+ # 1 for everybody
148
+ ... on Edible { origin }
149
+
150
+ # 1 for honey
151
+ ... on Sweetener { sweetness }
152
+ }
153
+ }
154
+ |}
155
+
156
+ it "gets the max among interface types" do
157
+ reduce_result
158
+ assert_equal 3, complexities.last
159
+ end
160
+ end
161
+
162
+ describe "redundant fields" do
163
+ let(:query_string) {%|
164
+ {
165
+ favoriteEdible {
166
+ fatContent
167
+ # this is executed separately and counts separately:
168
+ aliasedFatContent: fatContent
169
+
170
+ ... on Edible {
171
+ fatContent
172
+ }
173
+
174
+ ... edibleFields
175
+ }
176
+ }
177
+
178
+ fragment edibleFields on Edible {
179
+ fatContent
180
+ }
181
+ |}
182
+
183
+ it "only counts them once" do
184
+ reduce_result
185
+ assert_equal 3, complexities.last
186
+ end
187
+ end
188
+ end
189
+
190
+
191
+ describe "custom complexities" do
192
+ let(:query) { GraphQL::Query.new(complexity_schema, query_string) }
193
+ let(:complexity_schema) {
194
+ complexity_type = GraphQL::ObjectType.define do
195
+ name "Complexity"
196
+ field :value, types.Int, complexity: 0.1 do
197
+ resolve -> (obj, args, ctx) { obj }
198
+ end
199
+ field :complexity, -> { complexity_type } do
200
+ argument :value, types.Int
201
+ complexity -> (ctx, args, child_complexity) { args[:value] + child_complexity }
202
+ resolve -> (obj, args, ctx) { args[:value] }
203
+ end
204
+ end
205
+
206
+ query_type = GraphQL::ObjectType.define do
207
+ name "Query"
208
+ field :complexity, -> { complexity_type } do
209
+ argument :value, types.Int
210
+ complexity -> (ctx, args, child_complexity) { args[:value] + child_complexity }
211
+ resolve -> (obj, args, ctx) { args[:value] }
212
+ end
213
+ end
214
+
215
+ GraphQL::Schema.new(query: query_type)
216
+ }
217
+ let(:query_string) {%|
218
+ {
219
+ a: complexity(value: 3) { value }
220
+ b: complexity(value: 6) {
221
+ value
222
+ complexity(value: 1) {
223
+ value
224
+ }
225
+ }
226
+ }
227
+ |}
228
+
229
+ it "sums the complexity" do
230
+ reduce_result
231
+ # 10 from `complexity`, `0.3` from `value`
232
+ assert_equal complexities, [query, 10.3]
233
+ end
234
+ end
235
+ end