gqli 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2d34056882c2b95aeff8c3c1ff8155c0ebe1018480085e3a00ab84653124fc51
4
- data.tar.gz: b32f2fd8fe6acd08587e71e36d11d7aedde72e3b30e106a10e9de017a3d9c908
3
+ metadata.gz: a2253633cb84598f7d7150f620b9161152c0c5961c92124ea2d517e97e7452b7
4
+ data.tar.gz: 84ceda416865f6295632b9044eeeeaeba29b24822a988a45e3cf8e1e6f85bfa9
5
5
  SHA512:
6
- metadata.gz: 55f5936ee3d57ca83373627034f70f081cc599358f24328017f3e97f25d7e97de247cf3ce4292ce2b68ff5435110b118d24f6273ab3001fcabeb5868f2d74733
7
- data.tar.gz: fa00c085ab0f2bc8d669425b769e0a7699fe6065024d5c6768dfa78a5f1934347bd95bcd1981689a4d9b41473739568c33f55ca6fd276fcb182547a9a3f9f226
6
+ metadata.gz: 298ecee49c25bca164942b594298fefe8ddd23d3757cac7ef3a5bd1c725aa4ee72be6ef583adf35469ff28f6a3d42bd9a7ec3ded93d76542109a35eda803ced4
7
+ data.tar.gz: bc5e10415ab97c885a816e856e5b2da73f18e3e49d6c34d81933f5bc80b6841b1be15c4596193cd270ac82bc51e2e5af4e59a99bd788ee2a692fd46f9ef5144c
@@ -9,6 +9,8 @@
9
9
  # Offense count: 3
10
10
  Metrics/AbcSize:
11
11
  Max: 20
12
+ Exclude:
13
+ - 'lib/gqli/validation.rb'
12
14
 
13
15
  # Blocks can be arbitrarily long due to GraphQL DSL syntax.
14
16
  Metrics/BlockLength:
@@ -17,6 +19,8 @@ Metrics/BlockLength:
17
19
  # Offense count: 1
18
20
  Metrics/CyclomaticComplexity:
19
21
  Max: 11
22
+ Exclude:
23
+ - 'lib/gqli/validation.rb'
20
24
 
21
25
  # Offense count: 8
22
26
  # Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
@@ -28,10 +32,14 @@ Metrics/LineLength:
28
32
  # Configuration parameters: CountComments.
29
33
  Metrics/MethodLength:
30
34
  Max: 15
35
+ Exclude:
36
+ - 'lib/gqli/validation.rb'
31
37
 
32
38
  # Offense count: 1
33
39
  Metrics/PerceivedComplexity:
34
40
  Max: 8
41
+ Exclude:
42
+ - 'lib/gqli/validation.rb'
35
43
 
36
44
  # Block delimiters are `{...}` to match GraphQL syntax more closely.
37
45
  Style/BlockDelimiters:
@@ -2,6 +2,10 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## v0.3.0
6
+ ### Added
7
+ * Refactored validations to their own `Validation` class, which now provide better error messages upon validation failure.
8
+
5
9
  ## v0.2.0
6
10
  ### Added
7
11
  * Added `__node` to be able to create nodes in case there's a name collision with a reserved keyword or a built-in method.
data/README.md CHANGED
@@ -281,7 +281,7 @@ query = GQLi::DSL.query {
281
281
 
282
282
  The helper method `__node`, can also receive arguments and have children nodes as expected from any other node declaration, for example:
283
283
 
284
- ```
284
+ ```ruby
285
285
  query = GQLi::DSL.query {
286
286
  __node('catCollection', limit: 5) {
287
287
  items {
@@ -295,7 +295,6 @@ query = GQLi::DSL.query {
295
295
 
296
296
  * Mutation queries
297
297
  * Subscription queries
298
- * Detailed validation errors
299
298
 
300
299
  ## Get involved
301
300
 
@@ -19,8 +19,7 @@ module GQLi
19
19
 
20
20
  # Adds type match node
21
21
  def __on(type_name, &block)
22
- require_relative './node'
23
- @__nodes << Node.new("... on #{type_name}", {}, __depth + 1, &block)
22
+ __node("... on #{type_name}", {}, &block)
24
23
  end
25
24
 
26
25
  # Adds children node into current node
@@ -52,8 +51,7 @@ module GQLi
52
51
  end
53
52
 
54
53
  def method_missing(name, *args, &block)
55
- require_relative './node'
56
- @__nodes << Node.new(name.to_s, __params_from_args(args), __depth + 1, &block)
54
+ __node(name.to_s, __params_from_args(args), &block)
57
55
  end
58
56
  end
59
57
  end
@@ -23,7 +23,10 @@ module GQLi
23
23
  # Executes a query
24
24
  # If validations are enabled, will perform validation check before request.
25
25
  def execute(query)
26
- fail 'Validation Error: query is invalid - HTTP Request not sent' unless valid?(query)
26
+ if validate_query
27
+ validation = schema.validate(query)
28
+ fail validation_error_message(validation) unless validation.valid?
29
+ end
27
30
 
28
31
  execute!(query)
29
32
  end
@@ -44,11 +47,20 @@ module GQLi
44
47
  def valid?(query)
45
48
  return true unless validate_query
46
49
 
47
- @schema.valid?(query)
50
+ schema.valid?(query)
48
51
  end
49
52
 
50
53
  protected
51
54
 
55
+ def validation_error_message(validation)
56
+ <<~ERROR
57
+ Validation Error: query is invalid - HTTP Request not sent.
58
+
59
+ Errors:
60
+ - #{validation.errors.join("\n - ")}
61
+ ERROR
62
+ end
63
+
52
64
  def request_headers
53
65
  {
54
66
  accept: 'application/json',
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative './dsl'
4
+ require_relative './validation'
4
5
 
5
6
  module GQLi
6
7
  # Introspection schema and validator
@@ -85,131 +86,14 @@ module GQLi
85
86
  @types = schema.types
86
87
  end
87
88
 
88
- # Returns wether the query is valid or not
89
- def valid?(query)
90
- return false unless query.is_a?(Query)
91
-
92
- query_type = types.find { |t| t.name.casecmp('query').zero? }
93
- query.__nodes.each do |node|
94
- return false unless valid_node?(query_type, node)
95
- end
96
-
97
- true
98
- end
99
-
100
- private
101
-
102
- def valid_node?(parent_type, node)
103
- return true if parent_type.kind == 'SCALAR'
104
-
105
- return valid_match_node?(parent_type, node) if node.__name.start_with?('... on')
106
-
107
- node_type = parent_type.fetch('fields', []).find { |f| f.name == node.__name }
108
- return false if node_type.nil?
109
-
110
- return false unless valid_params?(node_type, node)
111
-
112
- resolved_node_type = type_for(node_type)
113
- return false if resolved_node_type.nil?
114
-
115
- return false unless valid_nesting_node?(resolved_node_type, node)
116
-
117
- node.__nodes.all? { |n| valid_node?(resolved_node_type, n) }
118
- end
119
-
120
- def valid_match_node?(parent_type, node)
121
- return true if parent_type.fetch('possibleTypes', []).find { |t| t.name == node.__name.gsub('... on ', '') }
122
- false
123
- end
124
-
125
- def valid_params?(node_type, node)
126
- node.__params.each do |param, value|
127
- arg = node_type.fetch('args', []).find { |a| a.name == param.to_s }
128
- return false if arg.nil?
129
-
130
- arg_type = type_for(arg)
131
- return false if arg_type.nil?
132
-
133
- return false unless valid_value_for_type?(arg_type, value)
134
- end
135
-
136
- true
89
+ # Returns the evaluated validation for a query
90
+ def validate(query)
91
+ Validation.new(self, query)
137
92
  end
138
93
 
139
- def valid_nesting_node?(node_type, node)
140
- return false unless valid_object_node?(node_type, node)
141
- return false unless valid_array_node?(node_type, node)
142
- true
143
- end
144
-
145
- def valid_object_node?(node_type, node)
146
- return false if %w[OBJECT INTERFACE].include?(node_type.kind) && node.__nodes.empty?
147
- true
148
- end
149
-
150
- def valid_array_node?(node_type, node)
151
- return false if %w[OBJECT INTERFACE].include?(node_type.kind) && node.__nodes.empty?
152
- true
153
- end
154
-
155
- def valid_value_for_type?(arg_type, value)
156
- case value
157
- when ::String
158
- return false unless arg_type.name == 'String' || arg_type.name == 'ID'
159
- when ::Integer
160
- return false unless arg_type.name == 'Int'
161
- when ::Float
162
- return false unless arg_type.name == 'Float'
163
- when ::Hash
164
- return valid_hash_value?(arg_type, value)
165
- when true, false
166
- return false unless arg_type.name == 'Boolean'
167
- else
168
- return false
169
- end
170
-
171
- true
172
- end
173
-
174
- def valid_hash_value?(arg_type, value)
175
- return false unless arg_type.kind == 'INPUT_OBJECT'
176
-
177
- type = types.find { |f| f.name == arg_type.name }
178
- return false if type.nil?
179
-
180
- value.each do |k, v|
181
- input_field = type.fetch('inputFields', []).find { |f| f.name == k.to_s }
182
- return false if input_field.nil?
183
-
184
- input_field_type = type_for(input_field)
185
- return false if input_field_type.nil?
186
-
187
- return false unless valid_value_for_type?(input_field_type, v)
188
- end
189
- end
190
-
191
- def type_for(field_type)
192
- type = case field_type.type.kind
193
- when 'NON_NULL'
194
- non_null_type(field_type.type.ofType)
195
- when 'LIST'
196
- field_type.type.ofType
197
- when 'OBJECT', 'INTERFACE', 'INPUT_OBJECT'
198
- field_type.type
199
- when 'SCALAR'
200
- field_type.type
201
- end
202
-
203
- types.find { |t| t.name == type.name }
204
- end
205
-
206
- def non_null_type(non_null)
207
- case non_null.kind
208
- when 'LIST'
209
- non_null.ofType
210
- else
211
- non_null
212
- end
94
+ # Returns if the query is valid
95
+ def valid?(query)
96
+ validate(query).valid?
213
97
  end
214
98
  end
215
99
  end
@@ -0,0 +1,170 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GQLi
4
+ # Validations
5
+ class Validation
6
+ attr_reader :schema, :query, :errors
7
+
8
+ def initialize(schema, query)
9
+ @schema = schema
10
+ @query = query
11
+ @errors = []
12
+
13
+ validate
14
+ end
15
+
16
+ # Returns wether the query is valid or not
17
+ def valid?
18
+ errors.empty?
19
+ end
20
+
21
+ protected
22
+
23
+ def validate
24
+ fail 'Not a Query object' unless query.is_a?(Query)
25
+
26
+ query_type = types.find { |t| t.name.casecmp('query').zero? }
27
+ query.__nodes.each do |node|
28
+ begin
29
+ validate_node(query_type, node)
30
+ rescue StandardError => e
31
+ errors << e
32
+ end
33
+ end
34
+
35
+ true
36
+ rescue StandardError => e
37
+ errors << e
38
+ end
39
+
40
+ private
41
+
42
+ def types
43
+ schema.types
44
+ end
45
+
46
+ def validate_node(parent_type, node)
47
+ return if parent_type.kind == 'SCALAR'
48
+
49
+ return valid_match_node?(parent_type, node) if node.__name.start_with?('... on')
50
+
51
+ node_type = parent_type.fetch('fields', []).find { |f| f.name == node.__name }
52
+ fail "Node type not found for '#{node.__name}'" if node_type.nil?
53
+
54
+ validate_params(node_type, node)
55
+
56
+ resolved_node_type = type_for(node_type)
57
+ fail "Node type not found for '#{node.__name}'" if resolved_node_type.nil?
58
+
59
+ validate_nesting_node(resolved_node_type, node)
60
+
61
+ node.__nodes.each { |n| validate_node(resolved_node_type, n) }
62
+ end
63
+
64
+ def valid_match_node?(parent_type, node)
65
+ return if parent_type.fetch('possibleTypes', []).find { |t| t.name == node.__name.gsub('... on ', '') }
66
+ fail "Match type '#{node.__name.gsub('... on ', '')}' invalid"
67
+ end
68
+
69
+ def validate_params(node_type, node)
70
+ node.__params.each do |param, value|
71
+ begin
72
+ arg = node_type.fetch('args', []).find { |a| a.name == param.to_s }
73
+ fail "Invalid argument '#{param}'" if arg.nil?
74
+
75
+ arg_type = type_for(arg)
76
+ fail "Argument type not found for '#{param}'" if arg_type.nil?
77
+
78
+ validate_value_for_type(arg_type, value, param)
79
+ rescue StandardError => e
80
+ errors << e
81
+ end
82
+ end
83
+ end
84
+
85
+ def validate_nesting_node(node_type, node)
86
+ fail "Invalid object for node '#{node.__name}'" unless valid_object_node?(node_type, node)
87
+ end
88
+
89
+ def valid_object_node?(node_type, node)
90
+ return false if %w[OBJECT INTERFACE].include?(node_type.kind) && node.__nodes.empty?
91
+ true
92
+ end
93
+
94
+ def valid_array_node?(node_type, node)
95
+ return false if %w[OBJECT INTERFACE].include?(node_type.kind) && node.__nodes.empty?
96
+ true
97
+ end
98
+
99
+ def value_type_error(is_type, should_be, for_arg)
100
+ fail "Value is '#{is_type}', but should be '#{should_be}' for '#{for_arg}'"
101
+ end
102
+
103
+ def validate_value_for_type(arg_type, value, for_arg)
104
+ case value
105
+ when ::String
106
+ unless arg_type.name == 'String' || arg_type.kind == 'ENUM' || arg_type.name == 'ID'
107
+ value_type_error('String, Enum or ID', arg_type.name, for_arg)
108
+ end
109
+ if arg_type.kind == 'ENUM' && !arg_type.enumValues.map(&:name).include?(value)
110
+ fail "Invalid value for Enum '#{arg_type.name}' for '#{for_arg}'"
111
+ end
112
+ when ::Integer
113
+ value_type_error('Integer', arg_type.name, for_arg) unless arg_type.name == 'Int'
114
+ when ::Float
115
+ value_type_error('Float', arg_type.name, for_arg) unless arg_type.name == 'Float'
116
+ when ::Hash
117
+ validate_hash_value(arg_type, value, for_arg)
118
+ when true, false
119
+ value_type_error('Boolean', arg_type.name, for_arg) unless arg_type.name == 'Boolean'
120
+ else
121
+ value_type_error(value.class.name, arg_type.name, for_arg)
122
+ end
123
+ end
124
+
125
+ def validate_hash_value(arg_type, value, for_arg)
126
+ value_type_error('Object', arg_type.name, for_arg) unless arg_type.kind == 'INPUT_OBJECT'
127
+
128
+ type = types.find { |f| f.name == arg_type.name }
129
+ fail "Type not found for '#{arg_type.name}'" if type.nil?
130
+
131
+ value.each do |k, v|
132
+ begin
133
+ input_field = type.fetch('inputFields', []).find { |f| f.name == k.to_s }
134
+ fail "Input field definition not found for '#{k}'" if input_field.nil?
135
+
136
+ input_field_type = type_for(input_field)
137
+ fail "Input field type not found for '#{k}'" if input_field_type.nil?
138
+
139
+ validate_value_for_type(input_field_type, v, k)
140
+ rescue StandardError => e
141
+ errors << e
142
+ end
143
+ end
144
+ end
145
+
146
+ def type_for(field_type)
147
+ type = case field_type.type.kind
148
+ when 'NON_NULL'
149
+ non_null_type(field_type.type.ofType)
150
+ when 'LIST'
151
+ field_type.type.ofType
152
+ when 'OBJECT', 'INTERFACE', 'INPUT_OBJECT'
153
+ field_type.type
154
+ when 'SCALAR'
155
+ field_type.type
156
+ end
157
+
158
+ types.find { |t| t.name == type.name }
159
+ end
160
+
161
+ def non_null_type(non_null)
162
+ case non_null.kind
163
+ when 'LIST'
164
+ non_null.ofType
165
+ else
166
+ non_null
167
+ end
168
+ end
169
+ end
170
+ end
@@ -3,5 +3,5 @@
3
3
  # GraphQL Client and DSL library
4
4
  module GQLi
5
5
  # Gem version
6
- VERSION = '0.2.0'
6
+ VERSION = '0.3.0'
7
7
  end
@@ -49,7 +49,12 @@ describe GQLi::Client do
49
49
  dsl.query {
50
50
  foobar
51
51
  }
52
- )}.to raise_exception 'Validation Error: query is invalid - HTTP Request not sent'
52
+ )}.to raise_exception <<~ERROR
53
+ Validation Error: query is invalid - HTTP Request not sent.
54
+
55
+ Errors:
56
+ - Node type not found for 'foobar'
57
+ ERROR
53
58
  }
54
59
  end
55
60
 
@@ -54,6 +54,10 @@ describe GQLi::Introspection do
54
54
  }
55
55
 
56
56
  expect(subject.valid?(query)).to be_truthy
57
+
58
+ validation = subject.validate(query)
59
+ expect(validation.valid?).to be_truthy
60
+ expect(validation.errors).to be_empty
57
61
  end
58
62
 
59
63
  it 'wrong node returns false' do
@@ -62,6 +66,11 @@ describe GQLi::Introspection do
62
66
  }
63
67
 
64
68
  expect(subject.valid?(query)).to be_falsey
69
+
70
+ validation = subject.validate(query)
71
+ expect(validation.valid?).to be_falsey
72
+ expect(validation.errors).not_to be_empty
73
+ expect(validation.errors.map(&:to_s)).to include("Node type not found for 'foo'")
65
74
  end
66
75
 
67
76
  it 'object node that doesnt have proper values returns false' do
@@ -70,6 +79,11 @@ describe GQLi::Introspection do
70
79
  }
71
80
 
72
81
  expect(subject.valid?(query)).to be_falsey
82
+
83
+ validation = subject.validate(query)
84
+ expect(validation.valid?).to be_falsey
85
+ expect(validation.errors).not_to be_empty
86
+ expect(validation.errors.map(&:to_s)).to include("Invalid object for node 'catCollection'")
73
87
  end
74
88
 
75
89
  it 'object list node that doesnt have proper values returns false' do
@@ -80,6 +94,11 @@ describe GQLi::Introspection do
80
94
  }
81
95
 
82
96
  expect(subject.valid?(query)).to be_falsey
97
+
98
+ validation = subject.validate(query)
99
+ expect(validation.valid?).to be_falsey
100
+ expect(validation.errors).not_to be_empty
101
+ expect(validation.errors.map(&:to_s)).to include("Invalid object for node 'items'")
83
102
  end
84
103
 
85
104
  it 'type matching on invalid type returns false' do
@@ -96,6 +115,11 @@ describe GQLi::Introspection do
96
115
  }
97
116
 
98
117
  expect(subject.valid?(query)).to be_falsey
118
+
119
+ validation = subject.validate(query)
120
+ expect(validation.valid?).to be_falsey
121
+ expect(validation.errors).not_to be_empty
122
+ expect(validation.errors.map(&:to_s)).to include("Match type 'InvalidType' invalid")
99
123
  end
100
124
 
101
125
  it 'invalid arguments return false' do
@@ -108,6 +132,11 @@ describe GQLi::Introspection do
108
132
  }
109
133
 
110
134
  expect(subject.valid?(query)).to be_falsey
135
+
136
+ validation = subject.validate(query)
137
+ expect(validation.valid?).to be_falsey
138
+ expect(validation.errors).not_to be_empty
139
+ expect(validation.errors.map(&:to_s)).to include("Invalid argument 'invalidParam'")
111
140
  end
112
141
 
113
142
  it 'invalid argument type returns false' do
@@ -120,6 +149,11 @@ describe GQLi::Introspection do
120
149
  }
121
150
 
122
151
  expect(subject.valid?(query)).to be_falsey
152
+
153
+ validation = subject.validate(query)
154
+ expect(validation.valid?).to be_falsey
155
+ expect(validation.errors).not_to be_empty
156
+ expect(validation.errors.map(&:to_s)).to include("Value is 'String, Enum or ID', but should be 'Int' for 'limit'")
123
157
  end
124
158
  end
125
159
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gqli
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Contentful GmbH (David Litvak Bruno)
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-10-18 00:00:00.000000000 Z
11
+ date: 2018-10-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: http
@@ -316,6 +316,7 @@ files:
316
316
  - doc/GQLi/Node.html
317
317
  - doc/GQLi/Query.html
318
318
  - doc/GQLi/Response.html
319
+ - doc/GQLi/Validation.html
319
320
  - doc/_index.html
320
321
  - doc/class_list.html
321
322
  - doc/css/common.css
@@ -342,6 +343,7 @@ files:
342
343
  - lib/gqli/node.rb
343
344
  - lib/gqli/query.rb
344
345
  - lib/gqli/response.rb
346
+ - lib/gqli/validation.rb
345
347
  - lib/gqli/version.rb
346
348
  - spec/fixtures/vcr_cassettes/catCollection.yml
347
349
  - spec/fixtures/vcr_cassettes/client.yml