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.
Files changed (73) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +5 -0
  3. data/.rubocop-http---shopify-github-io-ruby-style-guide-rubocop-yml +1133 -0
  4. data/.rubocop.yml +24 -0
  5. data/.travis.yml +6 -0
  6. data/CHANGELOG.md +3 -0
  7. data/CODE_OF_CONDUCT.md +45 -0
  8. data/CONTRIBUTING.md +28 -0
  9. data/CONTRIBUTING_DEVELOPER_CERTIFICATE_OF_ORIGIN.txt +37 -0
  10. data/Gemfile +8 -0
  11. data/LICENSE.md +21 -0
  12. data/README.md +96 -0
  13. data/Rakefile +13 -0
  14. data/bin/graphql-client +79 -0
  15. data/bin/rake +17 -0
  16. data/circle.yml +3 -0
  17. data/dev.yml +7 -0
  18. data/graphql_ruby_client.gemspec +26 -0
  19. data/lib/graphql_client.rb +46 -0
  20. data/lib/graphql_client/adapters/http_adapter.rb +72 -0
  21. data/lib/graphql_client/base.rb +53 -0
  22. data/lib/graphql_client/config.rb +42 -0
  23. data/lib/graphql_client/deserialization.rb +36 -0
  24. data/lib/graphql_client/error.rb +22 -0
  25. data/lib/graphql_client/graph_connection.rb +21 -0
  26. data/lib/graphql_client/graph_node.rb +24 -0
  27. data/lib/graphql_client/graph_object.rb +56 -0
  28. data/lib/graphql_client/introspection_query.rb +80 -0
  29. data/lib/graphql_client/query/add_inline_fragment.rb +42 -0
  30. data/lib/graphql_client/query/argument.rb +30 -0
  31. data/lib/graphql_client/query/document.rb +86 -0
  32. data/lib/graphql_client/query/field.rb +91 -0
  33. data/lib/graphql_client/query/fragment.rb +41 -0
  34. data/lib/graphql_client/query/has_selection_set.rb +72 -0
  35. data/lib/graphql_client/query/inline_fragment.rb +35 -0
  36. data/lib/graphql_client/query/mutation_document.rb +20 -0
  37. data/lib/graphql_client/query/operation.rb +46 -0
  38. data/lib/graphql_client/query/operations/mutation_operation.rb +17 -0
  39. data/lib/graphql_client/query/operations/query_operation.rb +17 -0
  40. data/lib/graphql_client/query/query_document.rb +20 -0
  41. data/lib/graphql_client/query/selection_set.rb +53 -0
  42. data/lib/graphql_client/response.rb +21 -0
  43. data/lib/graphql_client/response_connection.rb +18 -0
  44. data/lib/graphql_client/response_object.rb +32 -0
  45. data/lib/graphql_client/schema_patches.rb +17 -0
  46. data/lib/graphql_client/version.rb +7 -0
  47. data/shipit.rubygems.yml +1 -0
  48. data/shipit.yml +4 -0
  49. data/test/graphql_client/adapters/http_adapter_test.rb +111 -0
  50. data/test/graphql_client/config_test.rb +68 -0
  51. data/test/graphql_client/graph_connection_test.rb +8 -0
  52. data/test/graphql_client/graph_node_test.rb +8 -0
  53. data/test/graphql_client/graph_object_query_transforming_test.rb +156 -0
  54. data/test/graphql_client/graph_object_test.rb +142 -0
  55. data/test/graphql_client/graphql_client_test.rb +41 -0
  56. data/test/graphql_client/http_client_test.rb +52 -0
  57. data/test/graphql_client/query/add_inline_fragment_test.rb +57 -0
  58. data/test/graphql_client/query/arguments_test.rb +89 -0
  59. data/test/graphql_client/query/document_test.rb +246 -0
  60. data/test/graphql_client/query/field_test.rb +163 -0
  61. data/test/graphql_client/query/fragment_test.rb +45 -0
  62. data/test/graphql_client/query/inline_fragment_test.rb +37 -0
  63. data/test/graphql_client/query/mutation_document_test.rb +30 -0
  64. data/test/graphql_client/query/mutation_operation_test.rb +89 -0
  65. data/test/graphql_client/query/query_document_test.rb +30 -0
  66. data/test/graphql_client/query/query_operation_test.rb +262 -0
  67. data/test/graphql_client/query/selection_set_test.rb +116 -0
  68. data/test/graphql_client/response_connection_test.rb +50 -0
  69. data/test/graphql_client/response_object_test.rb +109 -0
  70. data/test/graphql_client/response_test.rb +67 -0
  71. data/test/support/fixtures/schema.json +13710 -0
  72. data/test/test_helper.rb +37 -0
  73. metadata +227 -0
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module Client
5
+ module Query
6
+ class QueryOperation < Operation
7
+ def operation_type
8
+ 'query'
9
+ end
10
+
11
+ def resolver_type
12
+ schema.type(schema.query_root_name)
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module Client
5
+ module Query
6
+ class QueryDocument
7
+ def self.new(schema, name = nil)
8
+ document = Document.new(schema)
9
+ query = document.add_query(name)
10
+
11
+ if block_given?
12
+ yield query
13
+ end
14
+
15
+ query
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module Client
5
+ module Query
6
+ class SelectionSet
7
+ attr_reader :fields, :fragments, :fields, :inline_fragments
8
+
9
+ def initialize
10
+ @fragments = {}
11
+ @fields = {}
12
+ @inline_fragments = []
13
+ end
14
+
15
+ def add_field(field)
16
+ @fields[field.name] = field
17
+ end
18
+
19
+ def add_fragment(fragment)
20
+ @fragments[fragment.name] = fragment
21
+ end
22
+
23
+ def add_inline_fragment(inline_fragment)
24
+ @inline_fragments << inline_fragment
25
+ end
26
+
27
+ def contains?(field_name)
28
+ fields.key?(field_name)
29
+ end
30
+
31
+ def empty?
32
+ selections.empty?
33
+ end
34
+
35
+ def lookup(name)
36
+ fields.fetch(name)
37
+ end
38
+
39
+ def selections
40
+ fields.values + fragments.values + inline_fragments
41
+ end
42
+
43
+ def to_query(indent = '')
44
+ selections.map do |selection|
45
+ selection.to_query(indent: indent + ' ')
46
+ end.join("\n")
47
+ end
48
+
49
+ alias_method :to_s, :to_query
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module Client
5
+ class Response
6
+ attr_reader :body, :data, :errors, :extensions
7
+
8
+ def initialize(response_body)
9
+ response = JSON.parse(response_body)
10
+ data, errors, extensions = response.values_at('data', 'errors', 'extensions')
11
+
12
+ raise ResponseError, errors if !data && errors
13
+
14
+ @body = response
15
+ @data = data
16
+ @errors = errors.to_a
17
+ @extensions = extensions.to_a
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module Client
5
+ class ResponseConnection < ResponseObject
6
+ extend Forwardable
7
+ include Enumerable
8
+
9
+ def_delegator :page_info, :has_next_page, :has_next_page?
10
+ def_delegator :page_info, :has_previous_page, :has_previous_page?
11
+
12
+ def each
13
+ return enum_for(:each) unless block_given?
14
+ edges.each { |edge| yield edge.node }
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module Client
5
+ class ResponseObject
6
+ include Deserialization
7
+
8
+ attr_reader :data
9
+
10
+ def initialize(data)
11
+ @data = Hash(data)
12
+
13
+ @data.each do |field_name, value|
14
+ response_object = case value
15
+ when Hash
16
+ if value.key?('edges')
17
+ ResponseConnection.new(value)
18
+ else
19
+ self.class.new(value)
20
+ end
21
+ when Array
22
+ value.map { |v| v.is_a?(Hash) ? self.class.new(v) : v }
23
+ else
24
+ value
25
+ end
26
+
27
+ create_accessor_methods(field_name, response_object)
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ class GraphQLSchema
4
+ def query_root
5
+ type(query_root_name)
6
+ end
7
+
8
+ def type(name)
9
+ types_by_name.fetch(name)
10
+ end
11
+
12
+ class TypeDefinition
13
+ def field(name)
14
+ fields_by_name.fetch(name)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module Client
5
+ VERSION = '0.3.3'
6
+ end
7
+ end
@@ -0,0 +1 @@
1
+ # using the default shipit config
@@ -0,0 +1,4 @@
1
+ deploy:
2
+ override:
3
+ - bundle exec rake build
4
+ - bundle exec package_cloud push shopify/gems pkg/*.gem
@@ -0,0 +1,111 @@
1
+ require 'test_helper'
2
+
3
+ module GraphQL
4
+ module Client
5
+ module Adapters
6
+ class HTTPAdapterTest < Minitest::Test
7
+ def test_request_builds_a_request
8
+ config = Config.new(url: 'http://example.org')
9
+ adapter = HTTPAdapter.new(config)
10
+
11
+ stub_request(:post, 'http://example.org')
12
+ .with(
13
+ body: {
14
+ query: 'query { shop }',
15
+ variables: {},
16
+ operation_name: nil
17
+ }.to_json,
18
+ headers: { 'Accept' => 'application/json' }
19
+ )
20
+ .to_return(body: { data: { id: 1 } }.to_json)
21
+
22
+ adapter.request('query { shop }')
23
+ end
24
+
25
+ def test_send_request_builds_a_request_with_operation_name
26
+ config = Config.new(url: 'http://example.org')
27
+ adapter = HTTPAdapter.new(config)
28
+
29
+ stub_request(:post, 'http://example.org')
30
+ .with(
31
+ body: {
32
+ query: 'query shopQuery { shop }',
33
+ variables: {},
34
+ operation_name: 'shopQuery'
35
+ }.to_json,
36
+ headers: { 'Accept' => 'application/json' }
37
+ )
38
+ .to_return(body: { data: { id: 1 } }.to_json)
39
+
40
+ adapter.request('query shopQuery { shop }', operation_name: 'shopQuery')
41
+ end
42
+
43
+ def test_send_request_returns_a_response_instance
44
+ config = Config.new(url: 'http://example.org')
45
+ adapter = HTTPAdapter.new(config)
46
+
47
+ stub_request(:post, 'http://example.org')
48
+ .with(
49
+ body: {
50
+ query: 'query { shop }',
51
+ variables: {},
52
+ operation_name: nil
53
+ }.to_json,
54
+ headers: { 'Accept' => 'application/json' }
55
+ )
56
+ .to_return(body: { data: { id: 1 } }.to_json)
57
+
58
+ response = adapter.request('query { shop }')
59
+
60
+ assert_instance_of Response, response
61
+ end
62
+
63
+ def test_send_request_raises_an_exception_on_net_http_error
64
+ config = Config.new(url: 'http://example.org')
65
+ adapter = HTTPAdapter.new(config)
66
+
67
+ stub_request(:post, 'http://example.org')
68
+ .with(
69
+ body: {
70
+ query: 'query { shop }',
71
+ variables: {},
72
+ operation_name: nil
73
+ }.to_json,
74
+ headers: { 'Accept' => 'application/json' }
75
+ )
76
+ .to_return(status: [401, 'Unauthorized'])
77
+
78
+ exception = assert_raises ClientError do
79
+ adapter.request('query { shop }')
80
+ end
81
+
82
+ assert_equal '401 Unauthorized', exception.message
83
+ assert_equal '401', exception.response.code
84
+ assert(exception.response.body.nil? || exception.response.body == '')
85
+ end
86
+
87
+ def test_send_request_raises_an_exception_on_net_http_open_timeout_error
88
+ config = Config.new(url: 'http://example.org')
89
+ adapter = HTTPAdapter.new(config)
90
+
91
+ stub_request(:post, 'http://example.org').to_raise(Net::OpenTimeout)
92
+
93
+ assert_raises OpenTimeoutError do
94
+ adapter.request('query { shop }')
95
+ end
96
+ end
97
+
98
+ def test_send_request_raises_an_exception_on_net_http_read_timeout_error
99
+ config = Config.new(url: 'http://example.org')
100
+ adapter = HTTPAdapter.new(config)
101
+
102
+ stub_request(:post, 'http://example.org').to_raise(Net::ReadTimeout)
103
+
104
+ assert_raises ReadTimeoutError do
105
+ adapter.request('query { shop }')
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,68 @@
1
+ require 'test_helper'
2
+
3
+ module GraphQL
4
+ module Client
5
+ class ConfigTest < Minitest::Test
6
+ def test_initialize_overrides_defaults
7
+ config = Config.new(
8
+ debug: true,
9
+ password: 'foo',
10
+ per_page: 5,
11
+ )
12
+
13
+ assert config.debug
14
+ assert_equal({}, config.headers)
15
+ assert_equal 'foo', config.password
16
+ assert_equal 5, config.per_page
17
+ assert_nil config.username
18
+ end
19
+
20
+ def test_initialize_parses_url_as_uri
21
+ config = Config.new(url: 'http://example.com')
22
+
23
+ assert_kind_of URI, config.url
24
+ end
25
+
26
+ def test_initialize_accepts_open_timeout_in_seconds
27
+ config = Config.new(open_timeout: 2)
28
+ assert_equal 2, config.open_timeout
29
+ end
30
+
31
+ def test_initialize_accepts_read_timeout_in_seconds
32
+ config = Config.new(read_timeout: 5)
33
+ assert_equal 5, config.read_timeout
34
+ end
35
+
36
+ def test_setting_open_timeout_after_initialization
37
+ config = Config.new
38
+ config.open_timeout = 2
39
+
40
+ assert_equal 2, config.open_timeout
41
+ end
42
+
43
+ def test_setting_read_timeout_after_initialization
44
+ config = Config.new
45
+ config.read_timeout = 5
46
+
47
+ assert_equal 5, config.read_timeout
48
+ end
49
+
50
+ def test_opent_timeout_is_5_seconds_when_not_set
51
+ config = Config.new
52
+ assert_equal 5, config.open_timeout
53
+ end
54
+
55
+ def test_read_timeout_is_5_seconds_when_not_set
56
+ config = Config.new
57
+ assert_equal 5, config.read_timeout
58
+ end
59
+
60
+ def test_url_writer_coerces_to_uri
61
+ config = Config.new
62
+ config.url = 'http://example.com'
63
+
64
+ assert_kind_of URI, config.url
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,8 @@
1
+ require 'test_helper'
2
+
3
+ module GraphQL
4
+ module Client
5
+ class GraphConnectionTest < Minitest::Test
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ require 'test_helper'
2
+
3
+ module GraphQL
4
+ module Client
5
+ class GraphNodeTest < Minitest::Test
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,156 @@
1
+ require 'test_helper'
2
+
3
+ module GraphQL
4
+ module Client
5
+ class GraphObjectQueryTransformingTest < Minitest::Test
6
+ COLLECTION_ID = 'gid://shopify/Collection/67890'
7
+ COLLECTION_CURSOR = 'collection-cursor'
8
+ PRODUCT_ID = 'gid://shopify/Product/72727'
9
+ VARIANTS_CURSOR = 'variants-cursor'
10
+ GRAPH_FIXTURE = {
11
+ data: {
12
+ shop: {
13
+ name: 'my-shop',
14
+ privacyPolicy: {
15
+ body: 'Text'
16
+ },
17
+ collections: {
18
+ pageInfo: {
19
+ hasPreviousPage: false,
20
+ hasNextPage: true
21
+ },
22
+ edges: [{
23
+ cursor: COLLECTION_CURSOR,
24
+ node: {
25
+ id: COLLECTION_ID,
26
+ handle: 'fancy-poles'
27
+ }
28
+ }]
29
+ },
30
+ products: {
31
+ pageInfo: {
32
+ hasPreviousPage: false,
33
+ hasNextPage: true
34
+ },
35
+ edges: [{
36
+ cursor: 'product-cursor',
37
+ node: {
38
+ id: PRODUCT_ID,
39
+ handle: 'some-product',
40
+ variants: {
41
+ pageInfo: {
42
+ hasPreviousPage: false,
43
+ hasNextPage: true
44
+ },
45
+ edges: [{
46
+ cursor: VARIANTS_CURSOR,
47
+ node: {
48
+ id: PRODUCT_ID,
49
+ title: 'large'
50
+ }
51
+ }]
52
+ }
53
+ }
54
+ }]
55
+ }
56
+ }
57
+ }
58
+ }
59
+
60
+ def setup
61
+ @schema = GraphQLSchema.new(schema_fixture('schema.json'))
62
+ @graphql_schema = GraphQL::Schema::Loader.load(schema_fixture('schema.json'))
63
+
64
+ @base_query = Query::QueryDocument.new(@schema) do |root|
65
+ root.add_field('shop') do |shop|
66
+ shop.add_field('name')
67
+ shop.add_field('privacyPolicy') do |address|
68
+ address.add_field('body')
69
+ end
70
+ shop.add_connection('collections', first: 1) do |collections|
71
+ collections.add_field('handle')
72
+ end
73
+ shop.add_connection('products', first: 1) do |products|
74
+ products.add_field('handle')
75
+ products.add_connection('variants', first: 1) do |variants|
76
+ variants.add_field('title')
77
+ end
78
+ end
79
+ end
80
+ end
81
+
82
+ data = GRAPH_FIXTURE[:data].to_json
83
+ @graph = GraphObject.new(data: JSON.parse(data), query: @base_query)
84
+ end
85
+
86
+ def test_nodes_can_generate_a_query_to_refetch_themselves
87
+ query_string = <<~QUERY
88
+ query {
89
+ node(id: "#{COLLECTION_ID}") {
90
+ ... on Collection {
91
+ id
92
+ handle
93
+ }
94
+ }
95
+ }
96
+ QUERY
97
+
98
+ assert_equal query_string, @graph.shop.collections.to_a[0].refetch_query.to_query
99
+ assert_valid_query query_string, @graphql_schema
100
+ end
101
+
102
+ def test_arrays_of_nodes_can_generate_a_query_to_fetch_the_next_page
103
+ query_string = <<~QUERY
104
+ query {
105
+ shop {
106
+ collections(first: 1, after: "#{COLLECTION_CURSOR}") {
107
+ edges {
108
+ cursor
109
+ node {
110
+ id
111
+ handle
112
+ }
113
+ }
114
+ pageInfo {
115
+ hasPreviousPage
116
+ hasNextPage
117
+ }
118
+ }
119
+ }
120
+ }
121
+ QUERY
122
+
123
+ assert_equal query_string, @graph.shop.collections.next_page_query.to_query
124
+ assert_valid_query query_string, @graphql_schema
125
+ end
126
+
127
+ def test_arrays_of_nodes_nested_under_a_truncated_query_to_fetch_their_next_page
128
+ query_string = <<~QUERY
129
+ query {
130
+ node(id: "#{PRODUCT_ID}") {
131
+ ... on Product {
132
+ id
133
+ variants(first: 1, after: "#{VARIANTS_CURSOR}") {
134
+ edges {
135
+ cursor
136
+ node {
137
+ id
138
+ title
139
+ }
140
+ }
141
+ pageInfo {
142
+ hasPreviousPage
143
+ hasNextPage
144
+ }
145
+ }
146
+ }
147
+ }
148
+ }
149
+ QUERY
150
+
151
+ assert_equal query_string, @graph.shop.products.to_a[0].variants.next_page_query.to_query
152
+ assert_valid_query query_string, @graphql_schema
153
+ end
154
+ end
155
+ end
156
+ end