graphql_client 0.3.3
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 +7 -0
- data/.gitignore +5 -0
- data/.rubocop-http---shopify-github-io-ruby-style-guide-rubocop-yml +1133 -0
- data/.rubocop.yml +24 -0
- data/.travis.yml +6 -0
- data/CHANGELOG.md +3 -0
- data/CODE_OF_CONDUCT.md +45 -0
- data/CONTRIBUTING.md +28 -0
- data/CONTRIBUTING_DEVELOPER_CERTIFICATE_OF_ORIGIN.txt +37 -0
- data/Gemfile +8 -0
- data/LICENSE.md +21 -0
- data/README.md +96 -0
- data/Rakefile +13 -0
- data/bin/graphql-client +79 -0
- data/bin/rake +17 -0
- data/circle.yml +3 -0
- data/dev.yml +7 -0
- data/graphql_ruby_client.gemspec +26 -0
- data/lib/graphql_client.rb +46 -0
- data/lib/graphql_client/adapters/http_adapter.rb +72 -0
- data/lib/graphql_client/base.rb +53 -0
- data/lib/graphql_client/config.rb +42 -0
- data/lib/graphql_client/deserialization.rb +36 -0
- data/lib/graphql_client/error.rb +22 -0
- data/lib/graphql_client/graph_connection.rb +21 -0
- data/lib/graphql_client/graph_node.rb +24 -0
- data/lib/graphql_client/graph_object.rb +56 -0
- data/lib/graphql_client/introspection_query.rb +80 -0
- data/lib/graphql_client/query/add_inline_fragment.rb +42 -0
- data/lib/graphql_client/query/argument.rb +30 -0
- data/lib/graphql_client/query/document.rb +86 -0
- data/lib/graphql_client/query/field.rb +91 -0
- data/lib/graphql_client/query/fragment.rb +41 -0
- data/lib/graphql_client/query/has_selection_set.rb +72 -0
- data/lib/graphql_client/query/inline_fragment.rb +35 -0
- data/lib/graphql_client/query/mutation_document.rb +20 -0
- data/lib/graphql_client/query/operation.rb +46 -0
- data/lib/graphql_client/query/operations/mutation_operation.rb +17 -0
- data/lib/graphql_client/query/operations/query_operation.rb +17 -0
- data/lib/graphql_client/query/query_document.rb +20 -0
- data/lib/graphql_client/query/selection_set.rb +53 -0
- data/lib/graphql_client/response.rb +21 -0
- data/lib/graphql_client/response_connection.rb +18 -0
- data/lib/graphql_client/response_object.rb +32 -0
- data/lib/graphql_client/schema_patches.rb +17 -0
- data/lib/graphql_client/version.rb +7 -0
- data/shipit.rubygems.yml +1 -0
- data/shipit.yml +4 -0
- data/test/graphql_client/adapters/http_adapter_test.rb +111 -0
- data/test/graphql_client/config_test.rb +68 -0
- data/test/graphql_client/graph_connection_test.rb +8 -0
- data/test/graphql_client/graph_node_test.rb +8 -0
- data/test/graphql_client/graph_object_query_transforming_test.rb +156 -0
- data/test/graphql_client/graph_object_test.rb +142 -0
- data/test/graphql_client/graphql_client_test.rb +41 -0
- data/test/graphql_client/http_client_test.rb +52 -0
- data/test/graphql_client/query/add_inline_fragment_test.rb +57 -0
- data/test/graphql_client/query/arguments_test.rb +89 -0
- data/test/graphql_client/query/document_test.rb +246 -0
- data/test/graphql_client/query/field_test.rb +163 -0
- data/test/graphql_client/query/fragment_test.rb +45 -0
- data/test/graphql_client/query/inline_fragment_test.rb +37 -0
- data/test/graphql_client/query/mutation_document_test.rb +30 -0
- data/test/graphql_client/query/mutation_operation_test.rb +89 -0
- data/test/graphql_client/query/query_document_test.rb +30 -0
- data/test/graphql_client/query/query_operation_test.rb +262 -0
- data/test/graphql_client/query/selection_set_test.rb +116 -0
- data/test/graphql_client/response_connection_test.rb +50 -0
- data/test/graphql_client/response_object_test.rb +109 -0
- data/test/graphql_client/response_test.rb +67 -0
- data/test/support/fixtures/schema.json +13710 -0
- data/test/test_helper.rb +37 -0
- metadata +227 -0
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL
|
4
|
+
module Client
|
5
|
+
module Adapters
|
6
|
+
class HTTPAdapter
|
7
|
+
JSON_MIME_TYPE = 'application/json'.freeze
|
8
|
+
DEFAULT_HEADERS = { 'Accept' => JSON_MIME_TYPE, 'Content-Type' => JSON_MIME_TYPE }
|
9
|
+
|
10
|
+
attr_reader :config
|
11
|
+
|
12
|
+
def initialize(config)
|
13
|
+
@config = config
|
14
|
+
end
|
15
|
+
|
16
|
+
def request(query, operation_name: nil, variables: {})
|
17
|
+
req = build_request(query, operation_name: operation_name, variables: variables)
|
18
|
+
|
19
|
+
http_options = {
|
20
|
+
use_ssl: https?,
|
21
|
+
open_timeout: config.open_timeout,
|
22
|
+
read_timeout: config.read_timeout
|
23
|
+
}
|
24
|
+
|
25
|
+
# IMPORTANT: open_timeout is only respected when it's supplied as part of the options
|
26
|
+
# when you call Net::HTTP.start. It is not respected when it's set inside the block of
|
27
|
+
# Net::HTTP.start (i.e. http.open_timeout = 1)
|
28
|
+
response = Net::HTTP.start(config.url.hostname, config.url.port, http_options) do |http|
|
29
|
+
http.request(req)
|
30
|
+
end
|
31
|
+
|
32
|
+
case response
|
33
|
+
when Net::HTTPOK
|
34
|
+
puts "Response body: \n#{JSON.pretty_generate(JSON.parse(response.body))}" if debug?
|
35
|
+
Response.new(response.body)
|
36
|
+
else
|
37
|
+
raise ClientError, response
|
38
|
+
end
|
39
|
+
rescue Net::OpenTimeout
|
40
|
+
raise OpenTimeoutError, "timeout while waiting for a connection"
|
41
|
+
rescue Net::ReadTimeout
|
42
|
+
raise ReadTimeoutError, "timeout while waiting for a response"
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def build_request(query, operation_name: nil, variables: {})
|
48
|
+
headers = DEFAULT_HEADERS.merge(config.headers)
|
49
|
+
|
50
|
+
Net::HTTP::Post.new(config.url, headers).tap do |req|
|
51
|
+
req.basic_auth(config.username, config.password)
|
52
|
+
puts "Query: #{query}" if debug?
|
53
|
+
|
54
|
+
req.body = {
|
55
|
+
query: query,
|
56
|
+
variables: variables,
|
57
|
+
operation_name: operation_name,
|
58
|
+
}.to_json
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def debug?
|
63
|
+
config.debug
|
64
|
+
end
|
65
|
+
|
66
|
+
def https?
|
67
|
+
config.url.scheme == 'https'
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL
|
4
|
+
module Client
|
5
|
+
class Base
|
6
|
+
attr_reader :adapter, :config, :schema
|
7
|
+
|
8
|
+
def initialize(schema, config: nil, adapter: nil, &block)
|
9
|
+
@config = config || Config.new
|
10
|
+
@schema = load_schema(schema)
|
11
|
+
@adapter = adapter || Adapters::HTTPAdapter.new(@config)
|
12
|
+
|
13
|
+
instance_eval(&block) if block_given?
|
14
|
+
end
|
15
|
+
|
16
|
+
def build_query
|
17
|
+
query = Query::Document.new(@schema)
|
18
|
+
|
19
|
+
if block_given?
|
20
|
+
yield query
|
21
|
+
else
|
22
|
+
query
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def configure
|
27
|
+
yield @config
|
28
|
+
end
|
29
|
+
|
30
|
+
def query(query, operation_name: nil, variables: {})
|
31
|
+
response = adapter.request(query.to_query, operation_name: operation_name, variables: variables)
|
32
|
+
GraphObject.new(data: response.data, query: query)
|
33
|
+
end
|
34
|
+
|
35
|
+
def raw_query(query_string, operation_name: nil, variables: {})
|
36
|
+
response = adapter.request(query_string, operation_name: operation_name, variables: variables)
|
37
|
+
ResponseObject.new(response.data)
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def load_schema(schema)
|
43
|
+
case schema
|
44
|
+
when Pathname
|
45
|
+
schema_string = JSON.parse(File.read(schema))
|
46
|
+
GraphQLSchema.new(schema_string)
|
47
|
+
else
|
48
|
+
GraphQLSchema.new(schema)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL
|
4
|
+
module Client
|
5
|
+
class Config
|
6
|
+
attr_accessor(
|
7
|
+
:debug,
|
8
|
+
:headers,
|
9
|
+
:per_page,
|
10
|
+
:password,
|
11
|
+
:username,
|
12
|
+
:url,
|
13
|
+
:open_timeout,
|
14
|
+
:read_timeout
|
15
|
+
)
|
16
|
+
|
17
|
+
DEFAULTS = {
|
18
|
+
debug: false,
|
19
|
+
headers: {},
|
20
|
+
per_page: 100,
|
21
|
+
open_timeout: 5,
|
22
|
+
read_timeout: 5
|
23
|
+
}
|
24
|
+
|
25
|
+
def initialize(options = {})
|
26
|
+
@options = DEFAULTS.merge(options)
|
27
|
+
@debug = @options[:debug]
|
28
|
+
@headers = @options[:headers]
|
29
|
+
@per_page = @options[:per_page]
|
30
|
+
@password = @options[:password]
|
31
|
+
@username = @options[:username]
|
32
|
+
@open_timeout = @options[:open_timeout]
|
33
|
+
@read_timeout = @options[:read_timeout]
|
34
|
+
@url = URI(@options[:url]) if @options[:url]
|
35
|
+
end
|
36
|
+
|
37
|
+
def url=(url)
|
38
|
+
@url = URI(url)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL
|
4
|
+
module Client
|
5
|
+
module Deserialization
|
6
|
+
private
|
7
|
+
|
8
|
+
def create_accessor_methods(field_name, graph_object)
|
9
|
+
define_singleton_method(field_name) do
|
10
|
+
ivar_name = "@#{field_name}"
|
11
|
+
|
12
|
+
if instance_variable_defined?(ivar_name)
|
13
|
+
instance_variable_get(ivar_name)
|
14
|
+
else
|
15
|
+
instance_variable_set(ivar_name, graph_object)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
underscored_name = underscore(field_name)
|
20
|
+
|
21
|
+
if field_name != underscored_name
|
22
|
+
define_singleton_method(underscored_name) { send field_name }
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def underscore(name)
|
27
|
+
name
|
28
|
+
.gsub(/::/, '/')
|
29
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
30
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
31
|
+
.tr('-', '_')
|
32
|
+
.downcase
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL
|
4
|
+
module Client
|
5
|
+
Error = Class.new(StandardError)
|
6
|
+
OpenTimeoutError = Class.new(Error)
|
7
|
+
ReadTimeoutError = Class.new(Error)
|
8
|
+
ResponseError = Class.new(Error)
|
9
|
+
|
10
|
+
class ClientError < Error
|
11
|
+
attr_reader :response
|
12
|
+
|
13
|
+
def initialize(response)
|
14
|
+
@response = response
|
15
|
+
end
|
16
|
+
|
17
|
+
def message
|
18
|
+
"#{response.code} #{response.message}"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL
|
4
|
+
module Client
|
5
|
+
class GraphConnection < GraphObject
|
6
|
+
include Enumerable
|
7
|
+
|
8
|
+
def each
|
9
|
+
return enum_for(:each) unless block_given?
|
10
|
+
edges.each { |edge| yield edge.node }
|
11
|
+
end
|
12
|
+
|
13
|
+
def next_page_query
|
14
|
+
build_minimal_query do |connection|
|
15
|
+
connection.add_arguments(**query.arguments, after: edges.last.cursor)
|
16
|
+
connection.selection_set = query.selection_set
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL
|
4
|
+
module Client
|
5
|
+
class GraphNode < GraphObject
|
6
|
+
def build_minimal_query
|
7
|
+
Query::QueryDocument.new(query.schema) do |root|
|
8
|
+
root.add_field('node', id: data.fetch('id')) do |node|
|
9
|
+
node.add_inline_fragment(query.resolver_type.name) do |fragment|
|
10
|
+
fragment.add_field('id')
|
11
|
+
yield fragment
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def refetch_query
|
18
|
+
build_minimal_query do |node_fragment|
|
19
|
+
node_fragment.selection_set = query.selection_set
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL
|
4
|
+
module Client
|
5
|
+
class GraphObject
|
6
|
+
include Deserialization
|
7
|
+
|
8
|
+
attr_reader :data, :parent, :query
|
9
|
+
|
10
|
+
def initialize(query:, data:, parent: nil)
|
11
|
+
@query = query
|
12
|
+
@data = Hash(data)
|
13
|
+
@parent = parent
|
14
|
+
|
15
|
+
@data.each do |field_name, value|
|
16
|
+
field = query.selection_set.lookup(field_name)
|
17
|
+
|
18
|
+
graph_object = case value
|
19
|
+
when Hash
|
20
|
+
klass = if field.connection?
|
21
|
+
GraphConnection
|
22
|
+
elsif field.node?
|
23
|
+
GraphNode
|
24
|
+
else
|
25
|
+
GraphObject
|
26
|
+
end
|
27
|
+
|
28
|
+
klass.new(query: field, data: value, parent: self)
|
29
|
+
when Array
|
30
|
+
value.map do |v|
|
31
|
+
if v.is_a? Hash
|
32
|
+
self.class.new(query: field, data: v, parent: self)
|
33
|
+
else
|
34
|
+
v
|
35
|
+
end
|
36
|
+
end
|
37
|
+
else
|
38
|
+
value
|
39
|
+
end
|
40
|
+
|
41
|
+
create_accessor_methods(field_name, graph_object)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def build_minimal_query
|
46
|
+
if parent
|
47
|
+
parent.build_minimal_query do |context|
|
48
|
+
yield context.add_field(query.field_defn.name, as: query.name, **query.arguments)
|
49
|
+
end
|
50
|
+
else
|
51
|
+
Query::QueryDocument.new(query.schema) { |root| yield root }
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL
|
4
|
+
module Client
|
5
|
+
INTROSPECTION_QUERY = '
|
6
|
+
query IntrospectionQuery {
|
7
|
+
__schema {
|
8
|
+
queryType { name }
|
9
|
+
mutationType { name }
|
10
|
+
subscriptionType { name }
|
11
|
+
types {
|
12
|
+
...FullType
|
13
|
+
}
|
14
|
+
directives {
|
15
|
+
name
|
16
|
+
description
|
17
|
+
locations
|
18
|
+
args {
|
19
|
+
...InputValue
|
20
|
+
}
|
21
|
+
}
|
22
|
+
}
|
23
|
+
}
|
24
|
+
fragment FullType on __Type {
|
25
|
+
kind
|
26
|
+
name
|
27
|
+
description
|
28
|
+
fields(includeDeprecated: true) {
|
29
|
+
name
|
30
|
+
description
|
31
|
+
args {
|
32
|
+
...InputValue
|
33
|
+
}
|
34
|
+
type {
|
35
|
+
...TypeRef
|
36
|
+
}
|
37
|
+
isDeprecated
|
38
|
+
deprecationReason
|
39
|
+
}
|
40
|
+
inputFields {
|
41
|
+
...InputValue
|
42
|
+
}
|
43
|
+
interfaces {
|
44
|
+
...TypeRef
|
45
|
+
}
|
46
|
+
enumValues(includeDeprecated: true) {
|
47
|
+
name
|
48
|
+
description
|
49
|
+
isDeprecated
|
50
|
+
deprecationReason
|
51
|
+
}
|
52
|
+
possibleTypes {
|
53
|
+
...TypeRef
|
54
|
+
}
|
55
|
+
}
|
56
|
+
fragment InputValue on __InputValue {
|
57
|
+
name
|
58
|
+
description
|
59
|
+
type { ...TypeRef }
|
60
|
+
defaultValue
|
61
|
+
}
|
62
|
+
fragment TypeRef on __Type {
|
63
|
+
kind
|
64
|
+
name
|
65
|
+
ofType {
|
66
|
+
kind
|
67
|
+
name
|
68
|
+
ofType {
|
69
|
+
kind
|
70
|
+
name
|
71
|
+
ofType {
|
72
|
+
kind
|
73
|
+
name
|
74
|
+
}
|
75
|
+
}
|
76
|
+
}
|
77
|
+
}
|
78
|
+
'
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL
|
4
|
+
module Client
|
5
|
+
module Query
|
6
|
+
module AddInlineFragment
|
7
|
+
INVALID_FRAGMENT_TARGET = Class.new(StandardError)
|
8
|
+
|
9
|
+
def add_inline_fragment(type_name = resolver_type.name)
|
10
|
+
target_type = validate_fragment_target(document.schema.type(type_name))
|
11
|
+
|
12
|
+
inline_fragment = InlineFragment.new(target_type, document: document)
|
13
|
+
selection_set.add_inline_fragment(inline_fragment)
|
14
|
+
|
15
|
+
if block_given?
|
16
|
+
yield inline_fragment
|
17
|
+
else
|
18
|
+
inline_fragment
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def valid_concrete_type?(type_name)
|
25
|
+
return true if resolver_type.object? && resolver_type.implement?(type_name)
|
26
|
+
|
27
|
+
return false unless resolver_type.union? || resolver_type.interface?
|
28
|
+
resolver_type.possible_types.any? { |type| type.name == type_name }
|
29
|
+
end
|
30
|
+
|
31
|
+
def validate_fragment_target(type)
|
32
|
+
if resolver_type.name != type.name && !valid_concrete_type?(type.name)
|
33
|
+
raise INVALID_FRAGMENT_TARGET,
|
34
|
+
"invalid target type '#{type.name}' for fragment of type #{resolver_type.name}"
|
35
|
+
else
|
36
|
+
type
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|