rspec-graphql_response 0.1.0 → 0.4.1

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 (35) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +14 -0
  3. data/.gitignore +3 -0
  4. data/.gituprc +128 -0
  5. data/Gemfile.lock +16 -7
  6. data/README.md +64 -72
  7. data/RELEASE_NOTES.md +45 -0
  8. data/UPGRADE.md +41 -0
  9. data/docs/add_helper.md +94 -0
  10. data/docs/add_validator.md +168 -1
  11. data/docs/configuration.md +12 -0
  12. data/docs/execute_graphql.md +50 -0
  13. data/docs/graphql_spec_type.md +14 -0
  14. data/docs/have_operation.md +46 -0
  15. data/docs/operation.md +1 -36
  16. data/docs/response.md +1 -0
  17. data/docs/response_data.md +146 -0
  18. data/lib/rspec/graphql_response.rb +2 -1
  19. data/lib/rspec/graphql_response/dig_dug/dig_dug.rb +83 -0
  20. data/lib/rspec/graphql_response/helpers.rb +35 -7
  21. data/lib/rspec/graphql_response/helpers/execute_graphql.rb +14 -1
  22. data/lib/rspec/graphql_response/helpers/graphql_context.rb +3 -0
  23. data/lib/rspec/graphql_response/helpers/graphql_operation.rb +3 -0
  24. data/lib/rspec/graphql_response/helpers/graphql_variables.rb +3 -0
  25. data/lib/rspec/graphql_response/helpers/operation.rb +1 -0
  26. data/lib/rspec/graphql_response/helpers/response_data.rb +13 -0
  27. data/lib/rspec/graphql_response/matchers.rb +1 -0
  28. data/lib/rspec/graphql_response/matchers/have_errors.rb +2 -2
  29. data/lib/rspec/graphql_response/matchers/have_operation.rb +23 -0
  30. data/lib/rspec/graphql_response/validators.rb +6 -0
  31. data/lib/rspec/graphql_response/validators/have_operation.rb +24 -0
  32. data/lib/rspec/graphql_response/version.rb +1 -1
  33. data/rspec-graphql_response.gemspec +3 -2
  34. metadata +52 -21
  35. data/.travis.yml +0 -7
data/docs/response.md ADDED
@@ -0,0 +1 @@
1
+ # The GraphQL Response, via Helper `response`
@@ -0,0 +1,146 @@
1
+ # Using the `response_data` Helper
2
+
3
+ The `response_data` helper will dig through a graphql response, through
4
+ the outer hash, into the response data for an operation, and through any
5
+ and all layers of hash and array.
6
+
7
+ ## Syntax
8
+
9
+ ```ruby
10
+ response_data *[dig_pattern]
11
+ ```
12
+
13
+ Data returned via this helper will assume a `"data" => ` key at the root of
14
+ the `response` object. This root does not need to be specified in the list
15
+ of attributes for the `dig_pattern`.
16
+
17
+ ### Params
18
+
19
+ * `*[dig_pattern]` - an array of attributes (`:symbol`, `"string"`, or `key: :value` pair) that describes
20
+ the data structure to dig through, and the final data set to retrieve from the graphql response.
21
+
22
+ #### dig_pattern
23
+
24
+ Each attribute added to the `dig_pattern` represents an attribute at the given level of the
25
+ data structure, in numeric order from left to right. The first attribute provides will dig into
26
+ that attribute at the first level of data (just below the `"data" =>` key). The second attribute
27
+ will dig through data just below that first level, etc. etc. etc.
28
+
29
+ For example, with a data structure as shown below, in "Basic Use", you could specifiy these
30
+ attributes for the dig pattern:
31
+
32
+ * :characters
33
+ * :name
34
+
35
+ Like this:
36
+
37
+ ```ruby
38
+ response_data :characters, :name
39
+ ```
40
+
41
+ This dig pattern will find the `"characters"` key just below `"data"`, then iterate through
42
+ the array of characters and retrieve the `"name"` of each character.
43
+
44
+ For more details and options for the dig pattern, see the examples below.
45
+
46
+ ## Basic Use
47
+
48
+ A `response` data structure may look something like the following.
49
+
50
+ ```ruby
51
+ {
52
+ "data" => {
53
+ "characters" => [
54
+ { "id" => "1", "name" => "Jam" },
55
+ { "id" => "2", "name" => "Redemption" },
56
+ { "id" => "3", "name" => "Pet" }
57
+ ]
58
+ }
59
+ }
60
+ ```
61
+
62
+ The `response_data` helper will dig through to give you simplified
63
+ results that are easier to verify.
64
+
65
+ For example, if only the names of the characters need to be checked:
66
+
67
+ ```ruby
68
+ response_data :characters, :name
69
+
70
+ # => ["Jam", "Redemption", "Pet"]
71
+ ```
72
+
73
+ Or perhaps only the name for 2nd character is needed:
74
+
75
+ ```ruby
76
+ response_data {characters: [1]}, :name
77
+
78
+ # => "Redemption"
79
+ ```
80
+
81
+ ## List Every Item in an Array
82
+
83
+ Many responses from a graphql call will include an array of data somewhere
84
+ in the data structure. If you need to return all of the items in an array,
85
+ you only need to specify that array's key:
86
+
87
+ ```ruby
88
+ it "has characters" do
89
+ characters = response_data(:characters)
90
+
91
+ expect(character).to include(
92
+ { id: 1, name: "Jam" },
93
+ # ...
94
+ )
95
+ end
96
+ ```
97
+
98
+ ## Dig a Field From Every Item in an Array
99
+
100
+ When validation only needs to occur on a specific field for items found in
101
+ an array, there are two options.
102
+
103
+ 1. Specify a list of fields as already shown
104
+ 2. change the array's key to a hash and provide a `:symbol` wrapped in an array as the value
105
+
106
+ The first option was already shown in the Basic Use section above.
107
+
108
+ ```ruby
109
+ response_data :characters, :name
110
+
111
+ # => ["Jam", "Redemption", "Pet"]
112
+ ```
113
+
114
+ For the second option, the code would look like this:
115
+
116
+ ```ruby
117
+ response_data characters: [:name]
118
+
119
+ # => ["Jam", "Redemption", "Pet"]
120
+ ```
121
+
122
+ Both of these options are functionaly the same. The primary difference will be
123
+ how you wish to express the data structure in your code. Changing the list of
124
+ attributes to a hash with an array wrapping the value will provide a better
125
+ indication that an array is expected at that point in the data structure.
126
+
127
+ ## Dig Out an Item By Index, From an Array
128
+
129
+ There may be times when only a single piece of a returned array needs to be
130
+ validated. To handle this, switch the key of the array to a hash, as in the
131
+ previous example. Rather than specifying a child node's key in the value, though,
132
+ specify the index of the item you wish to extract.
133
+
134
+ ```ruby
135
+ response_data characters: [1]
136
+ ```
137
+
138
+ This will return the character at index 1, from the array of characters.
139
+
140
+ ## Handling Nil
141
+
142
+ If there is no data the key supplied, the helper will return `nil`
143
+
144
+ ```ruby
145
+ response_data(:something_that_does_not_exist) #=> nil
146
+ ```
@@ -1,5 +1,6 @@
1
- require "RSpec"
1
+ require "rspec"
2
2
 
3
+ require_relative "graphql_response/dig_dug/dig_dug"
3
4
  require_relative "graphql_response/version"
4
5
  require_relative "graphql_response/configuration"
5
6
  require_relative "graphql_response/validators"
@@ -0,0 +1,83 @@
1
+ module RSpec
2
+ module GraphQLResponse
3
+ class DigDug
4
+ attr_reader :dig_pattern
5
+
6
+ def initialize(*dig_pattern)
7
+ @dig_pattern = parse_dig_pattern(*dig_pattern)
8
+ end
9
+
10
+ def dig(data)
11
+ dig_data(data, dig_pattern)
12
+ end
13
+
14
+ private
15
+
16
+ def dig_data(data, patterns)
17
+ return data if patterns.nil?
18
+ return data if patterns.empty?
19
+
20
+ node = patterns[0]
21
+ node_key = node[:key]
22
+ node_key = node_key.to_s if node_key.is_a? Symbol
23
+ node_value = node[:value]
24
+
25
+ if node[:type] == :symbol
26
+ result = dig_symbol(data, node_key)
27
+ elsif node[:type] == :array
28
+ if data.is_a? Hash
29
+ child_data = data[node_key]
30
+ result = dig_symbol(child_data, node_value)
31
+ elsif data.is_a? Array
32
+ result = data.map { |value|
33
+ child_data = value[node_key]
34
+ dig_symbol(child_data, node_value)
35
+ }.compact
36
+ else
37
+ result = data
38
+ end
39
+ end
40
+
41
+ dig_data(result, patterns.drop(1))
42
+ end
43
+
44
+ def parse_dig_pattern(*pattern)
45
+ pattern_config = pattern.map do |pattern_item|
46
+ if pattern_item.is_a? Symbol
47
+ {
48
+ type: :symbol,
49
+ key: pattern_item
50
+ }
51
+ elsif pattern_item.is_a? Hash
52
+ pattern_item.map do |key, value|
53
+ {
54
+ type: :array,
55
+ key: key,
56
+ value: value[0]
57
+ }
58
+ end
59
+ end
60
+ end
61
+
62
+ pattern_config.flatten
63
+ end
64
+
65
+ def dig_symbol(data, key)
66
+ key = key.to_s if key.is_a? Symbol
67
+ return data[key] if data.is_a? Hash
68
+
69
+ if data.is_a? Array
70
+ if key.is_a? Numeric
71
+ mapped_data = data[key]
72
+ else
73
+ mapped_data = data.map { |value| value[key] }.flatten
74
+ end
75
+
76
+ return mapped_data
77
+ end
78
+
79
+ return data
80
+ end
81
+ end
82
+ end
83
+ end
@@ -1,23 +1,51 @@
1
1
  module RSpec
2
2
  module GraphQLResponse
3
- def self.add_helper(name, &helper)
3
+ def self.add_context_helper(name, &helper)
4
+ self.add_helper(name, scope: :context, &helper)
5
+ end
6
+
7
+ def self.add_helper(name, scope: :spec, &helper)
4
8
  helper_module = Module.new do |mod|
5
- mod.define_method(name) do |*args|
6
- @result ||= self.instance_exec(*args, &helper)
9
+ mod.define_method(name) do |*args, &block|
10
+ instance_var = "@#{name}".to_sym
11
+
12
+ if self.instance_variables.include? instance_var
13
+ return self.instance_variable_get(instance_var)
14
+ end
15
+
16
+ args << block
17
+ result = self.instance_exec(*args, &helper)
18
+ self.instance_variable_set(instance_var, result)
7
19
  end
8
20
  end
9
21
 
10
22
  RSpec.configure do |config|
11
23
  config.after(:each) do
12
- helper_module.instance_variable_set(:@result, nil)
24
+ instance_var = "@#{name}".to_sym
25
+ helper_module.instance_variable_set(instance_var, nil)
13
26
  end
14
- end
15
27
 
16
- self.include(helper_module)
28
+ module_method = if scope == :spec
29
+ :include
30
+ elsif scope == :context
31
+ :extend
32
+ else
33
+ raise ArgumentError, "A helper method's scope must be either :spec or :describe"
34
+ end
35
+
36
+ config.send(module_method, helper_module, type: :graphql)
37
+ end
17
38
  end
18
39
  end
19
40
  end
20
41
 
42
+ # describe level helpers
43
+ require_relative "helpers/graphql_context"
44
+ require_relative "helpers/graphql_operation"
45
+ require_relative "helpers/graphql_variables"
46
+
47
+ # spec level helpers
48
+ require_relative "helpers/execute_graphql"
21
49
  require_relative "helpers/operation"
22
50
  require_relative "helpers/response"
23
- require_relative "helpers/execute_graphql"
51
+ require_relative "helpers/response_data"
@@ -1,4 +1,17 @@
1
1
  RSpec::GraphQLResponse.add_helper :execute_graphql do
2
2
  config = RSpec::GraphQLResponse.configuration
3
- config.graphql_schema.execute(query)
3
+
4
+ operation = graphql_operation if respond_to? :graphql_operation
5
+ operation = self.instance_eval(&graphql_operation) if operation.is_a? Proc
6
+
7
+ operation_vars = graphql_variables if respond_to? :graphql_variables
8
+ operation_vars = self.instance_eval(&graphql_variables) if operation_vars.is_a? Proc
9
+
10
+ operation_context = graphql_context if respond_to? :graphql_context
11
+ operation_context = self.instance_eval(&operation_context) if operation_context.is_a? Proc
12
+
13
+ config.graphql_schema.execute(operation, {
14
+ variables: operation_vars,
15
+ context: operation_context
16
+ })
4
17
  end
@@ -0,0 +1,3 @@
1
+ RSpec::GraphQLResponse.add_context_helper :graphql_context do |ctx|
2
+ self.define_method(:graphql_context) { ctx }
3
+ end
@@ -0,0 +1,3 @@
1
+ RSpec::GraphQLResponse.add_context_helper :graphql_operation do |gql|
2
+ self.define_method(:graphql_operation) { gql }
3
+ end
@@ -0,0 +1,3 @@
1
+ RSpec::GraphQLResponse.add_context_helper :graphql_variables do |vars|
2
+ self.define_method(:graphql_variables) { vars }
3
+ end
@@ -1,4 +1,5 @@
1
1
  RSpec::GraphQLResponse.add_helper :operation do |name|
2
+ warn 'WARNING: operation has been deprecated in favor of response_data. This helper will be removed in v0.5'
2
3
  return nil unless response.is_a? Hash
3
4
 
4
5
  response.dig("data", name.to_s)
@@ -0,0 +1,13 @@
1
+ RSpec::GraphQLResponse.add_helper :response_data do |*fields|
2
+ next nil unless response.is_a? Hash
3
+
4
+ response_data = response["data"]
5
+ next nil if response_data.nil?
6
+ next nil if response_data.empty?
7
+
8
+ fields = fields.compact
9
+ next response_data if fields.empty?
10
+
11
+ dig_dug = RSpec::GraphQLResponse::DigDug.new(*fields)
12
+ dig_dug.dig(response_data)
13
+ end
@@ -12,3 +12,4 @@ module RSpec
12
12
  end
13
13
 
14
14
  require_relative "matchers/have_errors"
15
+ require_relative "matchers/have_operation"
@@ -11,7 +11,7 @@ RSpec::GraphQLResponse.add_matcher :have_errors do |count = nil|
11
11
  @result.valid?
12
12
  end
13
13
 
14
- failure_message do |response|
14
+ failure_message do |_|
15
15
  @result.reason
16
16
  end
17
17
 
@@ -27,7 +27,7 @@ RSpec::GraphQLResponse.add_matcher :have_errors do |count = nil|
27
27
  @result.valid?
28
28
  end
29
29
 
30
- failure_message_when_negated do |response|
30
+ failure_message_when_negated do |_|
31
31
  @result.reason
32
32
  end
33
33
 
@@ -0,0 +1,23 @@
1
+ RSpec::GraphQLResponse.add_matcher :have_operation do |operation_name|
2
+ match do |response|
3
+ validator = RSpec::GraphQLResponse.validator(:have_operation)
4
+
5
+ @result = validator.validate(response, operation_name: operation_name)
6
+ @result.valid?
7
+ end
8
+
9
+ failure_message do |_|
10
+ @result.reason
11
+ end
12
+
13
+ match_when_negated do |response|
14
+ validator = RSpec::GraphQLResponse.validator(:have_operation)
15
+
16
+ @result = validator.validate_negated(response, operation_name: operation_name)
17
+ @result.valid?
18
+ end
19
+
20
+ failure_message_when_negated do |_|
21
+ @result.reason
22
+ end
23
+ end
@@ -11,6 +11,11 @@ module RSpec
11
11
  @validators[name] = validator_class
12
12
  end
13
13
 
14
+ def self.remove_validator(name)
15
+ @validators ||= {}
16
+ @validators.delete(:key)
17
+ end
18
+
14
19
  def self.validator(name)
15
20
  @validators[name].new
16
21
  end
@@ -18,3 +23,4 @@ module RSpec
18
23
  end
19
24
 
20
25
  require_relative "validators/have_errors"
26
+ require_relative "validators/have_operation"
@@ -0,0 +1,24 @@
1
+ RSpec::GraphQLResponse.add_validator :have_operation do
2
+ failure_message :nil, "Cannot evaluate operations on nil"
3
+ failure_message :not_found, ->(expected, actual) { "Expected to find operation result named #{expected}, but did not find it\n\t#{actual}" }
4
+
5
+ validate do |response, operation_name:|
6
+ next fail_validation(:nil) unless response.is_a? Hash
7
+
8
+ op = response.dig("data", operation_name.to_s)
9
+ next fail_validation(:not_found, operation_name, response) if op.nil?
10
+
11
+ pass_validation
12
+ end
13
+
14
+ failure_message :found, ->(expected, actual) { "Expected not to find operation result named #{expected}, but found it\n\t#{actual}" }
15
+
16
+ validate_negated do |response, operation_name:|
17
+ next fail_validation(:nil) unless response.is_a? Hash
18
+
19
+ op = response.dig("data", operation_name.to_s)
20
+ next fail_validation(:found, operation_name, response) unless op.nil?
21
+
22
+ pass_validation
23
+ end
24
+ end