graphql 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. checksums.yaml +4 -4
  2. data/lib/graphql/analysis/analyze_query.rb +4 -2
  3. data/lib/graphql/analysis/field_usage.rb +4 -4
  4. data/lib/graphql/analysis/query_complexity.rb +16 -21
  5. data/lib/graphql/argument.rb +13 -6
  6. data/lib/graphql/base_type.rb +2 -1
  7. data/lib/graphql/compatibility/execution_specification.rb +76 -0
  8. data/lib/graphql/compatibility/query_parser_specification.rb +16 -2
  9. data/lib/graphql/compatibility/query_parser_specification/parse_error_specification.rb +0 -5
  10. data/lib/graphql/compatibility/schema_parser_specification.rb +6 -0
  11. data/lib/graphql/define/assign_argument.rb +8 -2
  12. data/lib/graphql/define/instance_definable.rb +12 -15
  13. data/lib/graphql/directive.rb +2 -1
  14. data/lib/graphql/enum_type.rb +5 -7
  15. data/lib/graphql/field.rb +6 -11
  16. data/lib/graphql/field/resolve.rb +1 -0
  17. data/lib/graphql/input_object_type.rb +9 -9
  18. data/lib/graphql/interface_type.rb +2 -1
  19. data/lib/graphql/internal_representation.rb +1 -0
  20. data/lib/graphql/internal_representation/node.rb +31 -9
  21. data/lib/graphql/internal_representation/rewrite.rb +26 -26
  22. data/lib/graphql/internal_representation/selections.rb +41 -0
  23. data/lib/graphql/introspection/input_value_type.rb +6 -2
  24. data/lib/graphql/language/generation.rb +2 -0
  25. data/lib/graphql/language/lexer.rl +4 -0
  26. data/lib/graphql/language/nodes.rb +3 -0
  27. data/lib/graphql/language/parser.rb +525 -509
  28. data/lib/graphql/language/parser.y +2 -0
  29. data/lib/graphql/object_type.rb +2 -2
  30. data/lib/graphql/query.rb +21 -0
  31. data/lib/graphql/query/context.rb +52 -4
  32. data/lib/graphql/query/serial_execution.rb +3 -4
  33. data/lib/graphql/query/serial_execution/field_resolution.rb +35 -36
  34. data/lib/graphql/query/serial_execution/operation_resolution.rb +9 -15
  35. data/lib/graphql/query/serial_execution/selection_resolution.rb +14 -11
  36. data/lib/graphql/query/serial_execution/value_resolution.rb +18 -17
  37. data/lib/graphql/query/variables.rb +1 -1
  38. data/lib/graphql/relay/mutation.rb +5 -8
  39. data/lib/graphql/scalar_type.rb +1 -2
  40. data/lib/graphql/schema.rb +2 -13
  41. data/lib/graphql/schema/build_from_definition.rb +28 -13
  42. data/lib/graphql/schema/loader.rb +4 -1
  43. data/lib/graphql/schema/printer.rb +10 -3
  44. data/lib/graphql/schema/timeout_middleware.rb +18 -2
  45. data/lib/graphql/schema/unique_within_type.rb +6 -3
  46. data/lib/graphql/static_validation/literal_validator.rb +3 -1
  47. data/lib/graphql/union_type.rb +1 -2
  48. data/lib/graphql/version.rb +1 -1
  49. data/readme.md +1 -0
  50. data/spec/graphql/analysis/analyze_query_spec.rb +6 -8
  51. data/spec/graphql/argument_spec.rb +18 -0
  52. data/spec/graphql/define/assign_argument_spec.rb +48 -0
  53. data/spec/graphql/define/instance_definable_spec.rb +4 -2
  54. data/spec/graphql/execution_error_spec.rb +66 -0
  55. data/spec/graphql/input_object_type_spec.rb +81 -0
  56. data/spec/graphql/internal_representation/rewrite_spec.rb +104 -21
  57. data/spec/graphql/introspection/input_value_type_spec.rb +43 -6
  58. data/spec/graphql/introspection/schema_type_spec.rb +1 -0
  59. data/spec/graphql/introspection/type_type_spec.rb +2 -0
  60. data/spec/graphql/language/generation_spec.rb +3 -2
  61. data/spec/graphql/query/arguments_spec.rb +17 -4
  62. data/spec/graphql/query/context_spec.rb +23 -0
  63. data/spec/graphql/query/variables_spec.rb +15 -1
  64. data/spec/graphql/relay/mutation_spec.rb +42 -2
  65. data/spec/graphql/schema/build_from_definition_spec.rb +4 -2
  66. data/spec/graphql/schema/loader_spec.rb +59 -1
  67. data/spec/graphql/schema/printer_spec.rb +2 -0
  68. data/spec/graphql/schema/reduce_types_spec.rb +1 -1
  69. data/spec/graphql/schema/timeout_middleware_spec.rb +2 -2
  70. data/spec/graphql/schema/unique_within_type_spec.rb +9 -0
  71. data/spec/graphql/schema/validation_spec.rb +15 -3
  72. data/spec/graphql/static_validation/rules/argument_literals_are_compatible_spec.rb +122 -0
  73. data/spec/graphql/static_validation/rules/variable_default_values_are_correctly_typed_spec.rb +78 -0
  74. data/spec/support/dairy_app.rb +9 -0
  75. data/spec/support/minimum_input_object.rb +4 -0
  76. data/spec/support/star_wars_schema.rb +1 -1
  77. metadata +5 -5
  78. data/lib/graphql/query/serial_execution/execution_context.rb +0 -37
  79. data/spec/graphql/query/serial_execution/execution_context_spec.rb +0 -54
@@ -22,4 +22,22 @@ describe GraphQL::Argument do
22
22
  argument = GraphQL::Argument.define(name: :favoriteFood, type: -> { GraphQL::STRING_TYPE })
23
23
  assert_equal GraphQL::STRING_TYPE, argument.type
24
24
  end
25
+
26
+ it "accepts a default_value" do
27
+ argument = GraphQL::Argument.define(name: :favoriteFood, type: GraphQL::STRING_TYPE, default_value: 'Default')
28
+ assert_equal 'Default', argument.default_value
29
+ assert argument.default_value?
30
+ end
31
+
32
+ it "accepts a default_value of nil" do
33
+ argument = GraphQL::Argument.define(name: :favoriteFood, type: GraphQL::STRING_TYPE, default_value: nil)
34
+ assert argument.default_value.nil?
35
+ assert argument.default_value?
36
+ end
37
+
38
+ it "default_value is optional" do
39
+ argument = GraphQL::Argument.define(name: :favoriteFood, type: GraphQL::STRING_TYPE)
40
+ assert argument.default_value.nil?
41
+ assert !argument.default_value?
42
+ end
25
43
  end
@@ -0,0 +1,48 @@
1
+ require "spec_helper"
2
+
3
+ describe GraphQL::Define::AssignArgument do
4
+ it "it accepts default_value" do
5
+ arg = define_argument(:a, GraphQL::STRING_TYPE, default_value: 'Default')
6
+
7
+ assert_equal "Default", arg.default_value
8
+ assert arg.default_value?
9
+ end
10
+
11
+ it "default_value is optional" do
12
+ arg = define_argument(:a, GraphQL::STRING_TYPE)
13
+
14
+ assert arg.default_value.nil?
15
+ assert !arg.default_value?
16
+ end
17
+
18
+ it "default_value can be explicitly set to nil" do
19
+ arg = define_argument(:a, GraphQL::STRING_TYPE, default_value: nil)
20
+
21
+ assert arg.default_value.nil?
22
+ assert arg.default_value?
23
+ end
24
+
25
+ it "passing unknown keyword arguments will raise" do
26
+ err = assert_raises ArgumentError do
27
+ define_argument(:a, GraphQL::STRING_TYPE, blah: nil)
28
+ end
29
+
30
+ assert_equal 'unknown keyword: blah', err.message
31
+
32
+ err = assert_raises ArgumentError do
33
+ define_argument(:a, GraphQL::STRING_TYPE, blah: nil, blah2: nil)
34
+ end
35
+
36
+ assert_equal 'unknown keywords: blah, blah2', err.message
37
+ end
38
+
39
+ def define_argument(*args)
40
+ type = GraphQL::ObjectType.define do
41
+ field :a, types.String do
42
+ argument(*args)
43
+ end
44
+ end
45
+
46
+ type.fields['a'].arguments[args.first.to_s]
47
+ end
48
+ end
@@ -11,11 +11,13 @@ module Garden
11
11
 
12
12
  class Vegetable
13
13
  include GraphQL::Define::InstanceDefinable
14
- lazy_defined_attr_accessor :name, :start_planting_on, :end_planting_on
14
+ attr_accessor :name, :start_planting_on, :end_planting_on
15
+ ensure_defined(:name, :start_planting_on, :end_planting_on)
15
16
  accepts_definitions :name, plant_between: DefinePlantBetween, color: GraphQL::Define.assign_metadata_key(:color)
16
17
 
17
18
  # definition added later:
18
- lazy_defined_attr_accessor :height
19
+ attr_accessor :height
20
+ ensure_defined(:height)
19
21
  end
20
22
  end
21
23
 
@@ -44,6 +44,7 @@ describe GraphQL::ExecutionError do
44
44
  }
45
45
  }
46
46
  executionError
47
+ valueWithExecutionError
47
48
  }
48
49
 
49
50
  fragment similarCheeseFields on Cheese {
@@ -90,6 +91,7 @@ describe GraphQL::ExecutionError do
90
91
  ]
91
92
  },
92
93
  "executionError" => nil,
94
+ "valueWithExecutionError" => 0
93
95
  },
94
96
  "errors"=>[
95
97
  {
@@ -127,6 +129,11 @@ describe GraphQL::ExecutionError do
127
129
  "locations"=>[{"line"=>41, "column"=>7}],
128
130
  "path"=>["executionError"]
129
131
  },
132
+ {
133
+ "message"=>"Could not fetch latest value",
134
+ "locations"=>[{"line"=>42, "column"=>7}],
135
+ "path"=>["valueWithExecutionError"]
136
+ },
130
137
  ]
131
138
  }
132
139
  assert_equal(expected_result, result)
@@ -185,4 +192,63 @@ describe GraphQL::ExecutionError do
185
192
  assert_equal(expected_result, result)
186
193
  end
187
194
  end
195
+
196
+ describe "fragment query when returned from a field" do
197
+ let(:query_string) {%|
198
+ query MilkQuery {
199
+ dairy {
200
+ ...Dairy
201
+ }
202
+ }
203
+
204
+ fragment Dairy on Dairy {
205
+ milks {
206
+ source
207
+ executionError
208
+ allDairy {
209
+ __typename
210
+ ...Milk
211
+ }
212
+ }
213
+ }
214
+
215
+ fragment Milk on Milk {
216
+ origin
217
+ executionError
218
+ }
219
+ |}
220
+ it "the error is inserted into the errors key and the rest of the query is fulfilled" do
221
+ expected_result = {
222
+ "data"=>{
223
+ "dairy" => {
224
+ "milks" => [
225
+ {
226
+ "source" => "COW",
227
+ "executionError" => nil,
228
+ "allDairy" => [
229
+ { "__typename" => "Cheese" },
230
+ { "__typename" => "Cheese" },
231
+ { "__typename" => "Cheese" },
232
+ { "__typename" => "Milk", "origin" => "Antiquity", "executionError" => nil }
233
+ ]
234
+ }
235
+ ]
236
+ }
237
+ },
238
+ "errors"=>[
239
+ {
240
+ "message"=>"There was an execution error",
241
+ "locations"=>[{"line"=>11, "column"=>9}],
242
+ "path"=>["dairy", "milks", 0, "executionError"]
243
+ },
244
+ {
245
+ "message"=>"There was an execution error",
246
+ "locations"=>[{"line"=>21, "column"=>7}],
247
+ "path"=>["dairy", "milks", 0, "allDairy", 3, "executionError"]
248
+ }
249
+ ]
250
+ }
251
+ assert_equal(expected_result, result)
252
+ end
253
+ end
188
254
  end
@@ -29,6 +29,32 @@ describe GraphQL::InputObjectType do
29
29
  end
30
30
  end
31
31
 
32
+ describe "validate_input with null" do
33
+ let(:schema) { GraphQL::Schema.from_definition(%|
34
+ type Query {
35
+ a: Int
36
+ }
37
+
38
+ input ExampleInputObject {
39
+ a: String
40
+ b: Int!
41
+ }
42
+ |) }
43
+ let(:input_type) { schema.types['ExampleInputObject'] }
44
+
45
+ it "returns an invalid result when value is null for non-null argument" do
46
+ invalid_input = MinimumInputObject.new({"a" => "Test", "b" => nil})
47
+ result = input_type.validate_input(invalid_input, PermissiveWarden)
48
+ assert(!result.valid?)
49
+ end
50
+
51
+ it "returns valid result when value is null for nullable argument" do
52
+ invalid_input = MinimumInputObject.new({"a" => nil, "b" => 1})
53
+ result = input_type.validate_input(invalid_input, PermissiveWarden)
54
+ assert(result.valid?)
55
+ end
56
+ end
57
+
32
58
  describe "validate_input with enumerable input" do
33
59
  describe "with good input" do
34
60
  let(:input) do
@@ -115,6 +141,61 @@ describe GraphQL::InputObjectType do
115
141
  end
116
142
  end
117
143
 
144
+ describe "coerce_result" do
145
+ it "omits unspecified arguments" do
146
+ result = input_object.coerce_result(fatContent: 0.3)
147
+ assert_equal ["fatContent"], result.keys
148
+ assert_equal 0.3, result["fatContent"]
149
+ end
150
+ end
151
+
152
+ describe "coercion of null inputs" do
153
+ let(:schema) { GraphQL::Schema.from_definition(%|
154
+ type Query {
155
+ a: Int
156
+ }
157
+
158
+ input ExampleInputObject {
159
+ a: String
160
+ b: Int!
161
+ c: String = "Default"
162
+ }
163
+ |) }
164
+ let(:input_type) { schema.types['ExampleInputObject'] }
165
+
166
+ it "null values are returned in coerced input" do
167
+ input = MinimumInputObject.new({"a" => "Test", "b" => nil,"c" => "Test"})
168
+ result = input_type.coerce_input(input)
169
+
170
+ assert_equal 'Test', result['a']
171
+
172
+ assert result.key?('b')
173
+ assert_equal nil, result['b']
174
+
175
+ assert_equal "Test", result['c']
176
+ end
177
+
178
+ it "null values are preserved when argument has a default value" do
179
+ input = MinimumInputObject.new({"a" => "Test", "b" => 1, "c" => nil})
180
+ result = input_type.coerce_input(input)
181
+
182
+ assert_equal 'Test', result['a']
183
+ assert_equal 1, result['b']
184
+
185
+ assert result.key?('c')
186
+ assert_equal nil, result['c']
187
+ end
188
+
189
+ it "omitted arguments are not returned" do
190
+ input = MinimumInputObject.new({"b" => 1, "c" => "Test"})
191
+ result = input_type.coerce_input(input)
192
+
193
+ assert !result.key?('a')
194
+ assert_equal 1, result['b']
195
+ assert_equal 'Test', result['c']
196
+ end
197
+ end
198
+
118
199
  describe "when sent into a query" do
119
200
  let(:variables) { {} }
120
201
  let(:result) { DummySchema.execute(query_string, variables: variables) }
@@ -19,19 +19,21 @@ describe GraphQL::InternalRepresentation::Rewrite do
19
19
  }
20
20
  }
21
21
  |}
22
+
22
23
  it "produces a tree of nodes" do
23
24
  op_node = rewrite_result["getCheeses"]
24
25
 
25
- assert_equal 2, op_node.children.length
26
+ root_children = op_node.typed_children[DairyAppQueryType]
27
+ assert_equal 2, root_children.length
26
28
  assert_equal DairyAppQueryType, op_node.return_type
27
- first_field = op_node.children.values.first
28
- assert_equal 3, first_field.children.length
29
- assert_equal [DairyAppQueryType], first_field.definitions.keys
29
+ first_field = root_children.values.first
30
+ assert_equal 3, first_field.typed_children[CheeseType].length
31
+ assert_equal DairyAppQueryType, first_field.owner_type
30
32
  assert_equal CheeseType, first_field.return_type
31
33
 
32
- second_field = op_node.children.values.last
33
- assert_equal 1, second_field.children.length
34
- assert_equal [DairyAppQueryType], second_field.definitions.keys
34
+ second_field = root_children.values.last
35
+ assert_equal 1, second_field.typed_children[CheeseType].length
36
+ assert_equal DairyAppQueryType.get_field("cheese"), second_field.definition
35
37
  assert_equal CheeseType, second_field.return_type
36
38
  assert second_field.inspect.is_a?(String)
37
39
  end
@@ -47,9 +49,9 @@ describe GraphQL::InternalRepresentation::Rewrite do
47
49
  |}
48
50
 
49
51
  it "gets dynamic field definitions" do
50
- cheese_field = rewrite_result[nil].children["cheese"]
51
- typename_field = cheese_field.children["typename"]
52
- assert_equal "__typename", typename_field.definitions.values.first.name
52
+ cheese_field = rewrite_result[nil].typed_children[DairyAppQueryType]["cheese"]
53
+ typename_field = cheese_field.typed_children[CheeseType]["typename"]
54
+ assert_equal "__typename", typename_field.definition.name
53
55
  assert_equal "__typename", typename_field.definition_name
54
56
  end
55
57
  end
@@ -125,22 +127,103 @@ describe GraphQL::InternalRepresentation::Rewrite do
125
127
  it "puts all fragment members as children" do
126
128
  op_node = rewrite_result[nil]
127
129
 
128
- cheese_field = op_node.children["cheese"]
129
- assert_equal ["id1", "id2", "fatContent", "origin", "similarCow", "flavor"], cheese_field.children.keys
130
+ cheese_field = op_node.typed_children[DairyAppQueryType]["cheese"]
131
+ assert_equal ["id1", "id2", "fatContent", "similarCow", "flavor"], cheese_field.typed_children[CheeseType].keys
132
+ assert_equal ["fatContent", "origin"], cheese_field.typed_children[EdibleInterface].keys
130
133
  # Merge:
131
- similar_cow_field = cheese_field.children["similarCow"]
132
- assert_equal ["similarCowSource", "fatContent", "similarCheese", "id"], similar_cow_field.children.keys
134
+ similar_cow_field = cheese_field.typed_children[CheeseType]["similarCow"]
135
+ assert_equal ["similarCowSource", "fatContent", "similarCheese", "id"], similar_cow_field.typed_children[CheeseType].keys
133
136
  # Deep merge:
134
- similar_sheep_field = similar_cow_field.children["similarCheese"]
135
- assert_equal ["flavor", "source"], similar_sheep_field.children.keys
137
+ similar_sheep_field = similar_cow_field.typed_children[CheeseType]["similarCheese"]
138
+ assert_equal ["flavor", "source"], similar_sheep_field.typed_children[CheeseType].keys
139
+
140
+ edible_origin_node = cheese_field.typed_children[EdibleInterface]["origin"]
141
+ assert_equal EdibleInterface.get_field("origin"), edible_origin_node.definition
142
+ assert_equal EdibleInterface, edible_origin_node.owner_type
143
+
144
+ edible_fat_content_node = cheese_field.typed_children[EdibleInterface]["fatContent"]
145
+ assert_equal EdibleInterface.get_field("fatContent"), edible_fat_content_node.definition
146
+ assert_equal EdibleInterface, edible_fat_content_node.owner_type
147
+
148
+ cheese_fat_content_node = cheese_field.typed_children[CheeseType]["fatContent"]
149
+ assert_equal CheeseType.get_field("fatContent"), cheese_fat_content_node.definition
150
+ assert_equal CheeseType, cheese_fat_content_node.owner_type
151
+
152
+ cheese_flavor_node = cheese_field.typed_children[CheeseType]["flavor"]
153
+ assert_equal CheeseType.get_field("flavor"), cheese_flavor_node.definition
154
+ assert_equal CheeseType, cheese_flavor_node.owner_type
136
155
 
137
- assert_equal [EdibleInterface], cheese_field.children["origin"].definitions.keys
138
- assert_equal [CheeseType, EdibleInterface], cheese_field.children["fatContent"].definitions.keys
139
- assert_equal [CheeseType], cheese_field.children["flavor"].definitions.keys
140
156
 
141
157
  # nested spread inside fragment definition:
142
- cheese_2_field = op_node.children["cheese2"].children["similarCheese"]
143
- assert_equal ["id", "fatContent"], cheese_2_field.children.keys
158
+ cheese_2_field = op_node.typed_children[DairyAppQueryType]["cheese2"].typed_children[CheeseType]["similarCheese"]
159
+ assert_equal ["id", "fatContent"], cheese_2_field.typed_children[CheeseType].keys
160
+ end
161
+ end
162
+
163
+ describe "nested fields on typed fragments" do
164
+ let(:result) { DummySchema.execute(query_string) }
165
+ let(:query_string) {%|
166
+ {
167
+ allDairy {
168
+ __typename
169
+
170
+ ... on Milk {
171
+ selfAsEdible {
172
+ milkInlineOrigin: origin
173
+ }
174
+ }
175
+
176
+ ... on Cheese {
177
+ selfAsEdible {
178
+ cheeseInlineOrigin: origin
179
+ }
180
+ }
181
+
182
+ ... on Edible {
183
+ selfAsEdible {
184
+ edibleInlineOrigin: origin
185
+ }
186
+ }
187
+
188
+ ... {
189
+ ... on Edible {
190
+ selfAsEdible {
191
+ untypedInlineOrigin: origin
192
+ }
193
+ }
194
+ }
195
+ ...milkFields
196
+ ...cheeseFields
197
+ }
198
+ }
199
+
200
+ fragment cheeseFields on Cheese {
201
+ selfAsEdible {
202
+ cheeseFragmentOrigin: origin
203
+ }
204
+ }
205
+ fragment milkFields on Milk {
206
+ selfAsEdible {
207
+ milkFragmentOrigin: origin
208
+ }
209
+ }
210
+ |}
211
+
212
+ it "distinguishes between nested fields with the same name on different typed fragments" do
213
+ all_dairy = result["data"]["allDairy"]
214
+ cheeses = all_dairy.select { |d| d["__typename"] == "Cheese" }
215
+ milks = all_dairy.select { |d| d["__typename"] == "Milk" }
216
+
217
+ # Make sure all the data is there:
218
+ assert_equal 3, cheeses.length
219
+ assert_equal 1, milks.length
220
+
221
+ cheeses.each do |cheese|
222
+ assert_equal ["cheeseInlineOrigin", "cheeseFragmentOrigin", "edibleInlineOrigin", "untypedInlineOrigin"], cheese["selfAsEdible"].keys
223
+ end
224
+ milks.each do |milk|
225
+ assert_equal ["milkInlineOrigin", "milkFragmentOrigin", "edibleInlineOrigin", "untypedInlineOrigin"], milk["selfAsEdible"].keys
226
+ end
144
227
  end
145
228
  end
146
229
  end
@@ -5,19 +5,19 @@ describe GraphQL::Introspection::InputValueType do
5
5
  let(:query_string) {%|
6
6
  {
7
7
  __type(name: "DairyProductInput") {
8
- name,
9
- description,
10
- kind,
8
+ name
9
+ description
10
+ kind
11
11
  inputFields {
12
- name,
13
- type { kind, name },
12
+ name
13
+ type { kind, name }
14
14
  defaultValue
15
15
  description
16
16
  }
17
17
  }
18
18
  }
19
19
  |}
20
- let(:result) { DummySchema.execute(query_string)}
20
+ let(:result) { DummySchema.execute(query_string) }
21
21
 
22
22
  it "exposes metadata about input objects, giving extra quotes for strings" do
23
23
  expected = { "data" => {
@@ -62,4 +62,41 @@ describe GraphQL::Introspection::InputValueType do
62
62
 
63
63
  assert_equal('["COW"]', arg['defaultValue'])
64
64
  end
65
+
66
+ it "supports null default values" do
67
+ schema = GraphQL::Schema.from_definition(%|
68
+ type Query {
69
+ hello(person: Person): String
70
+ }
71
+
72
+ input Person {
73
+ firstName: String!
74
+ lastName: String = null
75
+ }
76
+ |)
77
+
78
+ result = schema.execute(%|
79
+ {
80
+ __type(name: "Person") {
81
+ inputFields {
82
+ name
83
+ defaultValue
84
+ }
85
+ }
86
+ }
87
+ |)
88
+
89
+ expected = {
90
+ "data" => {
91
+ "__type" => {
92
+ "inputFields" => [
93
+ { "name" => "firstName", "defaultValue" => nil},
94
+ { "name" => "lastName", "defaultValue" => "null"}
95
+ ]
96
+ }
97
+ }
98
+ }
99
+
100
+ assert_equal expected, result
101
+ end
65
102
  end