diff --git a/Gemfile.lock b/Gemfile.lock index 70cec2b..1e3b444 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,18 +1,22 @@ PATH remote: . specs: - rspec-graphql_response (0.4.0) + rspec-graphql_response (0.4.1) graphql (>= 1.0) rspec (>= 3.0) GEM remote: https://rubygems.org/ specs: + attr_extras (6.2.4) byebug (11.1.3) coderay (1.1.3) diff-lcs (1.4.4) - graphql (1.12.5) + graphql (1.12.6) method_source (1.0.0) + optimist (3.0.1) + patience_diff (1.2.0) + optimist (~> 3.0) pry (0.14.0) coderay (~> 1.1) method_source (~> 1.0) @@ -33,6 +37,10 @@ GEM diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.10.0) rspec-support (3.10.2) + super_diff (0.6.1) + attr_extras (>= 6.2.4) + diff-lcs + patience_diff PLATFORMS ruby @@ -43,6 +51,7 @@ DEPENDENCIES pry-byebug (~> 3.8) rake (>= 12.0) rspec-graphql_response! + super_diff (~> 0.6) BUNDLED WITH 1.17.2 diff --git a/README.md b/README.md index 14c71cc..31bfd61 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ Spec Helper Methods: - [execute_graphql](/docs/execute_graphql.md) - executes a graphql call with the registered schema, query, variables and context - [response](/docs/response.md) - the response, as JSON, of the executed graphql query -- [operation](/docs/operation.md) - retrieves the results of a named operation from the GraphQL response +- [response_data](/docs/response_data.md) - digs through the graphql response to return data from the specified node(s) API / Development diff --git a/docs/operation.md b/docs/operation.md index 1122e36..44288a9 100644 --- a/docs/operation.md +++ b/docs/operation.md @@ -1,38 +1,3 @@ # Using the `operation` helper -The `operation` helper will dig through a response to find a data -structure that looks like, - -```ruby -{ - "data" => { - operation_name - } -} -``` - -## Basic Use - -```ruby -it "has characters" do - characters = operation(:characters) - - expect(character).to include( - { id: 1, name: "Jam" }, - # ... - ) -end -``` - -## Handling Nil - -If there is no `"data"` or no named operation for the name supplied, the -`operation` helper will return `nil` - -```ruby -it "returns nil if operation doesn't exist" do - character = operation(:something_that_does_not_exist) - - expect(operation).to be_nil -end -``` +Deprecated. See [response_data](response_data.md) instead. diff --git a/docs/response.md b/docs/response.md index f17a706..564a764 100644 --- a/docs/response.md +++ b/docs/response.md @@ -1 +1 @@ -# Check the GraphQL Response with Helper `response` +# The GraphQL Response, via Helper `response` diff --git a/docs/response_data.md b/docs/response_data.md new file mode 100644 index 0000000..dae274b --- /dev/null +++ b/docs/response_data.md @@ -0,0 +1,146 @@ +# Using the `response_data` Helper + +The `response_data` helper will dig through a graphql response, through +the outer hash, into the response data for an operation, and through any +and all layers of hash and array. + +## Syntax + +```ruby +response_data *[dig_pattern] +``` + +Data returned via this helper will assume a `"data" => ` key at the root of +the `response` object. This root does not need to be specified in the list +of attributes for the `dig_pattern`. + +### Params + +* `*[dig_pattern]` - an array of attributes (`:symbol`, `"string"`, or `key: :value` pair) that describes +the data structure to dig through, and the final data set to retrieve from the graphql response. + +#### dig_pattern + +Each attribute added to the `dig_pattern` represents an attribute at the given level of the +data structure, in numeric order from left to right. The first attribute provides will dig into +that attribute at the first level of data (just below the `"data" =>` key). The second attribute +will dig through data just below that first level, etc. etc. etc. + +For example, with a data structure as shown below, in "Basic Use", you could specifiy these +attributes for the dig pattern: + +* :characters +* :name + +Like this: + +```ruby +response_data :characters, :name +``` + +This dig pattern will find the `"characters"` key just below `"data"`, then iterate through +the array of characters and retrieve the `"name"` of each character. + +For more details and options for the dig pattern, see the examples below. + +## Basic Use + +A `response` data structure may look something like the following. + +```ruby +{ + "data" => { + "characters" => [ + { "id" => "1", "name" => "Jam" }, + { "id" => "2", "name" => "Redemption" }, + { "id" => "3", "name" => "Pet" } + ] + } +} +``` + +The `response_data` helper will dig through to give you simplified +results that are easier to verify. + +For example, if only the names of the characters need to be checked: + +```ruby +response_data :characters, :name + +# => ["Jam", "Redemption", "Pet"] +``` + +Or perhaps only the name for 2nd character is needed: + +```ruby +response_data {characters: [1]}, :name + +# => "Redemption" +``` + +## List Every Item in an Array + +Many responses from a graphql call will include an array of data somewhere +in the data structure. If you need to return all of the items in an array, +you only need to specify that array's key: + +```ruby +it "has characters" do + characters = response_data(:characters) + + expect(character).to include( + { id: 1, name: "Jam" }, + # ... + ) +end +``` + +## Dig a Field From Every Item in an Array + +When validation only needs to occur on a specific field for items found in +an array, there are two options. + +1. Specify a list of fields as already shown +2. change the array's key to a hash and provide a `:symbol` wrapped in an array as the value + +The first option was already shown in the Basic Use section above. + +```ruby +response_data :characters, :name + +# => ["Jam", "Redemption", "Pet"] +``` + +For the second option, the code would look like this: + +```ruby +response_data characters: [:name] + +# => ["Jam", "Redemption", "Pet"] +``` + +Both of these options are functionaly the same. The primary difference will be +how you wish to express the data structure in your code. Changing the list of +attributes to a hash with an array wrapping the value will provide a better +indication that an array is expected at that point in the data structure. + +## Dig Out an Item By Index, From an Array + +There may be times when only a single piece of a returned array needs to be +validated. To handle this, switch the key of the array to a hash, as in the +previous example. Rather than specifying a child node's key in the value, though, +specify the index of the item you wish to extract. + +```ruby +response_data characters: [1] +``` + +This will return the character at index 1, from the array of characters. + +## Handling Nil + +If there is no data the key supplied, the helper will return `nil` + +```ruby +response_data(:something_that_does_not_exist) #=> nil +``` diff --git a/lib/rspec/graphql_response.rb b/lib/rspec/graphql_response.rb index c024f03..0379e09 100644 --- a/lib/rspec/graphql_response.rb +++ b/lib/rspec/graphql_response.rb @@ -1,5 +1,6 @@ require "rspec" +require_relative "graphql_response/dig_dug/dig_dug" require_relative "graphql_response/version" require_relative "graphql_response/configuration" require_relative "graphql_response/validators" diff --git a/lib/rspec/graphql_response/dig_dug/dig_dug.rb b/lib/rspec/graphql_response/dig_dug/dig_dug.rb new file mode 100644 index 0000000..aa004d9 --- /dev/null +++ b/lib/rspec/graphql_response/dig_dug/dig_dug.rb @@ -0,0 +1,83 @@ +module RSpec + module GraphQLResponse + class DigDug + attr_reader :dig_pattern + + def initialize(*dig_pattern) + @dig_pattern = parse_dig_pattern(*dig_pattern) + end + + def dig(data) + dig_data(data, dig_pattern) + end + + private + + def dig_data(data, patterns) + return data if patterns.nil? + return data if patterns.empty? + + node = patterns[0] + node_key = node[:key] + node_key = node_key.to_s if node_key.is_a? Symbol + node_value = node[:value] + + if node[:type] == :symbol + result = dig_symbol(data, node_key) + elsif node[:type] == :array + if data.is_a? Hash + child_data = data[node_key] + result = dig_symbol(child_data, node_value) + elsif data.is_a? Array + result = data.map { |value| + child_data = value[node_key] + dig_symbol(child_data, node_value) + }.compact + else + result = data + end + end + + dig_data(result, patterns.drop(1)) + end + + def parse_dig_pattern(*pattern) + pattern_config = pattern.map do |pattern_item| + if pattern_item.is_a? Symbol + { + type: :symbol, + key: pattern_item + } + elsif pattern_item.is_a? Hash + pattern_item.map do |key, value| + { + type: :array, + key: key, + value: value[0] + } + end + end + end + + pattern_config.flatten + end + + def dig_symbol(data, key) + key = key.to_s if key.is_a? Symbol + return data[key] if data.is_a? Hash + + if data.is_a? Array + if key.is_a? Numeric + mapped_data = data[key] + else + mapped_data = data.map { |value| value[key] }.flatten + end + + return mapped_data + end + + return data + end + end + end +end diff --git a/lib/rspec/graphql_response/helpers.rb b/lib/rspec/graphql_response/helpers.rb index ea57184..ac295f2 100644 --- a/lib/rspec/graphql_response/helpers.rb +++ b/lib/rspec/graphql_response/helpers.rb @@ -40,11 +40,12 @@ def self.add_helper(name, scope: :spec, &helper) end # describe level helpers +require_relative "helpers/graphql_context" require_relative "helpers/graphql_operation" require_relative "helpers/graphql_variables" -require_relative "helpers/graphql_context" # spec level helpers +require_relative "helpers/execute_graphql" require_relative "helpers/operation" require_relative "helpers/response" -require_relative "helpers/execute_graphql" +require_relative "helpers/response_data" diff --git a/lib/rspec/graphql_response/helpers/operation.rb b/lib/rspec/graphql_response/helpers/operation.rb index 2bb2a70..d320687 100644 --- a/lib/rspec/graphql_response/helpers/operation.rb +++ b/lib/rspec/graphql_response/helpers/operation.rb @@ -1,4 +1,5 @@ RSpec::GraphQLResponse.add_helper :operation do |name| + warn 'WARNING: operation has been deprecated in favor of response_data. This helper will be removed in v0.5' return nil unless response.is_a? Hash response.dig("data", name.to_s) diff --git a/lib/rspec/graphql_response/helpers/response_data.rb b/lib/rspec/graphql_response/helpers/response_data.rb new file mode 100644 index 0000000..961669b --- /dev/null +++ b/lib/rspec/graphql_response/helpers/response_data.rb @@ -0,0 +1,13 @@ +RSpec::GraphQLResponse.add_helper :response_data do |*fields| + next nil unless response.is_a? Hash + + response_data = response["data"] + next nil if response_data.nil? + next nil if response_data.empty? + + fields = fields.compact + next response_data if fields.empty? + + dig_dug = RSpec::GraphQLResponse::DigDug.new(*fields) + dig_dug.dig(response_data) +end diff --git a/lib/rspec/graphql_response/version.rb b/lib/rspec/graphql_response/version.rb index 8b7b65e..06e5b21 100644 --- a/lib/rspec/graphql_response/version.rb +++ b/lib/rspec/graphql_response/version.rb @@ -1,5 +1,5 @@ module RSpec module GraphQLResponse - VERSION = "0.4.0" + VERSION = "0.4.1" end end diff --git a/rspec-graphql_response.gemspec b/rspec-graphql_response.gemspec index 46c0263..c99772c 100644 --- a/rspec-graphql_response.gemspec +++ b/rspec-graphql_response.gemspec @@ -29,6 +29,7 @@ Gem::Specification.new do |spec| spec.add_development_dependency "rake", ">= 12.0" spec.add_development_dependency "pry", "~> 0.14" spec.add_development_dependency "pry-byebug", "~> 3.8" + spec.add_development_dependency "super_diff", "~> 0.6" spec.add_runtime_dependency "rspec", ">= 3.0" spec.add_runtime_dependency "graphql", ">= 1.0" diff --git a/spec/graphql/queries/characters.rb b/spec/graphql/queries/characters.rb index b6ab785..4b2c6f8 100644 --- a/spec/graphql/queries/characters.rb +++ b/spec/graphql/queries/characters.rb @@ -8,15 +8,25 @@ def resolve(name: nil) data = [ { "id" => "1", - "name" => "Jam" + "name" => "Jam", + "friends" => [ + { "id" => "2", "name" => "Redemption" } + ] }, { "id" => "2", - "name" => "Redemption" + "name" => "Redemption", + "friends" => [ + { "id" => "1", "name" => "Jam" }, + { "id" => "3", "name" => "Pet" } + ] }, { "id" => "3", - "name" => "Pet" + "name" => "Pet", + "friends" => [ + { "id" => "2", "name" => "Redemption" } + ] } ] diff --git a/spec/graphql/types/response/character.rb b/spec/graphql/types/response/character.rb index 001a02e..d7e4bef 100644 --- a/spec/graphql/types/response/character.rb +++ b/spec/graphql/types/response/character.rb @@ -5,6 +5,7 @@ class Character < GraphQL::Schema::Object field :id, ID, null: false field :name, String, null: false + field :friends, [Character], null: false end end end diff --git a/spec/graphql_response/dig_dug/dig_dug_spec.rb b/spec/graphql_response/dig_dug/dig_dug_spec.rb new file mode 100644 index 0000000..b56eec9 --- /dev/null +++ b/spec/graphql_response/dig_dug/dig_dug_spec.rb @@ -0,0 +1,144 @@ +RSpec.describe RSpec::GraphQLResponse::DigDug do + let(:response) do + { + "characters" => [ + { + "id" => "1", + "name" => "Jam", + "friends" => [ + { "id" => "2", "name" => "Redemption" } + ] + }, + { + "id" => "2", + "name" => "Redemption", + "friends" => [ + { "id" => "1", "name" => "Jam" }, + { "id" => "3", "name" => "Pet" } + ] + }, + { + "id" => "3", + "name" => "Pet", + "friends" => [ + { "id" => "2", "name" => "Redemption" } + ] + } + ] + } + end + + let(:dig_pattern) { nil } + + subject(:dig) do + dig_dug = described_class.new(*dig_pattern) + dig_dug.dig(response) + end + + context "dig one layer" do + let(:dig_pattern) { [:characters] } + + it "returns the correct data" do + expect(dig).to include( + { + "id" => "1", + "name" => "Jam", + "friends" => [ + { "id" => "2", "name" => "Redemption" } + ] + }, + { + "id" => "2", + "name" => "Redemption", + "friends" => [ + { "id" => "1", "name" => "Jam" }, + { "id" => "3", "name" => "Pet" } + ] + }, + { + "id" => "3", + "name" => "Pet", + "friends" => [ + { "id" => "2", "name" => "Redemption" } + ] + } + ) + end + end + + context "dig through an array" do + let(:dig_pattern) { [:characters, :friends] } + + it "returns the correct data" do + expect(dig).to include( + { "id" => "2", "name" => "Redemption" }, + { "id" => "1", "name" => "Jam" }, + { "id" => "3", "name" => "Pet" }, + { "id" => "2", "name" => "Redemption" } + ) + end + end + + context "dig through an array to nested fields" do + let(:dig_pattern) { [:characters, :friends, :name] } + + it "returns the correct data" do + expect(dig).to include( + "Redemption", + "Jam", + "Pet" + ) + end + end + + context "dig into an Array at the specified index" do + let(:dig_pattern) { [characters: [1]] } + + it "returns the correct data" do + expect(dig).to eq( + "id" => "2", + "name" => "Redemption", + "friends" => [ + { "id" => "1", "name" => "Jam" }, + { "id" => "3", "name" => "Pet" } + ] + ) + end + end + + context "dig multiple levels into an Array at the specified index" do + let(:dig_pattern) { [characters: [1], friends: [0]] } + + it "returns the correct data" do + expect(dig).to include( + { "id" => "1", "name" => "Jam" } + ) + end + end + + context "dig into a Hash that came through an Array" do + let(:dig_pattern) { [characters: [0], friends: [:name]] } + + it "returns the correct data" do + expect(dig).to eq(["Redemption"]) + end + end + + context "dig indexed item of value from hash that came through an array" do + let(:dig_pattern) { [:characters, friends: [1]] } + + it "returns the correct data" do + expect(dig).to include( + { "id" => "3", "name" => "Pet" } + ) + end + end + + context "dig multiple nested levels of hash and Array" do + let(:dig_pattern) { [:characters, {friends: [1]}, :name] } + + it "returns the correct data" do + expect(dig).to eq ["Pet"] + end + end +end diff --git a/spec/graphql_response/helpers/response_data_spec.rb b/spec/graphql_response/helpers/response_data_spec.rb new file mode 100644 index 0000000..3bede89 --- /dev/null +++ b/spec/graphql_response/helpers/response_data_spec.rb @@ -0,0 +1,114 @@ +RSpec.describe RSpec::GraphQLResponse, "helper#response", type: :graphql do + graphql_operation <<-GQL + query { + characters { + id, + name, + friends { + id + name + } + } + } + GQL + + context "has data returned" do + it "can return the hash" do + expect(response_data).to include( + "characters" => [ + { + "id" => "1", + "name" => "Jam", + "friends" => [ + { "id" => "2", "name" => "Redemption" } + ] + }, + { + "id" => "2", + "name" => "Redemption", + "friends" => [ + { "id" => "1", "name" => "Jam" }, + { "id" => "3", "name" => "Pet" } + ] + }, + { + "id" => "3", + "name" => "Pet", + "friends" => [ + { "id" => "2", "name" => "Redemption" } + ] + } + ] + ) + end + + it "can dig to the first layer" do + expect(response_data :characters).to include( + { + "id" => "1", + "name" => "Jam", + "friends" => [ + { "id" => "2", "name" => "Redemption" } + ] + }, + { + "id" => "2", + "name" => "Redemption", + "friends" => [ + { "id" => "1", "name" => "Jam" }, + { "id" => "3", "name" => "Pet" } + ] + }, + { + "id" => "3", + "name" => "Pet", + "friends" => [ + { "id" => "2", "name" => "Redemption" } + ] + } + ) + end + + it "can dig through an array" do + expect(response_data :characters, :friends).to include( + { "id" => "2", "name" => "Redemption" }, + { "id" => "1", "name" => "Jam" }, + { "id" => "3", "name" => "Pet" }, + { "id" => "2", "name" => "Redemption" } + ) + end + + it "can dig through an array to nested fields" do + expect(response_data :characters, :friends, :name).to include( + "Redemption", + "Jam", + "Pet" + ) + end + + it "can dig into an Array at the specified index" do + expect(response_data characters: [1]).to eq( + "id" => "2", + "name" => "Redemption", + "friends" => [ + { "id" => "1", "name" => "Jam" }, + { "id" => "3", "name" => "Pet" } + ] + ) + end + + it "can dig multiple levels into an Array at the specified index" do + expect(response_data characters: [1], friends: [0]).to include( + { "id" => "1", "name" => "Jam" }, + ) + end + + it "can dig into a Hash that came through an Array" do + expect(response_data characters: [0], friends: [:name]).to eq(["Redemption"]) + end + + it "can dig multiple nested levels of hash and Array" do + expect(response_data(:characters, {friends: [1]}, :name)).to eq(["Pet"]) + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index db09e26..fcc753a 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,5 +1,5 @@ require "pry-byebug" - +require "super_diff/rspec" require "graphql" require "graphql/example_schema" require "rspec/graphql_response"