graphql-client 0.1.2 → 0.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.
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