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,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