graphql-client 0.12.3 → 0.15.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
- SHA1:
3
- metadata.gz: 532760d69d3380633bfa7e7555697aa843b72fa7
4
- data.tar.gz: 947e81aeea7eb9058c01b844425e4035a955a003
2
+ SHA256:
3
+ metadata.gz: 1f7ced5b5ef5fb8e496d8c221867165c4629670a5f8ecf1e44f8c523e5774ae2
4
+ data.tar.gz: 49453ec41a714dca5774c32b24139527beb7623a8c99a2368e789f9b1443c79a
5
5
  SHA512:
6
- metadata.gz: 34f989f928690406e82004b449264785395ffc00c809e70784b53284889b8c4409f1bbc3226b1ae7ac62df69b1ca8d753871f44b2545746e2b775c0a0bc263e8
7
- data.tar.gz: 7f354451883ab22eefea03dea49cfe1bc848b8a6419ad9f626881a7dece1ec6922b3154a89561d5aa8ada72e9d919ecb24c529b4d13669c28ec63a7ab47d166e
6
+ metadata.gz: b69902cbed392a6ad77d5e124db8196439b17750ecb369fc3189a93593857f5e1cf80ade820edb5abc0e11cade056832c733726e2e4202207496d7435eb6ebfa
7
+ data.tar.gz: a918d43558b2f75ce3627a7dd003ba76e083b9c087f15c36e4df7e571d5eef71a4760fe929a7d6d93eb78a22b4f310c8ef75f9737a719a516c659eb70117026e
data/README.md CHANGED
@@ -1,21 +1,30 @@
1
- # graphql-client
1
+ # graphql-client [![Gem Version](https://badge.fury.io/rb/graphql-client.svg)](https://badge.fury.io/rb/graphql-client) [![Build Status](https://travis-ci.org/github/graphql-client.svg?branch=master)](https://travis-ci.org/github/graphql-client)
2
2
 
3
3
  GraphQL Client is a Ruby library for declaring, composing and executing GraphQL queries.
4
4
 
5
5
  ## Usage
6
6
 
7
+ ### Installation
8
+
9
+ Add `graphql-client` to your Gemfile and then run `bundle install`.
10
+
11
+ ```ruby
12
+ # Gemfile
13
+ gem 'graphql-client'
14
+ ```
15
+
7
16
  ### Configuration
8
17
 
9
18
  Sample configuration for a GraphQL Client to query from the [SWAPI GraphQL Wrapper](https://github.com/graphql/swapi-graphql).
10
19
 
11
- ``` ruby
20
+ ```ruby
12
21
  require "graphql/client"
13
22
  require "graphql/client/http"
14
23
 
15
24
  # Star Wars API example wrapper
16
25
  module SWAPI
17
26
  # Configure GraphQL endpoint using the basic HTTP network adapter.
18
- HTTP = GraphQL::Client::HTTP.new("http://graphql-swapi.parseapp.com/") do
27
+ HTTP = GraphQL::Client::HTTP.new("https://example.com/graphql") do
19
28
  def headers(context)
20
29
  # Optionally set any HTTP headers
21
30
  { "User-Agent": "My Client" }
@@ -42,7 +51,7 @@ If you haven't already, [familiarize yourself with the GraphQL query syntax](htt
42
51
 
43
52
  This client library encourages all GraphQL queries to be declared statically and assigned to a Ruby constant.
44
53
 
45
- ``` ruby
54
+ ```ruby
46
55
  HeroNameQuery = SWAPI::Client.parse <<-'GRAPHQL'
47
56
  query {
48
57
  hero {
@@ -51,6 +60,7 @@ HeroNameQuery = SWAPI::Client.parse <<-'GRAPHQL'
51
60
  }
52
61
  GRAPHQL
53
62
  ```
63
+
54
64
  Queries can reference variables that are passed in at query execution time.
55
65
 
56
66
  ```ruby
@@ -65,7 +75,7 @@ GRAPHQL
65
75
 
66
76
  Fragments are declared similarly.
67
77
 
68
- ``` ruby
78
+ ```ruby
69
79
  HumanFragment = SWAPI::Client.parse <<-'GRAPHQL'
70
80
  fragment on Human {
71
81
  name
@@ -76,7 +86,7 @@ GRAPHQL
76
86
 
77
87
  To include a fragment in a query, reference the fragment by constant.
78
88
 
79
- ``` ruby
89
+ ```ruby
80
90
  HeroNameQuery = SWAPI::Client.parse <<-'GRAPHQL'
81
91
  {
82
92
  luke: human(id: "1000") {
@@ -91,7 +101,7 @@ GRAPHQL
91
101
 
92
102
  This works for namespaced constants.
93
103
 
94
- ``` ruby
104
+ ```ruby
95
105
  module Hero
96
106
  Query = SWAPI::Client.parse <<-'GRAPHQL'
97
107
  {
@@ -112,7 +122,7 @@ end
112
122
 
113
123
  Pass the reference of a parsed query definition to `GraphQL::Client#query`. Data is returned back in a wrapped `GraphQL::Client::Schema::ObjectType` struct that provides Ruby-ish accessors.
114
124
 
115
- ``` ruby
125
+ ```ruby
116
126
  result = SWAPI::Client.query(Hero::Query)
117
127
 
118
128
  # The raw data is Hash of JSON values
@@ -121,18 +131,20 @@ result = SWAPI::Client.query(Hero::Query)
121
131
  # The wrapped result allows to you access data with Ruby methods
122
132
  result.data.luke.home_planet
123
133
  ```
124
- `GraphQL::Client#query` also accepts variables and context parameters that can be leveraged by the underlying network executor.
125
134
 
126
- ``` ruby
135
+ `GraphQL::Client#query` also accepts variables and context parameters that can be leveraged by the underlying network executor.
136
+
137
+ ```ruby
127
138
  result = SWAPI::Client.query(Hero::HeroFromEpisodeQuery, variables: {episode: "JEDI"}, context: {user_id: current_user_id})
128
139
  ```
140
+
129
141
  ### Rails ERB integration
130
142
 
131
143
  If you're using Ruby on Rails ERB templates, theres a ERB extension that allows static queries to be defined in the template itself.
132
144
 
133
145
  In standard Ruby you can simply assign queries and fragments to constants and they'll be available throughout the app. However, the contents of an ERB template is compiled into a Ruby method, and methods can't assign constants. So a new ERB tag was extended to declare static sections that include a GraphQL query.
134
146
 
135
- ``` erb
147
+ ```erb
136
148
  <%# app/views/humans/human.html.erb %>
137
149
  <%graphql
138
150
  fragment HumanFragment on Human {
@@ -161,7 +173,7 @@ These `<%graphql` sections are simply ignored at runtime but make their definiti
161
173
 
162
174
  Add `graphql-client` to your app's Gemfile:
163
175
 
164
- ``` ruby
176
+ ```ruby
165
177
  gem 'graphql-client'
166
178
  ```
167
179
 
@@ -14,39 +14,68 @@ module GraphQL
14
14
  #
15
15
  # Definitions MUST be assigned to a constant.
16
16
  class Definition < Module
17
- def self.for(irep_node:, **kargs)
18
- case irep_node.ast_node
17
+ def self.for(ast_node:, **kargs)
18
+ case ast_node
19
19
  when Language::Nodes::OperationDefinition
20
- OperationDefinition.new(irep_node: irep_node, **kargs)
20
+ OperationDefinition.new(ast_node: ast_node, **kargs)
21
21
  when Language::Nodes::FragmentDefinition
22
- FragmentDefinition.new(irep_node: irep_node, **kargs)
22
+ FragmentDefinition.new(ast_node: ast_node, **kargs)
23
23
  else
24
- raise TypeError, "expected node to be a definition type, but was #{irep_node.ast_node.class}"
24
+ raise TypeError, "expected node to be a definition type, but was #{ast_node.class}"
25
25
  end
26
26
  end
27
27
 
28
- def initialize(client:, document:, irep_node:, source_location:)
28
+ def initialize(client:, document:, source_document:, ast_node:, source_location:)
29
29
  @client = client
30
30
  @document = document
31
- @definition_node = irep_node.ast_node
31
+ @source_document = source_document
32
+ @definition_node = ast_node
32
33
  @source_location = source_location
33
- @schema_class = client.types.define_class(self, irep_node, irep_node.return_type)
34
+
35
+ definition_type = case ast_node
36
+ when GraphQL::Language::Nodes::OperationDefinition
37
+ case ast_node.operation_type
38
+ when "mutation"
39
+ @client.schema.mutation
40
+ when "subscription"
41
+ @client.schema.subscription
42
+ when "query", nil
43
+ @client.schema.query
44
+ else
45
+ raise "Unexpected operation_type: #{ast_node.operation_type}"
46
+ end
47
+ when GraphQL::Language::Nodes::FragmentDefinition
48
+ @client.schema.types[ast_node.type.name]
49
+ else
50
+ raise "Unexpected ast_node: #{ast_node}"
51
+ end
52
+
53
+ @schema_class = client.types.define_class(self, [ast_node], definition_type)
54
+
55
+ # Clear cache only needed during initialization
56
+ @indexes = nil
34
57
  end
35
58
 
36
59
  # Internal: Get associated owner GraphQL::Client instance.
37
60
  attr_reader :client
38
61
 
39
- # Internal root schema class for defintion. Returns
62
+ # Internal root schema class for definition. Returns
40
63
  # GraphQL::Client::Schema::ObjectType or
41
64
  # GraphQL::Client::Schema::PossibleTypes.
42
65
  attr_reader :schema_class
43
66
 
44
- # Internal: Get underlying operation or fragment defintion AST node for
67
+ # Internal: Get underlying operation or fragment definition AST node for
45
68
  # definition.
46
69
  #
47
70
  # Returns OperationDefinition or FragmentDefinition object.
48
71
  attr_reader :definition_node
49
72
 
73
+ # Internal: Get original document that created this definition, without
74
+ # any additional dependencies.
75
+ #
76
+ # Returns GraphQL::Language::Nodes::Document.
77
+ attr_reader :source_document
78
+
50
79
  # Public: Global name of definition in client document.
51
80
  #
52
81
  # Returns a GraphQL safe name of the Ruby constant String.
@@ -82,9 +111,9 @@ module GraphQL
82
111
  when GraphQL::Client::Schema::PossibleTypes
83
112
  case obj
84
113
  when NilClass
85
- nil
114
+ obj
86
115
  else
87
- schema_class.cast(obj.to_h, obj.errors)
116
+ cast_object(obj)
88
117
  end
89
118
  when GraphQL::Client::Schema::ObjectType
90
119
  case obj
@@ -93,14 +122,7 @@ module GraphQL
93
122
  when Hash
94
123
  schema_class.new(obj, errors)
95
124
  else
96
- if obj.class.is_a?(GraphQL::Client::Schema::ObjectType)
97
- unless obj.class._spreads.include?(definition_node.name)
98
- raise TypeError, "#{definition_node.name} is not included in #{obj.class.source_definition.name}"
99
- end
100
- schema_class.cast(obj.to_h, obj.errors)
101
- else
102
- raise TypeError, "unexpected #{obj.class}"
103
- end
125
+ cast_object(obj)
104
126
  end
105
127
  else
106
128
  raise TypeError, "unexpected #{schema_class}"
@@ -119,9 +141,26 @@ module GraphQL
119
141
  end
120
142
 
121
143
  private
144
+
145
+ def cast_object(obj)
146
+ if obj.class.is_a?(GraphQL::Client::Schema::ObjectType)
147
+ unless obj.class._spreads.include?(definition_node.name)
148
+ raise TypeError, "#{definition_node.name} is not included in #{obj.class.source_definition.name}"
149
+ end
150
+ schema_class.cast(obj.to_h, obj.errors)
151
+ else
152
+ raise TypeError, "unexpected #{obj.class}"
153
+ end
154
+ end
155
+
156
+ EMPTY_SET = Set.new.freeze
157
+
122
158
  def index_spreads(visitor)
123
159
  spreads = {}
124
- on_node = ->(node, _parent) { spreads[node] = Set.new(flatten_spreads(node).map(&:name)) }
160
+ on_node = ->(node, _parent) do
161
+ node_spreads = flatten_spreads(node).map(&:name)
162
+ spreads[node] = node_spreads.empty? ? EMPTY_SET : Set.new(node_spreads).freeze
163
+ end
125
164
 
126
165
  visitor[GraphQL::Language::Nodes::Field] << on_node
127
166
  visitor[GraphQL::Language::Nodes::FragmentDefinition] << on_node
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+ require "action_view"
3
+
4
+ module GraphQL
5
+ class Client
6
+ begin
7
+ require "action_view/template/handlers/erb/erubi"
8
+ rescue LoadError
9
+ require "graphql/client/erubis_enhancer"
10
+
11
+ # Public: Extended Erubis implementation that supports GraphQL static
12
+ # query sections.
13
+ #
14
+ # <%graphql
15
+ # query GetVersion {
16
+ # version
17
+ # }
18
+ # %>
19
+ # <%= data.version %>
20
+ #
21
+ # Configure ActionView's default ERB implementation to use this class.
22
+ #
23
+ # ActionView::Template::Handlers::ERB.erb_implementation = GraphQL::Client::Erubis
24
+ #
25
+ class ERB < ActionView::Template::Handlers::Erubis
26
+ include ErubisEnhancer
27
+ end
28
+ else
29
+ require "graphql/client/erubi_enhancer"
30
+
31
+ # Public: Extended Erubis implementation that supports GraphQL static
32
+ # query sections.
33
+ #
34
+ # <%graphql
35
+ # query GetVerison {
36
+ # version
37
+ # }
38
+ # %>
39
+ # <%= data.version %>
40
+ #
41
+ # Configure ActionView's default ERB implementation to use this class.
42
+ #
43
+ # ActionView::Template::Handlers::ERB.erb_implementation = GraphQL::Client::Erubi
44
+ #
45
+ class ERB < ActionView::Template::Handlers::ERB::Erubi
46
+ include ErubiEnhancer
47
+ end
48
+ end
49
+ end
50
+ end
@@ -1,30 +1,8 @@
1
1
  # frozen_string_literal: true
2
- require "action_view"
3
- require "graphql/client/erubis_enhancer"
2
+ require "graphql/client/erb"
4
3
 
5
4
  module GraphQL
6
5
  class Client
7
- # Ignore deprecation errors loading AV Erubis
8
- ActiveSupport::Deprecation.silence do
9
- ActionView::Template::Handlers::Erubis
10
- end
11
-
12
- # Public: Extended Erubis implementation that supports GraphQL static
13
- # query sections.
14
- #
15
- # <%graphql
16
- # query GetVersion {
17
- # version
18
- # }
19
- # %>
20
- # <%= data.version %>
21
- #
22
- # Configure ActionView's default ERB implementation to use this class.
23
- #
24
- # ActionView::Template::Handlers::ERB.erb_implementation = GraphQL::Client::Erubis
25
- #
26
- class Erubis < ActionView::Template::Handlers::Erubis
27
- include ErubisEnhancer
28
- end
6
+ Erubis = GraphQL::Client::ERB
29
7
  end
30
8
  end
@@ -13,33 +13,84 @@ module GraphQL
13
13
  # document - GraphQL::Language::Nodes::Document to modify
14
14
  # schema - Optional Map of GraphQL::Language::Nodes::Node to GraphQL::Type
15
15
  #
16
- # Returns nothing.
17
- def self.insert_typename_fields(document, types: {})
18
- on_selections = ->(node, _parent) do
19
- type = types[node]
20
-
21
- if node.selections.any?
22
- case type && type.unwrap
23
- when NilClass, GraphQL::InterfaceType, GraphQL::UnionType
24
- names = node_flatten_selections(node.selections).map { |s| s.respond_to?(:name) ? s.name : nil }
16
+ # Returns the document with `__typename` added to it
17
+ if GraphQL::Language::Nodes::AbstractNode.method_defined?(:merge)
18
+ # GraphQL 1.9 introduces a new visitor class
19
+ # and doesn't expose writer methods for node attributes.
20
+ # So, use the node mutation API instead.
21
+ class InsertTypenameVisitor < GraphQL::Language::Visitor
22
+ def initialize(document, types:)
23
+ @types = types
24
+ super(document)
25
+ end
26
+
27
+ def add_typename(node, parent)
28
+ type = @types[node]
29
+ type = type && type.unwrap
30
+
31
+ if (node.selections.any? && (type.nil? || type.is_a?(GraphQL::InterfaceType) || type.is_a?(GraphQL::UnionType))) ||
32
+ (node.selections.none? && type.is_a?(GraphQL::ObjectType))
33
+ names = QueryTypename.node_flatten_selections(node.selections).map { |s| s.respond_to?(:name) ? s.name : nil }
25
34
  names = Set.new(names.compact)
26
35
 
27
- unless names.include?("__typename")
28
- node.selections = [GraphQL::Language::Nodes::Field.new(name: "__typename")] + node.selections
36
+ if names.include?("__typename")
37
+ yield(node, parent)
38
+ else
39
+ node_with_typename = node.merge(selections: [GraphQL::Language::Nodes::Field.new(name: "__typename")] + node.selections)
40
+ yield(node_with_typename, parent)
29
41
  end
42
+ else
43
+ yield(node, parent)
30
44
  end
31
- elsif type && type.unwrap.is_a?(GraphQL::ObjectType)
32
- node.selections = [GraphQL::Language::Nodes::Field.new(name: "__typename")]
45
+ end
46
+
47
+ def on_operation_definition(node, parent)
48
+ add_typename(node, parent) { |n, p| super(n, p) }
49
+ end
50
+
51
+ def on_field(node, parent)
52
+ add_typename(node, parent) { |n, p| super(n, p) }
53
+ end
54
+
55
+ def on_fragment_definition(node, parent)
56
+ add_typename(node, parent) { |n, p| super(n, p) }
33
57
  end
34
58
  end
35
59
 
36
- visitor = GraphQL::Language::Visitor.new(document)
37
- visitor[GraphQL::Language::Nodes::Field].leave << on_selections
38
- visitor[GraphQL::Language::Nodes::FragmentDefinition].leave << on_selections
39
- visitor[GraphQL::Language::Nodes::OperationDefinition].leave << on_selections
40
- visitor.visit
60
+ def self.insert_typename_fields(document, types: {})
61
+ visitor = InsertTypenameVisitor.new(document, types: types)
62
+ visitor.visit
63
+ visitor.result
64
+ end
65
+
66
+ else
67
+ def self.insert_typename_fields(document, types: {})
68
+ on_selections = ->(node, _parent) do
69
+ type = types[node]
70
+
71
+ if node.selections.any?
72
+ case type && type.unwrap
73
+ when NilClass, GraphQL::InterfaceType, GraphQL::UnionType
74
+ names = node_flatten_selections(node.selections).map { |s| s.respond_to?(:name) ? s.name : nil }
75
+ names = Set.new(names.compact)
41
76
 
42
- nil
77
+ unless names.include?("__typename")
78
+ node.selections = [GraphQL::Language::Nodes::Field.new(name: "__typename")] + node.selections
79
+ end
80
+ end
81
+ elsif type && type.unwrap.is_a?(GraphQL::ObjectType)
82
+ node.selections = [GraphQL::Language::Nodes::Field.new(name: "__typename")]
83
+ end
84
+ end
85
+
86
+ visitor = GraphQL::Language::Visitor.new(document)
87
+ visitor[GraphQL::Language::Nodes::Field].leave << on_selections
88
+ visitor[GraphQL::Language::Nodes::FragmentDefinition].leave << on_selections
89
+ visitor[GraphQL::Language::Nodes::OperationDefinition].leave << on_selections
90
+ visitor.visit
91
+
92
+ document
93
+ end
43
94
  end
44
95
 
45
96
  def self.node_flatten_selections(selections)
@@ -22,8 +22,8 @@ module GraphQL
22
22
  end
23
23
 
24
24
  initializer "graphql.configure_erb_implementation" do |_app|
25
- require "graphql/client/erubis"
26
- ActionView::Template::Handlers::ERB.erb_implementation = GraphQL::Client::Erubis
25
+ require "graphql/client/erb"
26
+ ActionView::Template::Handlers::ERB.erb_implementation = GraphQL::Client::ERB
27
27
  end
28
28
 
29
29
  initializer "graphql.configure_views_namespace" do |app|
@@ -9,6 +9,34 @@ module GraphQL
9
9
  class EnumType < Module
10
10
  include BaseType
11
11
 
12
+ class EnumValue < String
13
+ def initialize(obj, enum_value, enum)
14
+ super(obj)
15
+ @enum_value = enum_value
16
+ @enum = enum
17
+ end
18
+
19
+ def respond_to_missing?(method_name, include_private = false)
20
+ if method_name[-1] == "?" && @enum.include?(method_name[0..-2])
21
+ true
22
+ else
23
+ super
24
+ end
25
+ end
26
+
27
+ def method_missing(method_name, *args)
28
+ if method_name[-1] == "?"
29
+ queried_value = method_name[0..-2]
30
+ if @enum.include?(queried_value)
31
+ raise ArgumentError, "wrong number of arguments (given #{args.size}, expected 0)" unless args.empty?
32
+ return @enum_value == queried_value
33
+ end
34
+ end
35
+
36
+ super
37
+ end
38
+ end
39
+
12
40
  # Internal: Construct enum wrapper from another GraphQL::EnumType.
13
41
  #
14
42
  # type - GraphQL::EnumType instance
@@ -21,22 +49,18 @@ module GraphQL
21
49
  @values = {}
22
50
 
23
51
  all_values = type.values.keys
52
+ comparison_set = all_values.map { |s| -s.downcase }.to_set
24
53
 
25
54
  all_values.each do |value|
26
- str = value.dup
27
- all_values.each do |v|
28
- str.define_singleton_method("#{v.downcase}?") { false }
29
- end
30
- str.define_singleton_method("#{value.downcase}?") { true }
31
- str.freeze
55
+ str = EnumValue.new(-value, -value.downcase, comparison_set).freeze
32
56
  const_set(value, str) if value =~ /^[A-Z]/
33
- @values[str] = str
57
+ @values[str.to_s] = str
34
58
  end
35
59
 
36
60
  @values.freeze
37
61
  end
38
62
 
39
- def define_class(definition, irep_node)
63
+ def define_class(definition, ast_nodes)
40
64
  self
41
65
  end
42
66
 
@@ -20,10 +20,12 @@ module GraphQL
20
20
  PossibleTypes.new(type, types)
21
21
  end
22
22
 
23
- def define_class(definition, irep_node)
24
- new(irep_node.typed_children.keys.map { |ctype|
25
- schema_module.get_class(ctype.name).define_class(definition, irep_node)
26
- })
23
+ def define_class(definition, ast_nodes)
24
+ possible_type_names = definition.client.schema.possible_types(type).map(&:graphql_name)
25
+ possible_types = possible_type_names.map { |concrete_type_name|
26
+ schema_module.get_class(concrete_type_name).define_class(definition, ast_nodes)
27
+ }
28
+ new(possible_types)
27
29
  end
28
30
  end
29
31
  end
@@ -1,5 +1,4 @@
1
1
  # frozen_string_literal: true
2
-
3
2
  require "active_support/inflector"
4
3
  require "graphql/client/error"
5
4
  require "graphql/client/errors"
@@ -20,34 +19,83 @@ module GraphQL
20
19
  end
21
20
  end
22
21
 
23
- def define_class(definition, irep_node)
24
- fields = irep_node.typed_children[type].inject({}) { |h, (field_name, field_irep_node)|
25
- if definition.indexes[:definitions][field_irep_node.ast_node] == definition.definition_node
26
- h[field_name.to_sym] = schema_module.define_class(definition, field_irep_node, field_irep_node.definition.type)
22
+ def define_class(definition, ast_nodes)
23
+ # First, gather all the ast nodes representing a certain selection, by name.
24
+ # We gather AST nodes into arrays so that multiple selections can be grouped, for example:
25
+ #
26
+ # {
27
+ # f1 { a b }
28
+ # f1 { b c }
29
+ # }
30
+ #
31
+ # should be treated like `f1 { a b c }`
32
+ field_nodes = {}
33
+ ast_nodes.each do |ast_node|
34
+ ast_node.selections.each do |selected_ast_node|
35
+ gather_selections(field_nodes, definition, selected_ast_node)
27
36
  end
28
- h
29
- }
37
+ end
30
38
 
31
- Class.new(self) do
32
- define_fields(fields)
39
+ # After gathering all the nodes by name, prepare to create methods and classes for them.
40
+ field_classes = {}
41
+ field_nodes.each do |result_name, field_ast_nodes|
42
+ # `result_name` might be an alias, so make sure to get the proper name
43
+ field_name = field_ast_nodes.first.name
44
+ field_definition = definition.client.schema.get_field(type.name, field_name)
45
+ field_return_type = field_definition.type
46
+ field_classes[result_name.to_sym] = schema_module.define_class(definition, field_ast_nodes, field_return_type)
47
+ end
33
48
 
34
- if definition.client.enforce_collocated_callers
35
- keys = fields.keys.map { |key| ActiveSupport::Inflector.underscore(key) }
36
- Client.enforce_collocated_callers(self, keys, definition.source_location[0])
37
- end
49
+ klass = Class.new(self)
50
+ klass.define_fields(field_classes)
51
+ klass.instance_variable_set(:@source_definition, definition)
52
+ klass.instance_variable_set(:@_spreads, definition.indexes[:spreads][ast_nodes.first])
53
+
54
+ if definition.client.enforce_collocated_callers
55
+ keys = field_classes.keys.map { |key| ActiveSupport::Inflector.underscore(key) }
56
+ Client.enforce_collocated_callers(klass, keys, definition.source_location[0])
57
+ end
58
+
59
+ klass
60
+ end
38
61
 
39
- class << self
40
- attr_reader :source_definition
41
- attr_reader :_spreads
62
+ PREDICATE_CACHE = Hash.new { |h, name|
63
+ h[name] = -> { @data[name] ? true : false }
64
+ }
65
+
66
+ METHOD_CACHE = Hash.new { |h, key|
67
+ h[key] = -> {
68
+ name = key.to_s
69
+ type = self.class::FIELDS[key]
70
+ @casted_data.fetch(name) do
71
+ @casted_data[name] = type.cast(@data[name], @errors.filter_by_path(name))
42
72
  end
73
+ }
74
+ }
43
75
 
44
- @source_definition = definition
45
- @_spreads = definition.indexes[:spreads][irep_node.ast_node]
76
+ MODULE_CACHE = Hash.new do |h, fields|
77
+ h[fields] = Module.new do
78
+ fields.each do |name|
79
+ GraphQL::Client::Schema::ObjectType.define_cached_field(name, self)
80
+ end
46
81
  end
47
82
  end
48
83
 
84
+ FIELDS_CACHE = Hash.new { |h, k| h[k] = k }
85
+
49
86
  def define_fields(fields)
50
- fields.each { |name, type| define_field(name, type) }
87
+ const_set :FIELDS, FIELDS_CACHE[fields]
88
+ mod = MODULE_CACHE[fields.keys.sort]
89
+ include mod
90
+ end
91
+
92
+ def self.define_cached_field(name, ctx)
93
+ key = name
94
+ name = -name.to_s
95
+ method_name = ActiveSupport::Inflector.underscore(name)
96
+
97
+ ctx.send(:define_method, method_name, &METHOD_CACHE[key])
98
+ ctx.send(:define_method, "#{method_name}?", &PREDICATE_CACHE[name])
51
99
  end
52
100
 
53
101
  def define_field(name, type)
@@ -75,9 +123,67 @@ module GraphQL
75
123
  raise InvariantError, "expected value to be a Hash, but was #{value.class}"
76
124
  end
77
125
  end
126
+
127
+ private
128
+
129
+ # Given an AST selection on this object, gather it into `fields` if it applies.
130
+ # If it's a fragment, continue recursively checking the selections on the fragment.
131
+ def gather_selections(fields, definition, selected_ast_node)
132
+ case selected_ast_node
133
+ when GraphQL::Language::Nodes::InlineFragment
134
+ continue_selection = if selected_ast_node.type.nil?
135
+ true
136
+ else
137
+ schema = definition.client.schema
138
+ type_condition = schema.types[selected_ast_node.type.name]
139
+ applicable_types = schema.possible_types(type_condition)
140
+ # continue if this object type is one of the types matching the fragment condition
141
+ applicable_types.include?(type)
142
+ end
143
+
144
+ if continue_selection
145
+ selected_ast_node.selections.each do |next_selected_ast_node|
146
+ gather_selections(fields, definition, next_selected_ast_node)
147
+ end
148
+ end
149
+ when GraphQL::Language::Nodes::FragmentSpread
150
+ fragment_definition = definition.document.definitions.find do |defn|
151
+ defn.is_a?(GraphQL::Language::Nodes::FragmentDefinition) && defn.name == selected_ast_node.name
152
+ end
153
+
154
+ schema = definition.client.schema
155
+ type_condition = schema.types[fragment_definition.type.name]
156
+ applicable_types = schema.possible_types(type_condition)
157
+ # continue if this object type is one of the types matching the fragment condition
158
+ continue_selection = applicable_types.include?(type)
159
+
160
+ if continue_selection
161
+ fragment_definition.selections.each do |next_selected_ast_node|
162
+ gather_selections(fields, definition, next_selected_ast_node)
163
+ end
164
+ end
165
+ when GraphQL::Language::Nodes::Field
166
+ operation_definition_for_field = definition.indexes[:definitions][selected_ast_node]
167
+ # Ignore fields defined in other documents.
168
+ if definition.source_document.definitions.include?(operation_definition_for_field)
169
+ field_method_name = selected_ast_node.alias || selected_ast_node.name
170
+ ast_nodes = fields[field_method_name] ||= []
171
+ ast_nodes << selected_ast_node
172
+ end
173
+ else
174
+ raise "Unexpected selection node: #{selected_ast_node}"
175
+ end
176
+ end
78
177
  end
79
178
 
80
179
  class ObjectClass
180
+ module ClassMethods
181
+ attr_reader :source_definition
182
+ attr_reader :_spreads
183
+ end
184
+
185
+ extend ClassMethods
186
+
81
187
  def initialize(data = {}, errors = Errors.new)
82
188
  @data = data
83
189
  @casted_data = {}
@@ -41,7 +41,7 @@ module GraphQL
41
41
  if type = possible_types[typename]
42
42
  type.cast(value, errors)
43
43
  else
44
- raise InvariantError, "expected value to be one of (#{possible_types.keys.join(", ")}), but was #{typename}"
44
+ raise InvariantError, "expected value to be one of (#{possible_types.keys.join(", ")}), but was #{typename.inspect}"
45
45
  end
46
46
  when NilClass
47
47
  nil
@@ -19,7 +19,7 @@ module GraphQL
19
19
  @type = type
20
20
  end
21
21
 
22
- def define_class(definition, irep_node)
22
+ def define_class(definition, ast_nodes)
23
23
  self
24
24
  end
25
25
 
@@ -20,10 +20,12 @@ module GraphQL
20
20
  PossibleTypes.new(type, types)
21
21
  end
22
22
 
23
- def define_class(definition, irep_node)
24
- new(irep_node.typed_children.keys.map { |ctype|
25
- schema_module.get_class(ctype.name).define_class(definition, irep_node)
26
- })
23
+ def define_class(definition, ast_nodes)
24
+ possible_type_names = definition.client.schema.possible_types(type).map(&:graphql_name)
25
+ possible_types = possible_type_names.map { |concrete_type_name|
26
+ schema_module.get_class(concrete_type_name).define_class(definition, ast_nodes)
27
+ }
28
+ new(possible_types)
27
29
  end
28
30
  end
29
31
  end
@@ -15,23 +15,25 @@ module GraphQL
15
15
  class Client
16
16
  module Schema
17
17
  module ClassMethods
18
- def define_class(definition, irep_node, type)
19
- type_klass = case type
18
+ def define_class(definition, ast_nodes, type)
19
+ type_class = case type
20
20
  when GraphQL::NonNullType
21
- define_class(definition, irep_node, type.of_type).to_non_null_type
21
+ define_class(definition, ast_nodes, type.of_type).to_non_null_type
22
22
  when GraphQL::ListType
23
- define_class(definition, irep_node, type.of_type).to_list_type
23
+ define_class(definition, ast_nodes, type.of_type).to_list_type
24
24
  else
25
- get_class(type.name).define_class(definition, irep_node)
25
+ get_class(type.name).define_class(definition, ast_nodes)
26
26
  end
27
27
 
28
- irep_node.ast_node.directives.inject(type_klass) do |klass, directive|
29
- if directive = self.directives[directive.name.to_sym]
30
- directive.new(klass)
31
- else
32
- klass
28
+ ast_nodes.each do |ast_node|
29
+ ast_node.directives.each do |directive|
30
+ if directive = self.directives[directive.name.to_sym]
31
+ type_class = directive.new(type_class)
32
+ end
33
33
  end
34
34
  end
35
+
36
+ type_class
35
37
  end
36
38
 
37
39
  def get_class(type_name)
@@ -50,6 +52,13 @@ module GraphQL
50
52
  const_set(class_name, klass)
51
53
  end
52
54
 
55
+ DIRECTIVES = { include: IncludeDirective,
56
+ skip: SkipDirective }.freeze
57
+
58
+ def directives
59
+ DIRECTIVES
60
+ end
61
+
53
62
  private
54
63
 
55
64
  def normalize_type_name(type_name)
@@ -61,10 +70,6 @@ module GraphQL
61
70
  mod = Module.new
62
71
  mod.extend ClassMethods
63
72
 
64
- mod.define_singleton_method :schema do
65
- schema
66
- end
67
-
68
73
  cache = {}
69
74
  schema.types.each do |name, type|
70
75
  next if name.start_with?("__")
@@ -74,11 +79,6 @@ module GraphQL
74
79
  end
75
80
  end
76
81
 
77
- directives = {}
78
- mod.define_singleton_method(:directives) { directives }
79
- directives[:include] = IncludeDirective
80
- directives[:skip] = SkipDirective
81
-
82
82
  mod
83
83
  end
84
84
 
@@ -12,8 +12,8 @@ require "graphql/client/operation_definition"
12
12
  require "graphql/client/query_typename"
13
13
  require "graphql/client/response"
14
14
  require "graphql/client/schema"
15
- require "graphql/language/nodes/deep_freeze_ext"
16
15
  require "json"
16
+ require "delegate"
17
17
 
18
18
  module GraphQL
19
19
  # GraphQL Client helps build and execute queries against a GraphQL backend.
@@ -67,9 +67,9 @@ module GraphQL
67
67
  end
68
68
  end
69
69
 
70
- IntrospectionDocument = GraphQL.parse(GraphQL::Introspection::INTROSPECTION_QUERY).deep_freeze
70
+ IntrospectionDocument = GraphQL.parse(GraphQL::Introspection::INTROSPECTION_QUERY)
71
71
 
72
- def self.dump_schema(schema, io = nil)
72
+ def self.dump_schema(schema, io = nil, context: {})
73
73
  unless schema.respond_to?(:execute)
74
74
  raise TypeError, "expected schema to respond to #execute(), but was #{schema.class}"
75
75
  end
@@ -78,7 +78,7 @@ module GraphQL
78
78
  document: IntrospectionDocument,
79
79
  operation_name: "IntrospectionQuery",
80
80
  variables: {},
81
- context: {}
81
+ context: context
82
82
  ).to_h
83
83
 
84
84
  if io
@@ -120,13 +120,21 @@ module GraphQL
120
120
 
121
121
  definition_dependencies = Set.new
122
122
 
123
+ # Replace Ruby constant reference with GraphQL fragment names,
124
+ # while populating `definition_dependencies` with
125
+ # GraphQL Fragment ASTs which this operation depends on
123
126
  str = str.gsub(/\.\.\.([a-zA-Z0-9_]+(::[a-zA-Z0-9_]+)*)/) do
124
127
  match = Regexp.last_match
125
128
  const_name = match[1]
126
129
 
127
130
  if str.match(/fragment\s*#{const_name}/)
131
+ # It's a fragment _definition_, not a fragment usage
128
132
  match[0]
129
133
  else
134
+ # It's a fragment spread, so we should load the fragment
135
+ # which corresponds to the spread.
136
+ # We depend on ActiveSupport to either find the already-loaded
137
+ # constant, or to load the constant by name
130
138
  begin
131
139
  fragment = ActiveSupport::Inflector.constantize(const_name)
132
140
  rescue NameError
@@ -135,6 +143,10 @@ module GraphQL
135
143
 
136
144
  case fragment
137
145
  when FragmentDefinition
146
+ # We found the fragment definition that this fragment spread belongs to.
147
+ # So, register the AST of this fragment in `definition_dependencies`
148
+ # and update the query string to valid GraphQL syntax,
149
+ # replacing the Ruby constant
138
150
  definition_dependencies.merge(fragment.document.definitions)
139
151
  "...#{fragment.definition_name}"
140
152
  else
@@ -157,10 +169,17 @@ module GraphQL
157
169
  doc = GraphQL.parse(str)
158
170
 
159
171
  document_types = DocumentTypes.analyze_types(self.schema, doc).freeze
160
- QueryTypename.insert_typename_fields(doc, types: document_types)
172
+ doc = QueryTypename.insert_typename_fields(doc, types: document_types)
161
173
 
162
174
  doc.definitions.each do |node|
163
- node.name ||= "__anonymous__"
175
+ if node.name.nil?
176
+ if node.respond_to?(:merge) # GraphQL 1.9 +
177
+ node_with_name = node.merge(name: "__anonymous__")
178
+ doc = doc.replace_child(node, node_with_name)
179
+ else
180
+ node.name = "__anonymous__"
181
+ end
182
+ end
164
183
  end
165
184
 
166
185
  document_dependencies = Language::Nodes::Document.new(definitions: doc.definitions + definition_dependencies.to_a)
@@ -183,39 +202,34 @@ module GraphQL
183
202
 
184
203
  definitions = {}
185
204
  doc.definitions.each do |node|
186
- irep_node = case node
187
- when GraphQL::Language::Nodes::OperationDefinition
188
- errors[:irep].operation_definitions[node.name]
189
- when GraphQL::Language::Nodes::FragmentDefinition
190
- errors[:irep].fragment_definitions[node.name]
191
- else
192
- raise TypeError, "unexpected #{node.class}"
193
- end
194
-
195
- node.name = nil if node.name == "__anonymous__"
196
205
  sliced_document = Language::DefinitionSlice.slice(document_dependencies, node.name)
197
206
  definition = Definition.for(
198
207
  client: self,
199
- irep_node: irep_node,
208
+ ast_node: node,
200
209
  document: sliced_document,
210
+ source_document: doc,
201
211
  source_location: source_location
202
212
  )
203
213
  definitions[node.name] = definition
204
214
  end
205
215
 
206
216
  name_hook = RenameNodeHook.new(definitions)
207
- visitor = Language::Visitor.new(doc)
217
+ visitor = Language::Visitor.new(document_dependencies)
208
218
  visitor[Language::Nodes::FragmentDefinition].leave << name_hook.method(:rename_node)
209
219
  visitor[Language::Nodes::OperationDefinition].leave << name_hook.method(:rename_node)
210
220
  visitor[Language::Nodes::FragmentSpread].leave << name_hook.method(:rename_node)
211
221
  visitor.visit
212
222
 
213
- doc.deep_freeze
214
-
215
- document.definitions.concat(doc.definitions) if document_tracking_enabled
223
+ if document_tracking_enabled
224
+ if @document.respond_to?(:merge) # GraphQL 1.9+
225
+ @document = @document.merge(definitions: @document.definitions + doc.definitions)
226
+ else
227
+ @document.definitions.concat(doc.definitions)
228
+ end
229
+ end
216
230
 
217
- if definitions[nil]
218
- definitions[nil]
231
+ if definitions["__anonymous__"]
232
+ definitions["__anonymous__"]
219
233
  else
220
234
  Module.new do
221
235
  definitions.each do |name, definition|
@@ -234,7 +248,7 @@ module GraphQL
234
248
  definition = @definitions[node.name]
235
249
  if definition
236
250
  node.extend(LazyName)
237
- node.name = -> { definition.definition_name }
251
+ node._definition = definition
238
252
  end
239
253
  end
240
254
  end
@@ -357,8 +371,10 @@ module GraphQL
357
371
  # name to point to a lazily defined Proc instead of a static string.
358
372
  module LazyName
359
373
  def name
360
- @name.call
374
+ @_definition.definition_name
361
375
  end
376
+
377
+ attr_writer :_definition
362
378
  end
363
379
 
364
380
  private
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.12.3
4
+ version: 0.15.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - GitHub
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-04-05 00:00:00.000000000 Z
11
+ date: 2019-08-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -17,9 +17,6 @@ dependencies:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
19
  version: '3.0'
20
- - - "<"
21
- - !ruby/object:Gem::Version
22
- version: '6.0'
23
20
  type: :runtime
24
21
  prerelease: false
25
22
  version_requirements: !ruby/object:Gem::Requirement
@@ -27,23 +24,20 @@ dependencies:
27
24
  - - ">="
28
25
  - !ruby/object:Gem::Version
29
26
  version: '3.0'
30
- - - "<"
31
- - !ruby/object:Gem::Version
32
- version: '6.0'
33
27
  - !ruby/object:Gem::Dependency
34
28
  name: graphql
35
29
  requirement: !ruby/object:Gem::Requirement
36
30
  requirements:
37
31
  - - "~>"
38
32
  - !ruby/object:Gem::Version
39
- version: '1.6'
33
+ version: '1.8'
40
34
  type: :runtime
41
35
  prerelease: false
42
36
  version_requirements: !ruby/object:Gem::Requirement
43
37
  requirements:
44
38
  - - "~>"
45
39
  - !ruby/object:Gem::Version
46
- version: '1.6'
40
+ version: '1.8'
47
41
  - !ruby/object:Gem::Dependency
48
42
  name: actionpack
49
43
  requirement: !ruby/object:Gem::Requirement
@@ -51,9 +45,6 @@ dependencies:
51
45
  - - ">="
52
46
  - !ruby/object:Gem::Version
53
47
  version: 3.2.22
54
- - - "<"
55
- - !ruby/object:Gem::Version
56
- version: '6.0'
57
48
  type: :development
58
49
  prerelease: false
59
50
  version_requirements: !ruby/object:Gem::Requirement
@@ -61,9 +52,6 @@ dependencies:
61
52
  - - ">="
62
53
  - !ruby/object:Gem::Version
63
54
  version: 3.2.22
64
- - - "<"
65
- - !ruby/object:Gem::Version
66
- version: '6.0'
67
55
  - !ruby/object:Gem::Dependency
68
56
  name: erubi
69
57
  requirement: !ruby/object:Gem::Requirement
@@ -126,28 +114,28 @@ dependencies:
126
114
  requirements:
127
115
  - - "~>"
128
116
  - !ruby/object:Gem::Version
129
- version: '0.6'
117
+ version: '0.10'
130
118
  type: :development
131
119
  prerelease: false
132
120
  version_requirements: !ruby/object:Gem::Requirement
133
121
  requirements:
134
122
  - - "~>"
135
123
  - !ruby/object:Gem::Version
136
- version: '0.6'
124
+ version: '0.10'
137
125
  - !ruby/object:Gem::Dependency
138
126
  name: rubocop
139
127
  requirement: !ruby/object:Gem::Requirement
140
128
  requirements:
141
129
  - - "~>"
142
130
  - !ruby/object:Gem::Version
143
- version: '0.49'
131
+ version: '0.55'
144
132
  type: :development
145
133
  prerelease: false
146
134
  version_requirements: !ruby/object:Gem::Requirement
147
135
  requirements:
148
136
  - - "~>"
149
137
  - !ruby/object:Gem::Version
150
- version: '0.49'
138
+ version: '0.55'
151
139
  description: A Ruby library for declaring, composing and executing GraphQL queries
152
140
  email: engineering@github.com
153
141
  executables: []
@@ -161,6 +149,7 @@ files:
161
149
  - lib/graphql/client/definition.rb
162
150
  - lib/graphql/client/definition_variables.rb
163
151
  - lib/graphql/client/document_types.rb
152
+ - lib/graphql/client/erb.rb
164
153
  - lib/graphql/client/error.rb
165
154
  - lib/graphql/client/errors.rb
166
155
  - lib/graphql/client/erubi_enhancer.rb
@@ -188,7 +177,6 @@ files:
188
177
  - lib/graphql/client/schema/skip_directive.rb
189
178
  - lib/graphql/client/schema/union_type.rb
190
179
  - lib/graphql/client/view_module.rb
191
- - lib/graphql/language/nodes/deep_freeze_ext.rb
192
180
  - lib/rubocop/cop/graphql/heredoc.rb
193
181
  - lib/rubocop/cop/graphql/overfetch.rb
194
182
  homepage: https://github.com/github/graphql-client
@@ -210,8 +198,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
210
198
  - !ruby/object:Gem::Version
211
199
  version: '0'
212
200
  requirements: []
213
- rubyforge_project:
214
- rubygems_version: 2.6.11
201
+ rubygems_version: 3.0.3
215
202
  signing_key:
216
203
  specification_version: 4
217
204
  summary: GraphQL Client
@@ -1,21 +0,0 @@
1
- # frozen_string_literal: true
2
- require "graphql"
3
-
4
- module GraphQL
5
- module Language
6
- module Nodes
7
- # :nodoc:
8
- class AbstractNode
9
- # Public: Freeze entire Node tree
10
- #
11
- # Returns self Node.
12
- def deep_freeze
13
- children.each(&:deep_freeze)
14
- scalars.each { |s| s && s.freeze }
15
- freeze
16
- self
17
- end
18
- end
19
- end
20
- end
21
- end