graphql 0.18.14 → 0.18.15

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/lib/graphql/analysis/query_complexity.rb +15 -6
  3. data/lib/graphql/analysis/query_depth.rb +11 -10
  4. data/lib/graphql/directive.rb +2 -6
  5. data/lib/graphql/directive/include_directive.rb +0 -4
  6. data/lib/graphql/directive/skip_directive.rb +0 -4
  7. data/lib/graphql/execution/directive_checks.rb +22 -13
  8. data/lib/graphql/execution_error.rb +7 -0
  9. data/lib/graphql/internal_representation/node.rb +20 -2
  10. data/lib/graphql/internal_representation/rewrite.rb +66 -20
  11. data/lib/graphql/language/generation.rb +22 -8
  12. data/lib/graphql/language/nodes.rb +48 -20
  13. data/lib/graphql/language/parser.rb +436 -423
  14. data/lib/graphql/language/parser.y +22 -19
  15. data/lib/graphql/language/parser_tests.rb +131 -2
  16. data/lib/graphql/query/serial_execution/field_resolution.rb +1 -0
  17. data/lib/graphql/query/serial_execution/selection_resolution.rb +1 -1
  18. data/lib/graphql/query/serial_execution/value_resolution.rb +4 -1
  19. data/lib/graphql/schema/printer.rb +1 -1
  20. data/lib/graphql/static_validation/message.rb +1 -1
  21. data/lib/graphql/version.rb +1 -1
  22. data/readme.md +10 -12
  23. data/spec/graphql/directive_spec.rb +139 -1
  24. data/spec/graphql/execution_error_spec.rb +63 -3
  25. data/spec/graphql/introspection/type_type_spec.rb +2 -0
  26. data/spec/graphql/language/generation_spec.rb +55 -7
  27. data/spec/graphql/query/executor_spec.rb +4 -2
  28. data/spec/graphql/schema/catchall_middleware_spec.rb +1 -0
  29. data/spec/graphql/schema/printer_spec.rb +1 -1
  30. data/spec/graphql/schema/timeout_middleware_spec.rb +10 -5
  31. data/spec/graphql/static_validation/rules/argument_literals_are_compatible_spec.rb +6 -6
  32. data/spec/graphql/static_validation/rules/arguments_are_defined_spec.rb +4 -4
  33. data/spec/graphql/static_validation/rules/directives_are_defined_spec.rb +2 -2
  34. data/spec/graphql/static_validation/rules/directives_are_in_valid_locations_spec.rb +2 -2
  35. data/spec/graphql/static_validation/rules/fields_are_defined_on_type_spec.rb +3 -3
  36. data/spec/graphql/static_validation/rules/fields_have_appropriate_selections_spec.rb +2 -2
  37. data/spec/graphql/static_validation/rules/fragment_spreads_are_possible_spec.rb +3 -3
  38. data/spec/graphql/static_validation/rules/fragment_types_exist_spec.rb +2 -2
  39. data/spec/graphql/static_validation/rules/fragments_are_finite_spec.rb +2 -2
  40. data/spec/graphql/static_validation/rules/fragments_are_named_spec.rb +1 -1
  41. data/spec/graphql/static_validation/rules/fragments_are_on_composite_types_spec.rb +3 -3
  42. data/spec/graphql/static_validation/rules/fragments_are_used_spec.rb +2 -2
  43. data/spec/graphql/static_validation/rules/mutation_root_exists_spec.rb +1 -1
  44. data/spec/graphql/static_validation/rules/required_arguments_are_present_spec.rb +3 -3
  45. data/spec/graphql/static_validation/rules/subscription_root_exists_spec.rb +1 -1
  46. data/spec/graphql/static_validation/rules/variable_default_values_are_correctly_typed_spec.rb +4 -4
  47. data/spec/graphql/static_validation/rules/variable_usages_are_allowed_spec.rb +4 -4
  48. data/spec/graphql/static_validation/rules/variables_are_input_types_spec.rb +3 -3
  49. data/spec/graphql/static_validation/rules/variables_are_used_and_defined_spec.rb +3 -3
  50. data/spec/graphql/static_validation/validator_spec.rb +1 -1
  51. data/spec/support/dairy_app.rb +8 -0
  52. metadata +30 -2
@@ -18,6 +18,28 @@ describe GraphQL::ExecutionError do
18
18
  }
19
19
  flavor
20
20
  }
21
+ allDairy {
22
+ ... on Cheese {
23
+ flavor
24
+ }
25
+ ... on Milk {
26
+ source
27
+ executionError
28
+ }
29
+ }
30
+ dairy {
31
+ milks {
32
+ source
33
+ executionError
34
+ allDairy {
35
+ __typename
36
+ ... on Milk {
37
+ origin
38
+ executionError
39
+ }
40
+ }
41
+ }
42
+ }
21
43
  executionError
22
44
  }
23
45
 
@@ -38,20 +60,58 @@ describe GraphQL::ExecutionError do
38
60
  },
39
61
  "flavor" => "Brie",
40
62
  },
63
+ "allDairy" => [
64
+ { "flavor" => "Brie" },
65
+ { "flavor" => "Gouda" },
66
+ { "flavor" => "Manchego" },
67
+ { "source" => "COW", "executionError" => nil }
68
+ ],
69
+ "dairy" => {
70
+ "milks" => [
71
+ {
72
+ "source" => "COW",
73
+ "executionError" => nil,
74
+ "allDairy" => [
75
+ { "__typename" => "Cheese" },
76
+ { "__typename" => "Cheese" },
77
+ { "__typename" => "Cheese" },
78
+ { "__typename" => "Milk", "origin" => "Antiquity", "executionError" => nil }
79
+ ]
80
+ }
81
+ ]
82
+ },
41
83
  "executionError" => nil,
42
84
  },
43
85
  "errors"=>[
44
86
  {
45
87
  "message"=>"No cheeses are made from Yak milk!",
46
- "locations"=>[{"line"=>5, "column"=>9}]
88
+ "locations"=>[{"line"=>5, "column"=>9}],
89
+ "path"=>["cheese", "error1"]
47
90
  },
48
91
  {
49
92
  "message"=>"No cheeses are made from Yak milk!",
50
- "locations"=>[{"line"=>8, "column"=>9}]
93
+ "locations"=>[{"line"=>8, "column"=>9}],
94
+ "path"=>["cheese", "error2"]
95
+ },
96
+ {
97
+ "message"=>"There was an execution error",
98
+ "locations"=>[{"line"=>22, "column"=>11}],
99
+ "path"=>["allDairy", 3, "executionError"]
100
+ },
101
+ {
102
+ "message"=>"There was an execution error",
103
+ "locations"=>[{"line"=>28, "column"=>11}],
104
+ "path"=>["dairy", "milks", 0, "executionError"]
105
+ },
106
+ {
107
+ "message"=>"There was an execution error",
108
+ "locations"=>[{"line"=>33, "column"=>15}],
109
+ "path"=>["dairy", "milks", 0, "allDairy", 3, "executionError"]
51
110
  },
52
111
  {
53
112
  "message"=>"There was an execution error",
54
- "locations"=>[{"line"=>16, "column"=>7}]
113
+ "locations"=>[{"line"=>38, "column"=>7}],
114
+ "path"=>["executionError"]
55
115
  },
56
116
  ]
57
117
  }
@@ -41,6 +41,8 @@ describe GraphQL::Introspection::TypeType do
41
41
  {"name"=>"LocalProduct"},
42
42
  ],
43
43
  "fields"=>[
44
+ {"type"=>{"name"=>"List", "ofType"=>{"name"=>"DairyProduct"}}},
45
+ {"type"=>{"name"=>"String", "ofType"=>nil}},
44
46
  {"type"=>{"name"=>"Non-Null", "ofType"=>{"name"=>"Float"}}},
45
47
  {"type"=>{"name"=>"List", "ofType"=>{"name"=>"String"}}},
46
48
  {"type"=>{"name"=>"Non-Null", "ofType"=>{"name"=>"ID"}}},
@@ -10,7 +10,7 @@ describe GraphQL::Language::Generation do
10
10
  ...moreNestedFields @skip(if: $skipNested)
11
11
  }
12
12
  ... on OtherType @include(unless: false) {
13
- field(arg: [{ key: "value", anotherKey: 0.9, anotherAnotherKey: WHATEVER }])
13
+ field(arg: [{key: "value", anotherKey: 0.9, anotherAnotherKey: WHATEVER}])
14
14
  anotherField
15
15
  }
16
16
  ... {
@@ -31,7 +31,7 @@ describe GraphQL::Language::Generation do
31
31
  describe "inputs" do
32
32
  let(:query_string) {%|
33
33
  query {
34
- field(int: 3, float: 4.7e-24, bool: false, string: "☀︎🏆\\n escaped \\" unicode ¶ /", enum: ENUM_NAME, array: [7, 8, 9], object: { a: [1, 2, 3], b: { c: "4" } }, unicode_bom: "\xef\xbb\xbfquery")
34
+ field(int: 3, float: 4.7e-24, bool: false, string: "☀︎🏆\\n escaped \\" unicode ¶ /", enum: ENUM_NAME, array: [7, 8, 9], object: {a: [1, 2, 3], b: {c: "4"}}, unicode_bom: "\xef\xbb\xbfquery")
35
35
  }
36
36
  |}
37
37
 
@@ -41,15 +41,63 @@ describe GraphQL::Language::Generation do
41
41
  end
42
42
 
43
43
  describe "schema" do
44
- let(:query_string) {%|
44
+ # From: https://github.com/graphql/graphql-js/blob/a725499b155285c2e33647a93393c82689b20b0f/src/language/__tests__/schema-kitchen-sink.graphql
45
+ let(:query_string) {<<-schema
45
46
  schema {
46
- query: Query
47
+ query: QueryType
48
+ mutation: MutationType
47
49
  }
48
50
 
49
- type Query {
50
- field: String!
51
+ type Foo implements Bar {
52
+ one: Type
53
+ two(argument: InputType!): Type
54
+ three(argument: InputType, other: String): Int
55
+ four(argument: String = "string"): String
56
+ five(argument: [String] = ["string", "string"]): String
57
+ six(argument: InputType = {key: "value"}): Type
51
58
  }
52
- |}
59
+
60
+ type AnnotatedObject @onObject(arg: "value") {
61
+ annotatedField(arg: Type = "default" @onArg): Type @onField
62
+ }
63
+
64
+ interface Bar {
65
+ one: Type
66
+ four(argument: String = "string"): String
67
+ }
68
+
69
+ interface AnnotatedInterface @onInterface {
70
+ annotatedField(arg: Type @onArg): Type @onField
71
+ }
72
+
73
+ union Feed = Story | Article | Advert
74
+
75
+ union AnnotatedUnion @onUnion = A | B
76
+
77
+ scalar CustomScalar
78
+
79
+ scalar AnnotatedScalar @onScalar
80
+
81
+ enum Site {
82
+ DESKTOP
83
+ MOBILE
84
+ }
85
+
86
+ enum AnnotatedEnum @onEnum {
87
+ ANNOTATED_VALUE @onEnumValue
88
+ OTHER_VALUE
89
+ }
90
+
91
+ input InputType {
92
+ key: String!
93
+ answer: Int = 42
94
+ }
95
+
96
+ input AnnotatedInput @onInputObjectType {
97
+ annotatedField: Type @onField
98
+ }
99
+ schema
100
+ }
53
101
 
54
102
  it "generate" do
55
103
  assert_equal query_string.gsub(/^ /, "").strip, document.to_query_string
@@ -143,7 +143,8 @@ describe GraphQL::Query::Executor do
143
143
  "errors" => [
144
144
  {
145
145
  "message" => "BOOM",
146
- "locations" => [ { "line" => 1, "column" => 28 } ]
146
+ "locations" => [ { "line" => 1, "column" => 28 } ],
147
+ "path" => ["noMilk", "cow", "cantBeNullButRaisesExecutionError"]
147
148
  }
148
149
  ]
149
150
  }
@@ -167,7 +168,8 @@ describe GraphQL::Query::Executor do
167
168
  "errors"=>[
168
169
  {
169
170
  "message"=>"Error was handled!",
170
- "locations" => [{"line"=>1, "column"=>17}]
171
+ "locations" => [{"line"=>1, "column"=>17}],
172
+ "path"=>["noMilk", "error"]
171
173
  }
172
174
  ]
173
175
  }
@@ -22,6 +22,7 @@ describe GraphQL::Schema::CatchallMiddleware do
22
22
  {
23
23
  "message"=>"Internal error",
24
24
  "locations"=>[{"line"=>1, "column"=>17}],
25
+ "path"=>["noMilk", "error"]
25
26
  },
26
27
  ]
27
28
  }
@@ -176,7 +176,7 @@ type Post {
176
176
  }
177
177
 
178
178
  type Query {
179
- post(id: ID!, varied: Varied = { id: \"123\", int: 234, float: 2.3, enum: FOO, sub: [{ string: \"str\" }] }): Post
179
+ post(id: ID!, varied: Varied = {id: \"123\", int: 234, float: 2.3, enum: FOO, sub: [{string: \"str\"}]}): Post
180
180
  }
181
181
 
182
182
  input Sub {
@@ -64,11 +64,13 @@ describe GraphQL::Schema::TimeoutMiddleware do
64
64
  expected_errors = [
65
65
  {
66
66
  "message"=>"Timeout on Query.sleepFor",
67
- "locations"=>[{"line"=>6, "column"=>9}]
67
+ "locations"=>[{"line"=>6, "column"=>9}],
68
+ "path"=>["d"]
68
69
  },
69
70
  {
70
71
  "message"=>"Timeout on Query.sleepFor",
71
- "locations"=>[{"line"=>7, "column"=>9}]
72
+ "locations"=>[{"line"=>7, "column"=>9}],
73
+ "path"=>["e"]
72
74
  },
73
75
  ]
74
76
  assert_equal expected_data, result["data"]
@@ -116,11 +118,13 @@ describe GraphQL::Schema::TimeoutMiddleware do
116
118
  expected_errors = [
117
119
  {
118
120
  "message"=>"Timeout on NestedSleep.seconds",
119
- "locations"=>[{"line"=>10, "column"=>15}]
121
+ "locations"=>[{"line"=>10, "column"=>15}],
122
+ "path"=>["a", "b", "c", "d", "seconds"]
120
123
  },
121
124
  {
122
125
  "message"=>"Timeout on NestedSleep.nestedSleep",
123
- "locations"=>[{"line"=>11, "column"=>15}]
126
+ "locations"=>[{"line"=>11, "column"=>15}],
127
+ "path"=>["a", "b", "c", "d", "e"]
124
128
  },
125
129
  ]
126
130
 
@@ -149,7 +153,8 @@ describe GraphQL::Schema::TimeoutMiddleware do
149
153
  expected_errors = [
150
154
  {
151
155
  "message"=>"Timeout on Query.sleepFor",
152
- "locations"=>[{"line"=>6, "column"=>9}]
156
+ "locations"=>[{"line"=>6, "column"=>9}],
157
+ "path"=>["d"]
153
158
  },
154
159
  ]
155
160
 
@@ -27,42 +27,42 @@ describe GraphQL::StaticValidation::ArgumentLiteralsAreCompatible do
27
27
  query_root_error = {
28
28
  "message"=>"Argument 'id' on Field 'cheese' has an invalid value. Expected type 'Int!'.",
29
29
  "locations"=>[{"line"=>3, "column"=>7}],
30
- "path"=>["query getCheese", "cheese", "id"],
30
+ "fields"=>["query getCheese", "cheese", "id"],
31
31
  }
32
32
  assert_includes(errors, query_root_error)
33
33
 
34
34
  directive_error = {
35
35
  "message"=>"Argument 'if' on Directive 'skip' has an invalid value. Expected type 'Boolean!'.",
36
36
  "locations"=>[{"line"=>4, "column"=>30}],
37
- "path"=>["query getCheese", "cheese", "source", "if"],
37
+ "fields"=>["query getCheese", "cheese", "source", "if"],
38
38
  }
39
39
  assert_includes(errors, directive_error)
40
40
 
41
41
  input_object_error = {
42
42
  "message"=>"Argument 'product' on Field 'badSource' has an invalid value. Expected type '[DairyProductInput]'.",
43
43
  "locations"=>[{"line"=>6, "column"=>7}],
44
- "path"=>["query getCheese", "badSource", "product"],
44
+ "fields"=>["query getCheese", "badSource", "product"],
45
45
  }
46
46
  assert_includes(errors, input_object_error)
47
47
 
48
48
  input_object_field_error = {
49
49
  "message"=>"Argument 'source' on InputObject 'DairyProductInput' has an invalid value. Expected type 'DairyAnimal!'.",
50
50
  "locations"=>[{"line"=>6, "column"=>40}],
51
- "path"=>["query getCheese", "badSource", "product", "source"],
51
+ "fields"=>["query getCheese", "badSource", "product", "source"],
52
52
  }
53
53
  assert_includes(errors, input_object_field_error)
54
54
 
55
55
  missing_required_field_error = {
56
56
  "message"=>"Argument 'product' on Field 'missingSource' has an invalid value. Expected type '[DairyProductInput]'.",
57
57
  "locations"=>[{"line"=>7, "column"=>7}],
58
- "path"=>["query getCheese", "missingSource", "product"],
58
+ "fields"=>["query getCheese", "missingSource", "product"],
59
59
  }
60
60
  assert_includes(errors, missing_required_field_error)
61
61
 
62
62
  fragment_error = {
63
63
  "message"=>"Argument 'source' on Field 'similarCheese' has an invalid value. Expected type '[DairyAnimal!]!'.",
64
64
  "locations"=>[{"line"=>13, "column"=>7}],
65
- "path"=>["fragment cheeseFields", "similarCheese", "source"],
65
+ "fields"=>["fragment cheeseFields", "similarCheese", "source"],
66
66
  }
67
67
  assert_includes(errors, fragment_error)
68
68
  end
@@ -24,28 +24,28 @@ describe GraphQL::StaticValidation::ArgumentsAreDefined do
24
24
  query_root_error = {
25
25
  "message"=>"Field 'cheese' doesn't accept argument 'silly'",
26
26
  "locations"=>[{"line"=>4, "column"=>7}],
27
- "path"=>["query getCheese", "cheese", "silly"],
27
+ "fields"=>["query getCheese", "cheese", "silly"],
28
28
  }
29
29
  assert_includes(errors, query_root_error)
30
30
 
31
31
  input_obj_record = {
32
32
  "message"=>"InputObject 'DairyProductInput' doesn't accept argument 'wacky'",
33
33
  "locations"=>[{"line"=>5, "column"=>29}],
34
- "path"=>["query getCheese", "searchDairy", "product", "wacky"],
34
+ "fields"=>["query getCheese", "searchDairy", "product", "wacky"],
35
35
  }
36
36
  assert_includes(errors, input_obj_record)
37
37
 
38
38
  fragment_error = {
39
39
  "message"=>"Field 'similarCheese' doesn't accept argument 'nonsense'",
40
40
  "locations"=>[{"line"=>9, "column"=>7}],
41
- "path"=>["fragment cheeseFields", "similarCheese", "nonsense"],
41
+ "fields"=>["fragment cheeseFields", "similarCheese", "nonsense"],
42
42
  }
43
43
  assert_includes(errors, fragment_error)
44
44
 
45
45
  directive_error = {
46
46
  "message"=>"Directive 'skip' doesn't accept argument 'something'",
47
47
  "locations"=>[{"line"=>10, "column"=>10}],
48
- "path"=>["fragment cheeseFields", "id", "something"],
48
+ "fields"=>["fragment cheeseFields", "id", "something"],
49
49
  }
50
50
  assert_includes(errors, directive_error)
51
51
  end
@@ -23,11 +23,11 @@ describe GraphQL::StaticValidation::DirectivesAreDefined do
23
23
  {
24
24
  "message"=>"Directive @nonsense is not defined",
25
25
  "locations"=>[{"line"=>5, "column"=>16}],
26
- "path"=>["query getCheese", "okCheese", "source"],
26
+ "fields"=>["query getCheese", "okCheese", "source"],
27
27
  }, {
28
28
  "message"=>"Directive @moreNonsense is not defined",
29
29
  "locations"=>[{"line"=>7, "column"=>18}],
30
- "path"=>["query getCheese", "okCheese", "... on Cheese", "flavor"],
30
+ "fields"=>["query getCheese", "okCheese", "... on Cheese", "flavor"],
31
31
  }
32
32
  ]
33
33
  assert_equal(expected, errors)
@@ -27,12 +27,12 @@ describe GraphQL::StaticValidation::DirectivesAreInValidLocations do
27
27
  {
28
28
  "message"=> "'@skip' can't be applied to queries (allowed: fields, fragment spreads, inline fragments)",
29
29
  "locations"=>[{"line"=>2, "column"=>21}],
30
- "path"=>["query getCheese"],
30
+ "fields"=>["query getCheese"],
31
31
  },
32
32
  {
33
33
  "message"=>"'@skip' can't be applied to fragment definitions (allowed: fields, fragment spreads, inline fragments)",
34
34
  "locations"=>[{"line"=>12, "column"=>33}],
35
- "path"=>["fragment whatever"],
35
+ "fields"=>["fragment whatever"],
36
36
  },
37
37
  ]
38
38
  assert_equal(expected, errors)
@@ -34,7 +34,7 @@ describe GraphQL::StaticValidation::FieldsAreDefinedOnType do
34
34
  {
35
35
  "message"=>"Field 'notDefinedField' doesn't exist on type 'Query'",
36
36
  "locations"=>[{"line"=>1, "column"=>18}],
37
- "path"=>["query getStuff", "notDefinedField"],
37
+ "fields"=>["query getStuff", "notDefinedField"],
38
38
  }
39
39
  ]
40
40
  assert_equal(expected_errors, errors)
@@ -49,7 +49,7 @@ describe GraphQL::StaticValidation::FieldsAreDefinedOnType do
49
49
  {
50
50
  "message"=>"Field 'amountThatILikeIt' doesn't exist on type 'Edible'",
51
51
  "locations"=>[{"line"=>1, "column"=>35}],
52
- "path"=>["query getStuff", "favoriteEdible", "amountThatILikeIt"],
52
+ "fields"=>["query getStuff", "favoriteEdible", "amountThatILikeIt"],
53
53
  }
54
54
  ]
55
55
  assert_equal(expected_errors, errors)
@@ -71,7 +71,7 @@ describe GraphQL::StaticValidation::FieldsAreDefinedOnType do
71
71
  "locations"=>[
72
72
  {"line"=>3, "column"=>7}
73
73
  ],
74
- "path"=>["fragment dbFields", "source"],
74
+ "fields"=>["fragment dbFields", "source"],
75
75
  }
76
76
  ]
77
77
  assert_equal(expected_errors, errors)
@@ -19,14 +19,14 @@ describe GraphQL::StaticValidation::FieldsHaveAppropriateSelections do
19
19
  illegal_selection_error = {
20
20
  "message"=>"Selections can't be made on scalars (field 'id' returns Int but has selections [something, someFields])",
21
21
  "locations"=>[{"line"=>5, "column"=>47}],
22
- "path"=>["query getCheese", "illegalSelectionCheese", "id"],
22
+ "fields"=>["query getCheese", "illegalSelectionCheese", "id"],
23
23
  }
24
24
  assert_includes(errors, illegal_selection_error, "finds illegal selections on scalarss")
25
25
 
26
26
  selection_required_error = {
27
27
  "message"=>"Objects must have selections (field 'cheese' returns Cheese but has no selections)",
28
28
  "locations"=>[{"line"=>4, "column"=>7}],
29
- "path"=>["query getCheese", "missingFieldsCheese"],
29
+ "fields"=>["query getCheese", "missingFieldsCheese"],
30
30
  }
31
31
  assert_includes(errors, selection_required_error, "finds objects without selections")
32
32
  end
@@ -32,17 +32,17 @@ describe GraphQL::StaticValidation::FragmentSpreadsArePossible do
32
32
  {
33
33
  "message"=>"Fragment on Milk can't be spread inside Cheese",
34
34
  "locations"=>[{"line"=>6, "column"=>9}],
35
- "path"=>["query getCheese", "cheese", "... on Milk"],
35
+ "fields"=>["query getCheese", "cheese", "... on Milk"],
36
36
  },
37
37
  {
38
38
  "message"=>"Fragment milkFields on Milk can't be spread inside Cheese",
39
39
  "locations"=>[{"line"=>4, "column"=>9}],
40
- "path"=>["query getCheese", "cheese", "... milkFields"],
40
+ "fields"=>["query getCheese", "cheese", "... milkFields"],
41
41
  },
42
42
  {
43
43
  "message"=>"Fragment milkFields on Milk can't be spread inside Cheese",
44
44
  "locations"=>[{"line"=>18, "column"=>7}],
45
- "path"=>["fragment cheeseFields", "... milkFields"],
45
+ "fields"=>["fragment cheeseFields", "... milkFields"],
46
46
  }
47
47
  ]
48
48
  assert_equal(expected, errors)