gqli 0.2.0 → 0.3.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.
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