graphlient 0.7.0 → 0.9.0

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 (80) hide show
  1. checksums.yaml +4 -4
  2. data/.github/ISSUE_TEMPLATE/bug_report.md +38 -38
  3. data/.github/ISSUE_TEMPLATE/feature_request.md +20 -20
  4. data/.github/workflows/ci.yml +33 -29
  5. data/.github/workflows/danger-comment.yml +10 -0
  6. data/.github/workflows/danger.yml +13 -22
  7. data/.github/workflows/rubocop.yml +19 -19
  8. data/.gitignore +59 -52
  9. data/.rspec +1 -1
  10. data/.rubocop.yml +26 -28
  11. data/.rubocop_todo.yml +72 -48
  12. data/CHANGELOG.md +128 -107
  13. data/CONTRIBUTING.md +125 -125
  14. data/Dangerfile +24 -24
  15. data/Gemfile +35 -27
  16. data/LICENSE +21 -21
  17. data/README.md +800 -508
  18. data/RELEASING.md +63 -63
  19. data/Rakefile +15 -15
  20. data/UPGRADING.md +23 -23
  21. data/graphlient.gemspec +19 -19
  22. data/lib/graphlient/adapters/http/adapter.rb +41 -41
  23. data/lib/graphlient/adapters/http/faraday_adapter.rb +79 -45
  24. data/lib/graphlient/adapters/http/http_adapter.rb +40 -39
  25. data/lib/graphlient/adapters/http.rb +3 -3
  26. data/lib/graphlient/adapters.rb +1 -1
  27. data/lib/graphlient/client.rb +98 -73
  28. data/lib/graphlient/errors/client_error.rb +6 -6
  29. data/lib/graphlient/errors/connection_failed_error.rb +6 -6
  30. data/lib/graphlient/errors/error.rb +13 -12
  31. data/lib/graphlient/errors/execution_error.rb +29 -29
  32. data/lib/graphlient/errors/faraday_server_error.rb +12 -12
  33. data/lib/graphlient/errors/graphql_error.rb +53 -52
  34. data/lib/graphlient/errors/http_options_error.rb +6 -6
  35. data/lib/graphlient/errors/http_server_error.rb +13 -13
  36. data/lib/graphlient/errors/server_error.rb +7 -7
  37. data/lib/graphlient/errors/timeout_error.rb +6 -6
  38. data/lib/graphlient/errors.rb +10 -10
  39. data/lib/graphlient/extensions/query.rb +15 -15
  40. data/lib/graphlient/extensions.rb +1 -1
  41. data/lib/graphlient/query/directive.rb +47 -0
  42. data/lib/graphlient/query/serializer/arguments.rb +67 -0
  43. data/lib/graphlient/query/serializer/directives.rb +17 -0
  44. data/lib/graphlient/query/serializer/evaluator.rb +15 -0
  45. data/lib/graphlient/query/serializer/fragments.rb +28 -0
  46. data/lib/graphlient/query/serializer/scalars.rb +63 -0
  47. data/lib/graphlient/query/serializer.rb +153 -0
  48. data/lib/graphlient/query.rb +29 -128
  49. data/lib/graphlient/schema.rb +27 -26
  50. data/lib/graphlient/version.rb +3 -3
  51. data/lib/graphlient.rb +8 -8
  52. data/spec/graphlient/adapters/http/faraday_adapter_spec.rb +172 -112
  53. data/spec/graphlient/adapters/http/http_adapter_spec.rb +60 -60
  54. data/spec/graphlient/client_dsl_spec.rb +153 -0
  55. data/spec/graphlient/client_query_spec.rb +369 -346
  56. data/spec/graphlient/client_schema_spec.rb +75 -55
  57. data/spec/graphlient/extensions/query_spec.rb +16 -16
  58. data/spec/graphlient/github_query_spec.rb +32 -32
  59. data/spec/graphlient/query_dsl_spec.rb +231 -0
  60. data/spec/graphlient/query_spec.rb +105 -105
  61. data/spec/graphlient/schema_spec.rb +56 -56
  62. data/spec/graphlient/static_client_query_spec.rb +75 -68
  63. data/spec/graphlient/webmock_client_query_spec.rb +41 -41
  64. data/spec/spec_helper.rb +26 -14
  65. data/spec/support/context/dummy_client.rb +33 -26
  66. data/spec/support/context/github_client.rb +18 -18
  67. data/spec/support/dummy_app.rb +21 -19
  68. data/spec/support/dummy_schema.rb +17 -17
  69. data/spec/support/fixtures/github/schema.yml +14282 -14282
  70. data/spec/support/fixtures/github/user.yml +76 -76
  71. data/spec/support/fixtures/github/viewer.yml +76 -76
  72. data/spec/support/fixtures/invoice_api.json +1288 -1288
  73. data/spec/support/mutations/create_invoice.rb +18 -18
  74. data/spec/support/queries/query.rb +48 -47
  75. data/spec/support/schema/github.json +44479 -44479
  76. data/spec/support/types/invoice_type.rb +13 -13
  77. data/spec/support/types/mutation_type.rb +5 -5
  78. data/spec/support/vcr.rb +9 -9
  79. metadata +13 -8
  80. data/Gemfile.danger +0 -5
@@ -0,0 +1,17 @@
1
+ module Graphlient
2
+ class Query
3
+ class Serializer
4
+ module Directives
5
+ # Matches _skip, _include, _myCustomDirective etc.
6
+ # Does NOT match ___Fragment (three underscores -- checked first in method_missing).
7
+ DIRECTIVE_PREFIX = /\A_[a-z]/.freeze
8
+
9
+ private
10
+
11
+ def directive?(method_name)
12
+ method_name.to_s.match?(DIRECTIVE_PREFIX)
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,15 @@
1
+ module Graphlient
2
+ class Query
3
+ class Serializer
4
+ module Evaluator
5
+ private
6
+
7
+ def evaluate(&block)
8
+ @last_block = block || self
9
+ (@context ||= {})[@last_block] ||= @last_block.binding
10
+ instance_eval(&block)
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,28 @@
1
+ module Graphlient
2
+ class Query
3
+ class Serializer
4
+ module Fragments
5
+ # Pattern for the legacy ___Const::Name triple-underscore convention.
6
+ FRAGMENT_DEFINITION = /___(?<const>[A-Z][a-zA-Z0-9_]*(?:__[A-Z][a-zA-Z0-9_]*)*)/.freeze
7
+
8
+ private
9
+
10
+ def fragment?(method_name)
11
+ method_name.to_s.start_with?('___')
12
+ end
13
+
14
+ def resolve_fragment_constant(value)
15
+ return nil unless (match = value.to_s.match(FRAGMENT_DEFINITION))
16
+
17
+ raw_const = match[:const].gsub('__', '::')
18
+ @context[@last_block].eval(raw_const).tap do |const|
19
+ next if const.is_a?(GraphQL::Client::FragmentDefinition)
20
+
21
+ msg = "Expected constant #{raw_const} to be GraphQL::Client::FragmentDefinition. Given #{const.class}"
22
+ raise Graphlient::Errors::Error, msg
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,63 @@
1
+ module Graphlient
2
+ class Query
3
+ class Serializer
4
+ module Scalars
5
+ BUILT_IN_SCALAR_TYPES = {
6
+ int: 'Int',
7
+ float: 'Float',
8
+ string: 'String',
9
+ boolean: 'Boolean'
10
+ }.freeze
11
+
12
+ def self.included(base)
13
+ base.extend(ClassMethods)
14
+ end
15
+
16
+ module ClassMethods
17
+ # Register a custom scalar type for use in variable declarations.
18
+ #
19
+ # Graphlient::Query::Serializer.scalar(:date, 'Date')
20
+ #
21
+ # Typically called via the client configuration block:
22
+ # client = Graphlient::Client.new(url) do |c|
23
+ # c.scalar :date, 'Date'
24
+ # c.scalar :uuid, 'UUID'
25
+ # c.scalar :decimal, 'Decimal'
26
+ # end
27
+ #
28
+ # After registration, use the symbol in variable declarations:
29
+ # query(created_after: :date) { ... } # -> query($createdAfter: Date)
30
+ def scalar(sym, graphql_type)
31
+ custom_scalar_types[sym.to_sym] = graphql_type.to_s
32
+ end
33
+
34
+ def custom_scalar_types
35
+ @custom_scalar_types ||= {}
36
+ end
37
+
38
+ def all_scalar_types
39
+ BUILT_IN_SCALAR_TYPES.merge(custom_scalar_types)
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ def scalar?(value)
46
+ all_scalar_types.key?(value.to_s.delete('!').to_sym)
47
+ end
48
+
49
+ def scalar_type(value)
50
+ str = value.to_s
51
+ base = str.delete('!')
52
+ non_null = str.end_with?('!')
53
+ type = all_scalar_types[base.to_sym]
54
+ non_null ? "#{type}!" : type
55
+ end
56
+
57
+ def all_scalar_types
58
+ self.class.all_scalar_types
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,153 @@
1
+ require_relative '../errors/error'
2
+ require_relative 'directive'
3
+ require_relative 'serializer/scalars'
4
+ require_relative 'serializer/arguments'
5
+ require_relative 'serializer/evaluator'
6
+ require_relative 'serializer/fragments'
7
+ require_relative 'serializer/directives'
8
+
9
+ module Graphlient
10
+ class Query
11
+ # Builds a GraphQL query string from a DSL block.
12
+ # Composed from focused concern modules; Query is a thin public wrapper.
13
+ class Serializer
14
+ include Scalars
15
+ include Arguments
16
+ include Evaluator
17
+ include Fragments
18
+ include Directives
19
+
20
+ ROOT_NODES = %w[query mutation subscription].freeze
21
+
22
+ ROOT_NODES.each do |root_node|
23
+ define_method(root_node) do |*args, &block|
24
+ @variables = args.first || {}
25
+ append_node(root_node, args, arg_processor: variable_processor, &block)
26
+ end
27
+ end
28
+
29
+ attr_accessor :query_str
30
+
31
+ def initialize(&block)
32
+ @indents = 0
33
+ @query_str = ''
34
+ @variables = {}
35
+ @fragments = {}
36
+ evaluate(&block) if block
37
+ end
38
+
39
+ # Full output: main query string + any inline fragment definitions.
40
+ def to_s
41
+ parts = [query_str.strip]
42
+ parts.concat(@fragments.values) unless @fragments.empty?
43
+ parts.join("\n\n")
44
+ end
45
+
46
+ # Raised when `spread` is called with neither a fragment name nor `on:`.
47
+ SPREAD_REQUIRES_NAME_OR_ON =
48
+ 'spread requires a fragment name (`spread :InvoiceFields`) or an inline ' \
49
+ 'type condition (`spread on: :PaidInvoice { ... }`).'.freeze
50
+ # Raised when a named spread is given a block (a named spread has no selection set).
51
+ SPREAD_NAME_TAKES_NO_BLOCK =
52
+ 'a named fragment spread takes no block; for an inline fragment use ' \
53
+ '`spread on: :Type { ... }`.'.freeze
54
+
55
+ # Fragment spread OR inline fragment -- one consistent entry point.
56
+ #
57
+ # Named fragment spread: ...FragmentName [@directive ...]
58
+ # spread :InvoiceFields
59
+ # spread :InvoiceFields, _skip(if: :x) # -> ...InvoiceFields @skip(if: $x)
60
+ #
61
+ # Inline fragment / type condition: ... on Type [@directive ...] { fields }
62
+ # spread on: :PaidInvoice { amount_paid } # -> ... on PaidInvoice { amountPaid }
63
+ # spread on: :DraftInvoice, _skip(if: :skip) { draft_id }
64
+ # spread on: :Invoice # bare type condition, no block
65
+ #
66
+ # A single verb covers both GraphQL forms; `on:` is the same keyword used by
67
+ # `fragment(name, on:)`, keeping the DSL consistent (and leaving room for a future
68
+ # `spread(:X).skip(...)` chaining form without a breaking change).
69
+ def spread(*args, on: nil, &block)
70
+ directives = args.select { |a| a.is_a?(Directive) }
71
+ if on
72
+ append_inline_fragment(on, directives, &block)
73
+ else
74
+ append_named_spread(args, directives, &block)
75
+ end
76
+ @query_str << "\n#{indent}"
77
+ end
78
+
79
+ # Inline fragment definition -- collected and appended after the main query.
80
+ # fragment(:InvoiceFields, on: :Invoice) { id; fee_in_cents }
81
+ def fragment(name, on:, &block)
82
+ body = self.class.new(&block).query_str.strip
83
+ @fragments[name] = "fragment #{name} on #{on} {\n#{body}\n}"
84
+ end
85
+
86
+ def method_missing(method_name, *args, &block)
87
+ if fragment?(method_name)
88
+ append_node("...#{resolve_fragment_constant(method_name)}".to_sym, args, &block)
89
+ elsif directive?(method_name)
90
+ Directive.new(method_name, args.first || {})
91
+ else
92
+ append_node(method_name, args, &block)
93
+ end
94
+ end
95
+
96
+ def respond_to_missing?(method_name, include_private = false)
97
+ super
98
+ end
99
+
100
+ private
101
+
102
+ def indent
103
+ ' ' * @indents
104
+ end
105
+
106
+ # Inline fragment / type condition: ... on Type [@directive ...] [{ fields }]
107
+ def append_inline_fragment(type, directives, &block)
108
+ @query_str << "\n#{indent}... on #{type}"
109
+ directives.each { |d| @query_str << " #{d}" }
110
+ return unless block
111
+
112
+ @indents += 1
113
+ @query_str << '{'
114
+ evaluate(&block)
115
+ @query_str << '}'
116
+ @indents -= 1
117
+ end
118
+
119
+ # Named fragment spread: ...FragmentName [@directive ...]
120
+ def append_named_spread(args, directives, &block)
121
+ fragment_name = args.find { |a| !a.is_a?(Directive) }
122
+ raise Graphlient::Errors::Error, SPREAD_REQUIRES_NAME_OR_ON if fragment_name.nil?
123
+ raise Graphlient::Errors::Error, SPREAD_NAME_TAKES_NO_BLOCK if block
124
+
125
+ @query_str << "\n#{indent}...#{fragment_name}"
126
+ directives.each { |d| @query_str << " #{d}" }
127
+ end
128
+
129
+ def append_node(node, args, arg_processor: nil, &block)
130
+ regular = field_args(args)
131
+ dirs = directive_args(args)
132
+
133
+ @query_str << "\n#{indent}#{node}"
134
+
135
+ if (h = hash_arg(regular))
136
+ @query_str << "(#{args_str(h, arg_processor: arg_processor)})"
137
+ end
138
+
139
+ dirs.each { |d| @query_str << " #{d}" }
140
+
141
+ if block_given?
142
+ @indents += 1
143
+ @query_str << '{'
144
+ evaluate(&block)
145
+ @query_str << '}'
146
+ @indents -= 1
147
+ end
148
+
149
+ @query_str << "\n#{indent}"
150
+ end
151
+ end
152
+ end
153
+ end
@@ -1,128 +1,29 @@
1
- module Graphlient
2
- class Query
3
- SCALAR_TYPES = {
4
- int: 'Int',
5
- float: 'Float',
6
- string: 'String',
7
- boolean: 'Boolean'
8
- }.freeze
9
-
10
- ROOT_NODES = %w[query mutation subscription].freeze
11
-
12
- FRAGMENT_DEFITION = /___(?<const>[A-Z][a-zA-Z0-9_]*(__[A-Z][a-zA-Z0-9_]*)*)/
13
-
14
- attr_accessor :query_str
15
-
16
- def initialize(&block)
17
- @indents = 0
18
- @query_str = ''
19
- @variables = []
20
- evaluate(&block)
21
- end
22
-
23
- def method_missing(method_name, *args, &block)
24
- append_node(method_name, args, &block)
25
- end
26
-
27
- ROOT_NODES.each do |root_node|
28
- define_method(root_node) do |*args, &block|
29
- @variables = args.first unless args.empty?
30
- append_node(root_node, args, arg_processor: ->(k, v) { "$#{k}: #{variable_string(v)}" }, &block)
31
- end
32
- end
33
-
34
- def respond_to_missing?(method_name, include_private = false)
35
- super
36
- end
37
-
38
- def to_s
39
- query_str.strip
40
- end
41
-
42
- private
43
-
44
- def evaluate(&block)
45
- @last_block = block || self
46
- (@context ||= {})[@last_block] ||= @last_block.binding
47
- instance_eval(&block)
48
- end
49
-
50
- def resolve_fragment_constant(value)
51
- return nil unless (match = value.to_s.match(FRAGMENT_DEFITION))
52
- raw_const = match[:const].gsub('__', '::')
53
- @context[@last_block].eval(raw_const).tap do |const|
54
- msg = "Expected constant #{raw_const} to be GraphQL::Client::FragmentDefinition. Given #{const.class}"
55
- raise Graphlient::Errors::Error, msg unless const.is_a? GraphQL::Client::FragmentDefinition
56
- end
57
- end
58
-
59
- def append_node(node, args, arg_processor: nil, &block)
60
- node = "...#{resolve_fragment_constant(node)}".to_sym if node.to_s.start_with?('___')
61
-
62
- # add field
63
- @query_str << "\n#{indent}#{node}"
64
- # add filter
65
- hash_arguments = hash_arg(args)
66
- @query_str << "(#{args_str(hash_arguments, arg_processor: arg_processor)})" if hash_arguments
67
-
68
- if block_given?
69
- @indents += 1
70
- @query_str << '{'
71
- evaluate(&block)
72
- @query_str << '}'
73
- @indents -= 1
74
- end
75
-
76
- @query_str << "\n#{indent}"
77
- end
78
-
79
- def indent
80
- ' ' * @indents
81
- end
82
-
83
- def hash_arg(args)
84
- args.detect { |arg| arg.is_a? Hash }
85
- end
86
-
87
- def args_str(hash_args, arg_processor: nil)
88
- hash_args.map do |k, v|
89
- arg_processor ? arg_processor.call(k, v) : argument_string(k, v)
90
- end.join(', ')
91
- end
92
-
93
- def argument_string(key, val)
94
- "#{key}: #{argument_value_string(val)}"
95
- end
96
-
97
- def variable_string(val)
98
- case val
99
- when :id, :id!
100
- val.to_s.upcase
101
- when ->(v) { SCALAR_TYPES.key?(v.to_s.delete('!').to_sym) }
102
- # scalar types
103
- val.to_s.camelize
104
- when Array
105
- "[#{variable_string(val.first)}]"
106
- else
107
- val.to_s
108
- end
109
- end
110
-
111
- def argument_value_string(value)
112
- case value
113
- when String
114
- "\"#{value}\""
115
- when Numeric
116
- value.to_s
117
- when Array
118
- "[#{value.map { |v| argument_value_string(v) }.join(', ')}]"
119
- when Hash
120
- "{ #{value.map { |k, v| "#{k}: #{argument_value_string(v)}" }.join(', ')} }"
121
- when Symbol
122
- @variables.key?(value) ? "$#{value}" : value.to_s.camelize(:lower)
123
- else
124
- value
125
- end
126
- end
127
- end
128
- end
1
+ require_relative 'query/serializer'
2
+
3
+ module Graphlient
4
+ # Public interface for building GraphQL query strings via a DSL block.
5
+ # Delegates all string-building work to Query::Serializer.
6
+ #
7
+ # Usage:
8
+ # Graphlient::Query.new do
9
+ # query do
10
+ # invoice(id: 10) { id; fee_in_cents }
11
+ # end
12
+ # end.to_s
13
+ class Query
14
+ # Register a custom scalar type for variable declarations.
15
+ # Delegates to Serializer so client configuration works:
16
+ # client = Graphlient::Client.new(url) { |c| c.scalar :date, 'Date' }
17
+ def self.scalar(sym, graphql_type)
18
+ Serializer.scalar(sym, graphql_type)
19
+ end
20
+
21
+ def initialize(&block)
22
+ @serializer = Serializer.new(&block)
23
+ end
24
+
25
+ def to_s
26
+ @serializer.to_s
27
+ end
28
+ end
29
+ end
@@ -1,26 +1,27 @@
1
- require 'delegate'
2
-
3
- module Graphlient
4
- class Schema < SimpleDelegator
5
- PATH_ERROR_MESSAGE = 'schema_path is missing. Please add it like this: `Graphlient.new(url, schema_path: YOUR_PATH)`'.freeze
6
-
7
- class MissingConfigurationError < StandardError; end
8
-
9
- alias graphql_schema __getobj__
10
-
11
- attr_reader :http, :path
12
-
13
- def initialize(http, path)
14
- schema_source = path || http
15
- super(GraphQL::Client.load_schema(schema_source))
16
-
17
- @path = path
18
- @http = http
19
- end
20
-
21
- def dump!
22
- raise MissingConfigurationError, PATH_ERROR_MESSAGE unless path
23
- GraphQL::Client.dump_schema(http, path)
24
- end
25
- end
26
- end
1
+ require 'delegate'
2
+
3
+ module Graphlient
4
+ class Schema < SimpleDelegator
5
+ PATH_ERROR_MESSAGE = 'schema_path is missing. Please add it like this: `Graphlient.new(url, schema_path: YOUR_PATH)`'.freeze
6
+
7
+ class MissingConfigurationError < StandardError; end
8
+
9
+ alias graphql_schema __getobj__
10
+
11
+ attr_reader :http, :path
12
+
13
+ def initialize(http, path)
14
+ schema_source = path || http
15
+ super(GraphQL::Client.load_schema(schema_source))
16
+
17
+ @path = path
18
+ @http = http
19
+ end
20
+
21
+ def dump!
22
+ raise MissingConfigurationError, PATH_ERROR_MESSAGE unless path
23
+
24
+ GraphQL::Client.dump_schema(http, path)
25
+ end
26
+ end
27
+ end
@@ -1,3 +1,3 @@
1
- module Graphlient
2
- VERSION = '0.7.0'.freeze
3
- end
1
+ module Graphlient
2
+ VERSION = '0.9.0'.freeze
3
+ end
data/lib/graphlient.rb CHANGED
@@ -1,8 +1,8 @@
1
- require 'graphql/client'
2
- require 'graphlient/version'
3
- require 'graphlient/extensions'
4
- require 'graphlient/errors'
5
- require 'graphlient/query'
6
- require 'graphlient/adapters'
7
- require 'graphlient/client'
8
- require 'graphlient/schema'
1
+ require 'graphql/client'
2
+ require 'graphlient/version'
3
+ require 'graphlient/extensions'
4
+ require 'graphlient/errors'
5
+ require 'graphlient/query'
6
+ require 'graphlient/adapters'
7
+ require 'graphlient/client'
8
+ require 'graphlient/schema'