graphql_client 0.3.3
Sign up to get free protection for your applications and to get access to all the features.
- 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,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL
|
4
|
+
module Client
|
5
|
+
module Query
|
6
|
+
Argument = Struct.new(:value) do
|
7
|
+
def to_query
|
8
|
+
case value
|
9
|
+
when FalseClass, Float, Integer, NilClass, String, TrueClass
|
10
|
+
generate_query_value(value)
|
11
|
+
when Array
|
12
|
+
"[#{value.map { |v| generate_query_value(v) }.join(', ')}]"
|
13
|
+
when Hash
|
14
|
+
"{ #{value.map { |k, v| "#{k}: #{generate_query_value(v)}" }.join(', ')} }"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def generate_query_value(value)
|
21
|
+
if value.to_s.start_with?('$')
|
22
|
+
value
|
23
|
+
else
|
24
|
+
JSON.generate(value, quirks_mode: true)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL
|
4
|
+
module Client
|
5
|
+
module Query
|
6
|
+
class Document
|
7
|
+
DEFAULT_OPERATION_NAME = 'default'
|
8
|
+
|
9
|
+
DUPLICATE_OPERATION_NAME = Class.new(StandardError)
|
10
|
+
INVALID_DOCUMENT = Class.new(StandardError)
|
11
|
+
INVALID_FRAGMENT_TARGET = Class.new(StandardError)
|
12
|
+
|
13
|
+
attr_reader :fragments, :operations, :schema
|
14
|
+
|
15
|
+
def initialize(schema)
|
16
|
+
@schema = schema
|
17
|
+
@fragments = {}
|
18
|
+
@operations = {}
|
19
|
+
|
20
|
+
yield self if block_given?
|
21
|
+
end
|
22
|
+
|
23
|
+
def add_mutation(name = nil, &block)
|
24
|
+
add_operation(MutationOperation, name, &block)
|
25
|
+
end
|
26
|
+
|
27
|
+
def add_query(name = nil, &block)
|
28
|
+
add_operation(QueryOperation, name, &block)
|
29
|
+
end
|
30
|
+
|
31
|
+
def define_fragment(name, on:)
|
32
|
+
type = schema.type(on)
|
33
|
+
|
34
|
+
unless type.object? || type.interface? || type.union?
|
35
|
+
raise INVALID_FRAGMENT_TARGET, "invalid target type (#{type.kind}) for fragment #{name}"
|
36
|
+
end
|
37
|
+
|
38
|
+
fragment = Fragment.new(name, type, document: self)
|
39
|
+
@fragments[name] = fragment
|
40
|
+
|
41
|
+
if block_given?
|
42
|
+
yield fragment
|
43
|
+
else
|
44
|
+
fragment
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def fragment_definitions
|
49
|
+
fragments.values.map(&:to_definition).join("\n")
|
50
|
+
end
|
51
|
+
|
52
|
+
def to_query
|
53
|
+
''.dup.tap do |query_string|
|
54
|
+
query_string << "#{fragment_definitions}\n" unless fragments.empty?
|
55
|
+
query_string << operations.values.map(&:to_query).join("\n")
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
alias_method :to_s, :to_query
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def add_operation(operation_type, name)
|
64
|
+
if operations.key?(DEFAULT_OPERATION_NAME)
|
65
|
+
raise INVALID_DOCUMENT, 'a document with multiple operations must have named operations'
|
66
|
+
end
|
67
|
+
|
68
|
+
operation = operation_type.new(self, name: name)
|
69
|
+
operation_name = operation.name || DEFAULT_OPERATION_NAME
|
70
|
+
|
71
|
+
if operations.key?(operation_name)
|
72
|
+
raise DUPLICATE_OPERATION_NAME, "an operation named #{operation_name} already exists in the document"
|
73
|
+
end
|
74
|
+
|
75
|
+
@operations[operation_name] = operation
|
76
|
+
|
77
|
+
if block_given?
|
78
|
+
yield operation
|
79
|
+
else
|
80
|
+
operation
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL
|
4
|
+
module Client
|
5
|
+
module Query
|
6
|
+
class Field
|
7
|
+
include AddInlineFragment
|
8
|
+
include HasSelectionSet
|
9
|
+
|
10
|
+
INVALID_ARGUMENTS = Class.new(StandardError)
|
11
|
+
|
12
|
+
attr_reader :arguments, :as, :document, :field_defn
|
13
|
+
|
14
|
+
def initialize(field_defn, document:, arguments: {}, as: nil)
|
15
|
+
@field_defn = field_defn
|
16
|
+
@document = document
|
17
|
+
@arguments = validate_arguments(arguments)
|
18
|
+
@as = as
|
19
|
+
@selection_set = SelectionSet.new
|
20
|
+
end
|
21
|
+
|
22
|
+
def add_arguments(**arguments)
|
23
|
+
new_arguments = validate_arguments(arguments)
|
24
|
+
@arguments.merge!(new_arguments)
|
25
|
+
end
|
26
|
+
|
27
|
+
def aliased?
|
28
|
+
name != field_defn.name
|
29
|
+
end
|
30
|
+
|
31
|
+
def arguments=(arguments)
|
32
|
+
@arguments = validate_arguments(arguments)
|
33
|
+
end
|
34
|
+
|
35
|
+
def connection?
|
36
|
+
resolver_type.name.to_s.end_with?('Connection')
|
37
|
+
end
|
38
|
+
|
39
|
+
def name
|
40
|
+
as || field_defn.name
|
41
|
+
end
|
42
|
+
|
43
|
+
def node?
|
44
|
+
field_defn.name == 'Node' || (resolver_type.object? && resolver_type.implement?('Node'))
|
45
|
+
end
|
46
|
+
|
47
|
+
def resolver_type
|
48
|
+
@resolver_type ||= schema.type(field_defn.type.unwrap.name)
|
49
|
+
end
|
50
|
+
|
51
|
+
def schema
|
52
|
+
document.schema
|
53
|
+
end
|
54
|
+
|
55
|
+
def to_query(indent: '')
|
56
|
+
indent.dup.tap do |query_string|
|
57
|
+
query_string << "#{as}: " if aliased?
|
58
|
+
query_string << field_defn.name
|
59
|
+
query_string << "(#{arguments_string.join(', ')})" if arguments.any?
|
60
|
+
|
61
|
+
unless selection_set.empty?
|
62
|
+
query_string << " {\n"
|
63
|
+
query_string << selection_set.to_query(indent)
|
64
|
+
query_string << "\n#{indent}}"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
alias_method :to_s, :to_query
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
def arguments_string
|
74
|
+
arguments.map do |name, value|
|
75
|
+
"#{name}: #{value.to_query}"
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def validate_arguments(arguments)
|
80
|
+
arguments.each_with_object({}) do |(arg_name, value), hash|
|
81
|
+
if field_defn.args.any? { |arg| arg.name == arg_name.to_s }
|
82
|
+
hash[arg_name] = value.is_a?(Argument) ? value : Argument.new(value)
|
83
|
+
else
|
84
|
+
raise INVALID_ARGUMENTS, "#{arg_name} is not a valid arg for #{name}"
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL
|
4
|
+
module Client
|
5
|
+
module Query
|
6
|
+
class Fragment
|
7
|
+
include AddInlineFragment
|
8
|
+
include HasSelectionSet
|
9
|
+
|
10
|
+
attr_reader :document, :name, :type
|
11
|
+
|
12
|
+
def initialize(name, type, document:)
|
13
|
+
@name = name
|
14
|
+
@type = type
|
15
|
+
@document = document
|
16
|
+
@selection_set = SelectionSet.new
|
17
|
+
|
18
|
+
yield self if block_given?
|
19
|
+
end
|
20
|
+
|
21
|
+
def resolver_type
|
22
|
+
type
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_definition(indent: '')
|
26
|
+
indent.dup.tap do |query_string|
|
27
|
+
query_string << "fragment #{name} on #{type.name} {\n"
|
28
|
+
query_string << selection_set.to_query(indent)
|
29
|
+
query_string << "\n#{indent}}\n"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def to_query(indent: '')
|
34
|
+
"#{indent}...#{name}"
|
35
|
+
end
|
36
|
+
|
37
|
+
alias_method :to_s, :to_query
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL
|
4
|
+
module Client
|
5
|
+
module Query
|
6
|
+
module HasSelectionSet
|
7
|
+
ID_FIELD_NAME = 'id'
|
8
|
+
INVALID_FIELD = Class.new(StandardError)
|
9
|
+
UNDEFINED_FRAGMENT = Class.new(StandardError)
|
10
|
+
|
11
|
+
attr_accessor :selection_set
|
12
|
+
|
13
|
+
def add_connection(connection_name, as: nil, **arguments)
|
14
|
+
node_field = nil
|
15
|
+
|
16
|
+
add_field(connection_name, as: as, **arguments) do |connection|
|
17
|
+
connection.add_field('edges') do |edges|
|
18
|
+
edges.add_field('cursor')
|
19
|
+
edges.add_field('node') do |node|
|
20
|
+
node_field = node
|
21
|
+
yield node
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
connection.add_field('pageInfo') do |page_info|
|
26
|
+
page_info.add_field('hasPreviousPage')
|
27
|
+
page_info.add_field('hasNextPage')
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
node_field
|
32
|
+
end
|
33
|
+
|
34
|
+
def add_field(field_name, as: nil, **arguments)
|
35
|
+
field_defn = resolve(field_name)
|
36
|
+
field = Field.new(field_defn, arguments: arguments, as: as, document: document)
|
37
|
+
selection_set.add_field(field)
|
38
|
+
|
39
|
+
field.add_field(ID_FIELD_NAME) if field.node?
|
40
|
+
|
41
|
+
if block_given?
|
42
|
+
yield field
|
43
|
+
else
|
44
|
+
field
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def add_fields(*field_names)
|
49
|
+
field_names.each do |field_name|
|
50
|
+
add_field(field_name)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def add_fragment(fragment_name)
|
55
|
+
fragment = document.fragments.fetch(fragment_name) do
|
56
|
+
raise UNDEFINED_FRAGMENT, "a fragment named #{fragment_name} has not been defined"
|
57
|
+
end
|
58
|
+
|
59
|
+
selection_set.add_fragment(fragment)
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def resolve(field_name)
|
65
|
+
resolver_type.field(field_name) do
|
66
|
+
raise INVALID_FIELD, "#{field_name} is not a valid field for #{resolver_type}"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL
|
4
|
+
module Client
|
5
|
+
module Query
|
6
|
+
class InlineFragment
|
7
|
+
include HasSelectionSet
|
8
|
+
|
9
|
+
attr_reader :document, :type
|
10
|
+
|
11
|
+
def initialize(type, document:)
|
12
|
+
@type = type
|
13
|
+
@document = document
|
14
|
+
@selection_set = SelectionSet.new
|
15
|
+
|
16
|
+
yield self if block_given?
|
17
|
+
end
|
18
|
+
|
19
|
+
def resolver_type
|
20
|
+
type
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_query(indent: '')
|
24
|
+
indent.dup.tap do |query_string|
|
25
|
+
query_string << "... on #{type.name} {\n"
|
26
|
+
query_string << selection_set.to_query(indent)
|
27
|
+
query_string << "\n#{indent}}"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
alias_method :to_s, :to_query
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL
|
4
|
+
module Client
|
5
|
+
module Query
|
6
|
+
class MutationDocument
|
7
|
+
def self.new(schema, name = nil)
|
8
|
+
document = Document.new(schema)
|
9
|
+
mutation = document.add_mutation(name)
|
10
|
+
|
11
|
+
if block_given?
|
12
|
+
yield mutation
|
13
|
+
end
|
14
|
+
|
15
|
+
mutation
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL
|
4
|
+
module Client
|
5
|
+
module Query
|
6
|
+
class Operation
|
7
|
+
include HasSelectionSet
|
8
|
+
|
9
|
+
attr_reader :document, :name, :selection_set, :variables
|
10
|
+
|
11
|
+
def initialize(document, name: nil, variables: {})
|
12
|
+
@document = document
|
13
|
+
@name = name
|
14
|
+
@selection_set = SelectionSet.new
|
15
|
+
@variables = variables
|
16
|
+
|
17
|
+
yield self if block_given?
|
18
|
+
end
|
19
|
+
|
20
|
+
def schema
|
21
|
+
document.schema
|
22
|
+
end
|
23
|
+
|
24
|
+
def to_query
|
25
|
+
operation_type.dup.tap do |query_string|
|
26
|
+
query_string << " #{name}" if name
|
27
|
+
query_string << "(#{variables_string.join(', ')})" if variables.any?
|
28
|
+
query_string << " {\n"
|
29
|
+
query_string << selection_set.to_query
|
30
|
+
query_string << "\n}\n"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
alias_method :to_s, :to_query
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def variables_string
|
39
|
+
variables.map do |name, type|
|
40
|
+
"$#{name}: #{type}"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL
|
4
|
+
module Client
|
5
|
+
module Query
|
6
|
+
class MutationOperation < Operation
|
7
|
+
def operation_type
|
8
|
+
'mutation'
|
9
|
+
end
|
10
|
+
|
11
|
+
def resolver_type
|
12
|
+
schema.type(schema.mutation_root_name)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|