graphql-client 0.1.2 → 0.2.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
  SHA1:
3
- metadata.gz: 40e658148a8c85013b801639a0f476c7287db05a
4
- data.tar.gz: 47fedb450c1456d4436ec983c11c4fc4000a2c89
3
+ metadata.gz: c2383236dd4b9c7a7589212a7ec4fa057d0355cb
4
+ data.tar.gz: 0e858ee34e190846593cadec92238c406b9903dc
5
5
  SHA512:
6
- metadata.gz: 37ac81a288e27b7f7e283a5ab650908e97a4eac6d4c0c0e839f9ad11d9e6611d2b4a132383f65c81f13065adf115b8de173df252dbbe456c555caf73c72e8fa7
7
- data.tar.gz: 2249225df289e023f31be5c41a89a6b3ca8030ed7e2e1298162cb718a74b9dea9dcd7ae43921c071432ad5cc92e11d4b0829b7392c4536454b3e0311fb9652e9
6
+ metadata.gz: 4fa1b4185938e68e90082eaa6be4ce1c814503b2254aea005c44cfe636f2c2ea358fc97250ef868ad5271e4609359296eb7e18b32a9dbf697b323becea67866d
7
+ data.tar.gz: 3f3ae9c9a78dddfcc5b8758a072dd173b47d5fe5a1bfa1a853443bebcd5386250a360862ff3c6482a1a40e3671f9664215dbbf7f46351323fdc3401a98ce109a
@@ -2,10 +2,10 @@ require "active_support/inflector"
2
2
  require "active_support/notifications"
3
3
  require "graphql"
4
4
  require "graphql/client/error"
5
+ require "graphql/client/errors"
5
6
  require "graphql/client/query_result"
6
7
  require "graphql/client/response"
7
8
  require "graphql/language/nodes/deep_freeze_ext"
8
- require "graphql/language/operation_slice"
9
9
  require "json"
10
10
 
11
11
  module GraphQL
@@ -203,7 +203,7 @@ module GraphQL
203
203
  definitions = {}
204
204
  doc.definitions.each do |node|
205
205
  node.name = nil if node.name == "__anonymous__"
206
- sliced_document = Language::OperationSlice.slice(document_dependencies, node.name)
206
+ sliced_document = Language::DefinitionSlice.slice(document_dependencies, node.name)
207
207
  definition = Definition.for(node: node, document: sliced_document)
208
208
  definitions[node.name] = definition
209
209
  end
@@ -265,7 +265,16 @@ module GraphQL
265
265
  )
266
266
  end
267
267
 
268
- Response.for(definition, result)
268
+ data, errors, extensions = result.values_at("data", "errors", "extensions")
269
+
270
+ errors ||= []
271
+ GraphQL::Client::Errors.normalize_error_paths(data, errors)
272
+
273
+ Response.new(
274
+ data: definition.new(data, Errors.new(errors, ["data"])),
275
+ errors: Errors.new(errors),
276
+ extensions: extensions
277
+ )
269
278
  end
270
279
 
271
280
  # Internal: FragmentSpread and FragmentDefinition extension to allow its
@@ -0,0 +1,200 @@
1
+ require "graphql/client/hash_with_indifferent_access"
2
+
3
+ module GraphQL
4
+ class Client
5
+ # Public: Collection of errors associated with GraphQL object type.
6
+ #
7
+ # Inspired by ActiveModel::Errors.
8
+ class Errors
9
+ include Enumerable
10
+
11
+ # Internal: Normalize GraphQL Error "path" ensuring the path exists.
12
+ #
13
+ # Records "normalizedPath" value to error object.
14
+ #
15
+ # data - Hash of response data
16
+ # errors - Array of error Hashes
17
+ #
18
+ # Returns nothing.
19
+ def self.normalize_error_paths(data = nil, errors = [])
20
+ errors.each do |error|
21
+ path = ["data"]
22
+ current = data
23
+ error.fetch("path", []).each do |key|
24
+ break unless current
25
+ path << key
26
+ current = current[key]
27
+ end
28
+ error["normalizedPath"] = path
29
+ end
30
+ errors
31
+ end
32
+
33
+ # Internal: Initalize from collection of errors.
34
+ #
35
+ # errors - Array of GraphQL Hash error objects
36
+ # path - Array of String|Integer fields to data
37
+ # all - Boolean flag if all nested errors should be available
38
+ def initialize(errors = [], path = [], all = false)
39
+ @ast_path = path
40
+ @all = all
41
+ @raw_errors = errors
42
+ end
43
+
44
+ # Public: Return collection of all nested errors.
45
+ #
46
+ # data.errors[:node]
47
+ # data.errors.all[:node]
48
+ #
49
+ # Returns Errors collection.
50
+ def all
51
+ if @all
52
+ self
53
+ else
54
+ self.class.new(@raw_errors, @ast_path, true)
55
+ end
56
+ end
57
+
58
+ # Internal: Return collection of errors for a given subfield.
59
+ #
60
+ # data.errors.filter_by_path("node")
61
+ #
62
+ # Returns Errors collection.
63
+ def filter_by_path(field)
64
+ self.class.new(@raw_errors, @ast_path + [field], @all)
65
+ end
66
+
67
+ # Public: Access Hash of error messages.
68
+ #
69
+ # data.errors.messages["node"]
70
+ # data.errors.messages[:node]
71
+ #
72
+ # Returns HashWithIndifferentAccess.
73
+ def messages
74
+ return @messages if defined? @messages
75
+
76
+ messages = {}
77
+
78
+ details.each do |field, errors|
79
+ messages[field] ||= []
80
+ errors.each do |error|
81
+ messages[field] << error.fetch("message")
82
+ end
83
+ end
84
+
85
+ @messages = HashWithIndifferentAccess.new(messages)
86
+ end
87
+
88
+ # Public: Access Hash of error objects.
89
+ #
90
+ # data.errors.details["node"]
91
+ # data.errors.details[:node]
92
+ #
93
+ # Returns HashWithIndifferentAccess.
94
+ def details
95
+ return @details if defined? @details
96
+
97
+ details = {}
98
+
99
+ @raw_errors.each do |error|
100
+ path = error.fetch("normalizedPath", [])
101
+ matched_path = @all ? path[0, @ast_path.length] : path[0...-1]
102
+ next unless @ast_path == matched_path
103
+
104
+ field = path[@ast_path.length]
105
+ next unless field
106
+
107
+ details[field] ||= []
108
+ details[field] << error
109
+ end
110
+
111
+ @details = HashWithIndifferentAccess.new(details)
112
+ end
113
+
114
+ # Public: When passed a symbol or a name of a field, returns an array of
115
+ # errors for the method.
116
+ #
117
+ # data.errors[:node] # => ["couldn't find node by id"]
118
+ # data.errors['node'] # => ["couldn't find node by id"]
119
+ #
120
+ # Returns Array of errors.
121
+ def [](key)
122
+ messages.fetch(key, [])
123
+ end
124
+
125
+ # Public: Iterates through each error key, value pair in the error
126
+ # messages hash. Yields the field and the error for that attribute. If the
127
+ # field has more than one error message, yields once for each error
128
+ # message.
129
+ def each
130
+ return enum_for(:each) unless block_given?
131
+ messages.keys.each do |field|
132
+ messages[field].each { |error| yield field, error }
133
+ end
134
+ end
135
+
136
+ # Public: Check if there are any errors on a given field.
137
+ #
138
+ # data.errors.messages # => {"node"=>["couldn't find node by id", "unauthorized"]}
139
+ # data.errors.include?("node") # => true
140
+ # data.errors.include?("version") # => false
141
+ #
142
+ # Returns true if the error messages include an error for the given field,
143
+ # otherwise false.
144
+ def include?(field)
145
+ self[field].any?
146
+ end
147
+ alias has_key? include?
148
+ alias key? include?
149
+
150
+ # Public: Count the number of errors on object.
151
+ #
152
+ # data.errors.messages # => {"node"=>["couldn't find node by id", "unauthorized"]}
153
+ # data.errors.size # => 2
154
+ #
155
+ # Returns the number of error messages.
156
+ def size
157
+ values.flatten.size
158
+ end
159
+ alias count size
160
+
161
+ # Public: Check if there are no errors on object.
162
+ #
163
+ # data.errors.messages # => {"node"=>["couldn't find node by id"]}
164
+ # data.errors.empty? # => false
165
+ #
166
+ # Returns true if no errors are found, otherwise false.
167
+ def empty?
168
+ size.zero?
169
+ end
170
+ alias blank? empty?
171
+
172
+ # Public: Returns all message keys.
173
+ #
174
+ # data.errors.messages # => {"node"=>["couldn't find node by id"]}
175
+ # data.errors.values # => ["node"]
176
+ #
177
+ # Returns Array of String field names.
178
+ def keys
179
+ messages.keys
180
+ end
181
+
182
+ # Public: Returns all message values.
183
+ #
184
+ # data.errors.messages # => {"node"=>["couldn't find node by id"]}
185
+ # data.errors.values # => [["couldn't find node by id"]]
186
+ #
187
+ # Returns Array of Array String messages.
188
+ def values
189
+ messages.values
190
+ end
191
+
192
+ # Public: Display console friendly representation of errors collection.
193
+ #
194
+ # Returns String.
195
+ def inspect
196
+ "#<#{self.class} @messages=#{messages.inspect} @details=#{details.inspect}>"
197
+ end
198
+ end
199
+ end
200
+ end
@@ -0,0 +1,57 @@
1
+ require "active_support/inflector"
2
+
3
+ module GraphQL
4
+ class Client
5
+ # Public: Implements a read only hash where keys can be accessed by
6
+ # strings, symbols, snake or camel case.
7
+ #
8
+ # Also see ActiveSupport::HashWithIndifferentAccess.
9
+ class HashWithIndifferentAccess
10
+ extend Forwardable
11
+ include Enumerable
12
+
13
+ def initialize(hash = {})
14
+ @hash = hash
15
+ @aliases = {}
16
+
17
+ hash.keys.each do |key|
18
+ if key.is_a?(String)
19
+ key_alias = ActiveSupport::Inflector.underscore(key)
20
+ @aliases[key_alias] = key if key != key_alias
21
+ end
22
+ end
23
+
24
+ freeze
25
+ end
26
+
27
+ def_delegators :@hash, :each, :empty?, :inspect, :keys, :length, :size, :to_h, :to_hash, :values
28
+
29
+ def [](key)
30
+ @hash[convert_value(key)]
31
+ end
32
+
33
+ def fetch(key, *args, &block)
34
+ @hash.fetch(convert_value(key), *args, &block)
35
+ end
36
+
37
+ def key?(key)
38
+ @hash.key?(convert_value(key))
39
+ end
40
+ alias include? key?
41
+ alias has_key? key?
42
+ alias member? key?
43
+
44
+ private
45
+
46
+ def convert_value(key)
47
+ case key
48
+ when String, Symbol
49
+ key = key.to_s
50
+ @aliases.fetch(key, key)
51
+ else
52
+ key
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,19 @@
1
+ require "graphql/client/errors"
2
+
3
+ module GraphQL
4
+ class Client
5
+ # Public: Array wrapper for value returned from GraphQL List.
6
+ class List < Array
7
+ def initialize(values, errors = Errors.new)
8
+ super(values)
9
+ @errors = errors
10
+ freeze
11
+ end
12
+
13
+ # Public: Return errors associated with list of data.
14
+ #
15
+ # Returns Errors collection.
16
+ attr_reader :errors
17
+ end
18
+ end
19
+ end
@@ -1,5 +1,7 @@
1
1
  require "active_support/inflector"
2
2
  require "graphql"
3
+ require "graphql/client/errors"
4
+ require "graphql/client/list"
3
5
  require "set"
4
6
 
5
7
  module GraphQL
@@ -58,18 +60,18 @@ module GraphQL
58
60
 
59
61
  next unless field == "edges"
60
62
  class_eval <<-RUBY, __FILE__, __LINE__
61
- def each_node
62
- return enum_for(:each_node) unless block_given?
63
- edges.each { |edge| yield edge.node }
64
- self
65
- end
63
+ def each_node
64
+ return enum_for(:each_node) unless block_given?
65
+ edges.each { |edge| yield edge.node }
66
+ self
67
+ end
66
68
  RUBY
67
69
  end
68
70
 
69
71
  assigns = fields.map do |field, type|
70
72
  if type
71
73
  <<-RUBY
72
- @#{field} = self.class.fields[:#{field}].cast(@data["#{field}"])
74
+ @#{field} = self.class.fields[:#{field}].cast(@data["#{field}"], @errors.filter_by_path("#{field}"))
73
75
  RUBY
74
76
  else
75
77
  <<-RUBY
@@ -79,8 +81,10 @@ module GraphQL
79
81
  end
80
82
 
81
83
  class_eval <<-RUBY, __FILE__, __LINE__
82
- def initialize(data)
84
+ def initialize(data, errors = Errors.new)
83
85
  @data = data
86
+ @errors = errors
87
+
84
88
  #{assigns.join("\n")}
85
89
  freeze
86
90
  end
@@ -106,10 +110,10 @@ module GraphQL
106
110
  "#<#{name} fields=#{@fields.keys.inspect}>"
107
111
  end
108
112
 
109
- def self.cast(obj)
113
+ def self.cast(obj, errors = Errors.new)
110
114
  case obj
111
115
  when Hash
112
- new(obj)
116
+ new(obj, errors)
113
117
  when self
114
118
  return obj
115
119
  when QueryResult
@@ -121,9 +125,9 @@ module GraphQL
121
125
  message << GraphQL::Language::Generation.generate(obj.class.source_node).sub(/\n}$/, "#{suggestion}\n}")
122
126
  raise TypeError, message
123
127
  end
124
- cast(obj.to_h)
128
+ cast(obj.to_h, obj.errors)
125
129
  when Array
126
- obj.map { |e| cast(e) }
130
+ List.new(obj.each_with_index.map { |e, idx| cast(e, errors.filter_by_path(idx)) }, errors)
127
131
  when NilClass
128
132
  nil
129
133
  else
@@ -145,12 +149,12 @@ module GraphQL
145
149
  end
146
150
  end
147
151
 
148
- def self.new(obj)
152
+ def self.new(obj, *args)
149
153
  case obj
150
154
  when Hash
151
155
  super
152
156
  else
153
- cast(obj)
157
+ cast(obj, *args)
154
158
  end
155
159
  end
156
160
 
@@ -167,6 +171,11 @@ module GraphQL
167
171
  define(name: self.name, source_node: source_node, fields: new_fields)
168
172
  end
169
173
 
174
+ # Public: Return errors associated with data.
175
+ #
176
+ # Returns Errors collection.
177
+ attr_reader :errors
178
+
170
179
  attr_reader :data
171
180
  alias to_h data
172
181
 
@@ -1,4 +1,4 @@
1
- require "graphql/client/error"
1
+ require "graphql/client/errors"
2
2
 
3
3
  module GraphQL
4
4
  class Client
@@ -6,45 +6,6 @@ module GraphQL
6
6
  #
7
7
  # https://facebook.github.io/graphql/#sec-Response-Format
8
8
  class Response
9
- # Internal: Initialize Response subclass.
10
- def self.for(definition, result)
11
- data, errors, extensions = result.values_at("data", "errors", "extensions")
12
-
13
- if data && errors
14
- PartialResponse.new(
15
- data: definition.new(data),
16
- errors: ResponseErrors.new(definition, errors),
17
- extensions: extensions
18
- )
19
- elsif data && !errors
20
- SuccessfulResponse.new(
21
- data: definition.new(data),
22
- extensions: extensions
23
- )
24
- elsif !data && errors
25
- FailedResponse.new(
26
- errors: ResponseErrors.new(definition, errors),
27
- extensions: extensions
28
- )
29
- else
30
- FailedResponse.new(
31
- errors: ResponseErrors.new(definition, [{ "message" => "invalid GraphQL response" }])
32
- )
33
- end
34
- end
35
-
36
- # Public: Hash of server specific extension metadata.
37
- attr_reader :extensions
38
-
39
- # Internal: Initialize base class.
40
- def initialize(extensions: nil)
41
- @extensions = extensions || {}
42
- end
43
- end
44
-
45
- # Public: A successful response means the query executed without any errors
46
- # and returned all the requested data.
47
- class SuccessfulResponse < Response
48
9
  # Public: Wrapped QueryResult of data returned from the server.
49
10
  #
50
11
  # https://facebook.github.io/graphql/#sec-Data
@@ -52,89 +13,21 @@ module GraphQL
52
13
  # Returns instance of QueryResult subclass.
53
14
  attr_reader :data
54
15
 
55
- # Internal: Initialize SuccessfulResponse.
56
- def initialize(data:, **kargs)
57
- @data = data
58
- super(**kargs)
59
- end
60
- end
61
-
62
- # Public: A partial response means the query executed with some errors but
63
- # returned all non-nullable fields. PartialResponse is still considered a
64
- # SuccessfulResponse as it returns data and the client may still proceed
65
- # with its normal render flow.
66
- class PartialResponse < SuccessfulResponse
67
16
  # Public: Get partial failures from response.
68
17
  #
69
18
  # https://facebook.github.io/graphql/#sec-Errors
70
19
  #
71
- # Returns ResponseErrors collection object with zero or more errors.
20
+ # Returns Errors collection object with zero or more errors.
72
21
  attr_reader :errors
73
22
 
74
- # Internal: Initialize PartialResponse.
75
- def initialize(errors:, **kargs)
76
- @errors = errors
77
- super(**kargs)
78
- end
79
- end
80
-
81
- # Public: A failed response returns no data and at least one error message.
82
- # Cases may likely be a query validation error, missing authorization,
83
- # or internal server crash.
84
- class FailedResponse < Response
85
- # Public: Get errors from response.
86
- #
87
- # https://facebook.github.io/graphql/#sec-Errors
88
- #
89
- # Returns ResponseErrors collection object with one or more errors.
90
- attr_reader :errors
23
+ # Public: Hash of server specific extension metadata.
24
+ attr_reader :extensions
91
25
 
92
- # Internal: Initialize FailedResponse.
93
- def initialize(errors:, **kargs)
26
+ # Internal: Initialize base class.
27
+ def initialize(data: nil, errors: Errors.new, extensions: {})
28
+ @data = data
94
29
  @errors = errors
95
- super(**kargs)
96
- end
97
- end
98
-
99
- # Public: An error received from the server on execution.
100
- #
101
- # Extends StandardError hierarchy so you may raise this instance.
102
- #
103
- # Examples
104
- #
105
- # raise response.errors.first
106
- #
107
- class ResponseError < Error
108
- # Internal: Initialize ResponseError.
109
- def initialize(definition, error)
110
- @request_definition = definition
111
- @locations = error["locations"]
112
- super error["message"]
113
- end
114
- end
115
-
116
- # Public: A collection of errors received from the server on execution.
117
- #
118
- # Extends StandardError hierarchy so you may raise this instance.
119
- #
120
- # Examples
121
- #
122
- # raise response.errors
123
- #
124
- class ResponseErrors < Error
125
- include Enumerable
126
-
127
- attr_reader :errors
128
-
129
- # Internal: Initialize ResponseErrors.
130
- def initialize(definition, errors)
131
- @request_definition = definition
132
- @errors = errors.map { |error| ResponseError.new(definition, error) }
133
- super @errors.map(&:message).join(", ")
134
- end
135
-
136
- def each(&block)
137
- errors.each(&block)
30
+ @extensions = extensions
138
31
  end
139
32
  end
140
33
  end
@@ -12,6 +12,12 @@ module GraphQL
12
12
  self.class.child_attributes.each do |attr_name|
13
13
  public_send(attr_name).freeze.each(&:deep_freeze)
14
14
  end
15
+
16
+ self.class.scalar_attributes.each do |attr_name|
17
+ object = public_send(attr_name)
18
+ object.freeze if object
19
+ end
20
+
15
21
  freeze
16
22
  end
17
23
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: graphql-client
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - GitHub
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-10-07 00:00:00.000000000 Z
11
+ date: 2016-10-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -36,20 +36,20 @@ dependencies:
36
36
  requirements:
37
37
  - - "~>"
38
38
  - !ruby/object:Gem::Version
39
- version: '0.18'
39
+ version: '0.19'
40
40
  - - ">="
41
41
  - !ruby/object:Gem::Version
42
- version: 0.18.11
42
+ version: 0.19.2
43
43
  type: :runtime
44
44
  prerelease: false
45
45
  version_requirements: !ruby/object:Gem::Requirement
46
46
  requirements:
47
47
  - - "~>"
48
48
  - !ruby/object:Gem::Version
49
- version: '0.18'
49
+ version: '0.19'
50
50
  - - ">="
51
51
  - !ruby/object:Gem::Version
52
- version: 0.18.11
52
+ version: 0.19.2
53
53
  - !ruby/object:Gem::Dependency
54
54
  name: actionpack
55
55
  requirement: !ruby/object:Gem::Requirement
@@ -122,15 +122,17 @@ files:
122
122
  - README.md
123
123
  - lib/graphql/client.rb
124
124
  - lib/graphql/client/error.rb
125
+ - lib/graphql/client/errors.rb
125
126
  - lib/graphql/client/erubis.rb
127
+ - lib/graphql/client/hash_with_indifferent_access.rb
126
128
  - lib/graphql/client/http.rb
129
+ - lib/graphql/client/list.rb
127
130
  - lib/graphql/client/log_subscriber.rb
128
131
  - lib/graphql/client/query_result.rb
129
132
  - lib/graphql/client/railtie.rb
130
133
  - lib/graphql/client/response.rb
131
134
  - lib/graphql/client/view_module.rb
132
135
  - lib/graphql/language/nodes/deep_freeze_ext.rb
133
- - lib/graphql/language/operation_slice.rb
134
136
  homepage: https://github.com/github/graphql-client
135
137
  licenses:
136
138
  - MIT
@@ -1,44 +0,0 @@
1
- require "graphql"
2
-
3
- module GraphQL
4
- module Language
5
- # Public: Document transformations to return a minimal document to represent
6
- # operation.
7
- module OperationSlice
8
- # Public: Return's minimal document to represent operation.
9
- #
10
- # Find's target operation and any fragment dependencies and returns a
11
- # new document with just those definitions.
12
- #
13
- # document - The Nodes::Document to find definitions.
14
- # operation_name - The String name of Nodes::OperationDefinition
15
- #
16
- # Returns new Nodes::Document.
17
- def self.slice(document, operation_name)
18
- seen = Set.new([operation_name])
19
- stack = [operation_name]
20
-
21
- until stack.empty?
22
- name = stack.pop
23
- names = find_definition_fragment_spreads(document, name)
24
- seen.merge(names)
25
- stack.concat(names.to_a)
26
- end
27
-
28
- Nodes::Document.new(definitions: document.definitions.select { |node| seen.include?(node.name) })
29
- end
30
-
31
- def self.find_definition_fragment_spreads(document, definition_name)
32
- definition = document.definitions.find { |node| node.name == definition_name }
33
- raise "missing definition: #{definition_name}" unless definition
34
- spreads = Set.new
35
- visitor = Visitor.new(definition)
36
- visitor[Nodes::FragmentSpread].enter << -> (node, _parent) do
37
- spreads << node.name
38
- end
39
- visitor.visit
40
- spreads
41
- end
42
- end
43
- end
44
- end