Skip to content

Commit

Permalink
Merge pull request #50 from khamusa/graphql-ruby-2-compatibility
Browse files Browse the repository at this point in the history
Graphql 2.0+ compatibility
  • Loading branch information
khamusa authored Apr 16, 2023
2 parents 7d8a79a + 8ab63d5 commit 39f0429
Show file tree
Hide file tree
Showing 17 changed files with 134 additions and 101 deletions.
27 changes: 27 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,32 @@
# Changelog

## 2.0.0 (April 16th, 2023)

- Adds compatibility with graphql-ruby 2.0+. If you're still using an earlier version, you should stick to 1.4.x.

### Deprecations

- The usage of the `#types` helper when writing tests (if you're not including `RSpec::GraphqlMatchers::TypesHelper` anywhere, you shoudn't have to change anything). Use `GraphQL::Types::<TYPE_NAME>` instead
- The usage of type instances as expected value for a type comparison is deprecated. Instead,
use the string that represents the type specification. Examples:

```
# Bad - These are deprecated:
expect(a_type).to have_a_field(:lorem).of_type(GraphQL::Types::ID)
expect(a_type).to have_a_field(:lorem).of_type(Types::MyCustomType)
# Good - Use these instead
expect(a_type).to have_a_field(:lorem).of_type('ID')
expect(a_type).to have_a_field(:lorem).of_type('MyCustomType')
```

The reason behind this change relies on the fact that we should have a decoupling between the
internal constants used to define our API and the public names exposed through it. If we test
a published API using the internal constants, it is possible to perform breaking changes by
renaming the graphql names of our types or entities without actually breaking the tests.

If we're performing a breaking change to a public API, we want our tests to fail.

## 1.4.0 (April 16th, 2023)

- Removal of deprecated calls to #to_graphql, replacing them with #to_type_signature that was added in graphql-ruby 1.8.3 (https://github.com/khamusa/rspec-graphql_matchers/pull/43 @RobinDaugherty)
Expand Down
16 changes: 6 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,9 @@ The matchers currently supported are:
- `expect(a_field).to be_of_type(valid_type)`
- `expect(an_input).to accept_argument(argument_name).of_type(valid_type)`

Where a valid type for the expectation is either:
Where `valid_type` is a your type signature as a String: `"String!"`, `"Int!"`, `"[String]!"` (note the exclamation mark at the end, as required by the [GraphQL specifications](http://graphql.org/).

- A reference to the actual type you expect;
- [Recommended] A String representation of a type: `"String!"`, `"Int!"`, `"[String]!"`
(note the exclamation mark at the end, as required by the [GraphQL specifications](http://graphql.org/).
Please note that using references to type instances is deprecated and will be removed in a future release.

## Examples

Expand All @@ -44,9 +42,9 @@ class PostType < GraphQL::Schema::Object
field :published, Boolean, null: false, deprecation_reason: 'Use isPublished instead'

field :subposts, PostType, null: true do
argument :filter, types.String, required: false
argument :id, types.ID, required: false
argument :isPublished, types.Boolean, required: false
argument :filter, String, required: false
argument :id, ID, required: false
argument :isPublished, Boolean, required: false
end
end
```
Expand All @@ -57,7 +55,7 @@ end
describe PostType do
subject { described_class }

it { is_expected.to have_field(:id).of_type(!types.ID) }
it { is_expected.to have_field(:id).of_type("ID!") }
it { is_expected.to have_field(:comments).of_type("[String!]!") }
it { is_expected.to have_field(:isPublished).of_type("Boolean") }

Expand Down Expand Up @@ -123,8 +121,6 @@ describe PostType do
expect(subject).to implement('Node')
end

# Accepts arguments as an array and type objects directly
it { is_expected.to implement(GraphQL::Types::Relay::Node) }
it { is_expected.not_to implement('OtherInterface') }
end
```
Expand Down
2 changes: 1 addition & 1 deletion lib/rspec/graphql_matchers/accept_arguments.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def matches_argument?(arg_name, arg_type)

def describe_arguments(what_args)
what_args.sort.map do |arg_name, arg_type|
"#{arg_name}(#{arg_type})"
"#{arg_name}(#{type_name(arg_type)})"
end.join(', ')
end
end
Expand Down
3 changes: 2 additions & 1 deletion lib/rspec/graphql_matchers/be_of_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ def matches?(actual_sample)
end

def failure_message
"expected field '#{member_name(sample)}' to be of type '#{expected}', " \
"expected field '#{member_name(sample)}' to " \
"be of type '#{type_name(expected)}', " \
"but it was '#{type_name(sample.type)}'"
end

Expand Down
5 changes: 2 additions & 3 deletions lib/rspec/graphql_matchers/have_a_field.rb
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,8 @@ def field_collection

def matcher_name
case @fields
when :fields then 'have_a_field'
when :input_fields then 'have_an_input_field'
when :return_fields then 'have_a_return_field'
when :fields then 'have_a_field'
when :arguments then 'have_an_input_field'
end
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ module GraphqlMatchers
module HaveAFieldMatchers
class OfType < RSpec::GraphqlMatchers::BeOfType
def description
"of type `#{expected}`"
"of type `#{type_name(expected)}`"
end

def failure_message
Expand Down
4 changes: 2 additions & 2 deletions lib/rspec/graphql_matchers/matchers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,12 @@ def have_a_field(field_name)
alias have_field have_a_field

def have_an_input_field(field_name)
RSpec::GraphqlMatchers::HaveAField.new(field_name, :input_fields)
RSpec::GraphqlMatchers::HaveAField.new(field_name, :arguments)
end
alias have_input_field have_an_input_field

def have_a_return_field(field_name)
RSpec::GraphqlMatchers::HaveAField.new(field_name, :return_fields)
RSpec::GraphqlMatchers::HaveAField.new(field_name)
end
alias have_return_field have_a_return_field
# rubocop:enable Naming/PredicateName
Expand Down
14 changes: 13 additions & 1 deletion lib/rspec/graphql_matchers/types_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,20 @@
module RSpec
module GraphqlMatchers
module TypesHelper
class << self
extend Gem::Deprecate

GraphQL::Types.constants.each do |constant_name|
klass = GraphQL::Types.const_get(constant_name)

define_method(constant_name) { klass }

deprecate constant_name, "GraphQL::Types::#{constant_name}", 2023, 10
end
end

def types
GraphQL::Define::TypeDefiner.instance
TypesHelper
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion lib/rspec/graphql_matchers/version.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@

module Rspec
module GraphqlMatchers
VERSION = '1.4.0'.freeze
VERSION = '2.0.0-rc.0'.freeze
end
end
5 changes: 1 addition & 4 deletions rspec-graphql_matchers.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,6 @@ Gem::Specification.new do |spec|
spec.homepage = 'https://github.com/khamusa/rspec-graphql_matchers'
spec.license = 'MIT'

# raise 'RubyGems 2.0 or newer is required to protect against public gem ' \
# 'pushes.'

spec.files =
`git ls-files -z`
.split("\x0")
Expand All @@ -25,7 +22,7 @@ Gem::Specification.new do |spec|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
spec.require_paths = ['lib']

spec.add_dependency 'graphql', '>= 1.10.12', '< 2.0'
spec.add_dependency 'graphql', '~> 2.0'
spec.add_dependency 'rspec', '~> 3.0'
spec.add_development_dependency 'bundler', '~> 2.0'
# CodeClimate does not yet support SimpleCov 0.18
Expand Down
6 changes: 3 additions & 3 deletions spec/rspec/accept_argument_matcher_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ module GraphqlMatchers

it 'passes when the type defines the field with correct type as ' \
'graphql objects' do
expect(a_type).to accept_argument(:id).of_type(types.String)
expect(a_type).to accept_argument(:id).of_type(GraphQL::Types::String)
expect(a_type).to accept_argument('other').of_type('ID!')
end

Expand All @@ -79,13 +79,13 @@ module GraphqlMatchers
'of type `String!`, but it was `String`'
)

expect { expect(a_type).to accept_argument('other').of_type(!types.Int) }
expect { expect(a_type).to accept_argument('other').of_type(GraphQL::Types::Int.to_non_null_type) }
.to fail_with(
'expected TestObject to accept argument `other` ' \
'of type `Int!`, but it was `ID!`'
)

expect { expect(a_type).to accept_argument('other' => !types.Int) }
expect { expect(a_type).to accept_argument('other' => GraphQL::Types::Int.to_non_null_type) }
.to fail_with(
'expected TestObject to accept argument `other` ' \
'of type `Int!`, but it was `ID!`'
Expand Down
32 changes: 16 additions & 16 deletions spec/rspec/accept_arguments_matcher_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,37 +18,37 @@

describe '#matches?' do
context 'when expecting a single argument with type' do
let(:expected_args) { { id: !types.ID } }
let(:expected_args) { { id: GraphQL::Types::ID.to_non_null_type } }

context 'when the field accepts the expected argument name and type' do
it { is_expected.to accept_arguments(expected_args) }
end

context 'the field accepts an argument with the same name but different type' do
let(:expected_args) { { id: types.ID } }
let(:expected_args) { { id: GraphQL::Types::ID } }

it { is_expected.not_to accept_arguments(expected_args) }
end

context 'the field does not accept the expected args' do
let(:expected_args) { { idz: !types.ID } }
let(:expected_args) { { idz: GraphQL::Types::ID.to_non_null_type } }

it { is_expected.not_to accept_arguments(expected_args) }
end

context 'when the expected argument is camelcase' do
let(:expected_args) { { isTest: types.Boolean } }
let(:expected_args) { { isTest: GraphQL::Types::Boolean } }

it { is_expected.to accept_arguments(expected_args) }
end

context 'when the expected argument is underscored' do
let(:expected_args) { { is_test: types.Boolean } }
let(:expected_args) { { is_test: GraphQL::Types::Boolean } }

it { is_expected.to accept_arguments(expected_args) }

context 'when the actual argument is not camelized' do
let(:expected_args) { { not_camelized: types.Boolean } }
let(:expected_args) { { not_camelized: GraphQL::Types::Boolean } }

it { is_expected.to accept_arguments(expected_args) }
end
Expand All @@ -59,9 +59,9 @@
context 'when the field accepts only one argument with correct name and type' do
let(:expected_args) do
{
id: !types.ID,
age: types[types.Int],
name: types.String
id: GraphQL::Types::ID.to_non_null_type,
age: GraphQL::Types::Int.to_list_type,
name: GraphQL::Types::String
}
end

Expand All @@ -71,9 +71,9 @@
context 'when the field accepts all but one of the argument expected args' do
let(:expected_args) do
{
id: !types.ID,
age: types.Int,
name: !types.Float
id: GraphQL::Types::ID.to_non_null_type,
age: GraphQL::Types::Int,
name: GraphQL::Types::Float.to_non_null_type
}
end

Expand All @@ -83,9 +83,9 @@
context 'when the field accepts all arguments with correct type' do
let(:expected_args) do
{
id: !types.ID,
age: types.Int,
name: !types.String
id: GraphQL::Types::ID.to_non_null_type,
age: GraphQL::Types::Int,
name: GraphQL::Types::String.to_non_null_type
}
end

Expand Down Expand Up @@ -113,7 +113,7 @@

context 'with multiple expected arguments with types specified' do
let(:expected_args) do
{ ability: types.Int, id: types.Int, some: types.Boolean }
{ ability: GraphQL::Types::Int, id: GraphQL::Types::Int, some: GraphQL::Types::Boolean }
end

it 'describes the arguments the field should accept and their types' do
Expand Down
34 changes: 22 additions & 12 deletions spec/rspec/be_of_type_matcher_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,38 @@
require 'spec_helper'

describe 'expect(a_field).to be_of_type(graphql_type)' do
scalar_types = [types.Boolean, types.Int, types.Float, types.String, types.ID]
list_types = scalar_types.map { |t| types[t] }
all_types = scalar_types + scalar_types.map(&:'!') + list_types.map(&:'!')
scalar_types = {
"Boolean" => GraphQL::Types::Boolean,
"Int" => GraphQL::Types::Int,
"Float" => GraphQL::Types::Float,
"String" => GraphQL::Types::String,
"ID" => GraphQL::Types::ID
}

non_nullable_scalar_types = scalar_types.each_with_object({}) do |(string_name, type), result|
result["#{string_name}!"] = type.to_non_null_type
end

all_types = scalar_types.merge(non_nullable_scalar_types)

all_types.each do |scalar_type|
context "when the field has type #{scalar_type}" do
all_types.each do |(graphql_name, scalar_type)|
context "when the field has type #{graphql_name}" do
subject(:field) { double('GrahQL Field', type: field_type) }
let(:field_type) { scalar_type }

it "matches a graphQL type object representing #{scalar_type}" do
it "matches a graphQL type object representing #{graphql_name}" do
expect(field).to be_of_type(scalar_type)
end

it "matches the string '#{scalar_type}'" do
expect(field).to be_of_type(scalar_type.to_s)
it "matches the string '#{graphql_name}'" do
expect(field).to be_of_type(graphql_name)
end

it "does not match the string '#{scalar_type.to_s.downcase}'" do
expect(field).not_to be_of_type(scalar_type.to_s.downcase)
it "does not match the string '#{graphql_name.downcase}'" do
expect(field).not_to be_of_type(graphql_name.downcase)
end

scalar_types.each do |another_scalar|
scalar_types.each do |(another_graphql_name, another_scalar)|
next if another_scalar == scalar_type

context "when matching against the type #{another_scalar}" do
Expand All @@ -42,7 +52,7 @@

it 'informs the expected and actual types' do
expect(failure_message).to end_with(
"to be of type '#{expected_type}', but it was '#{field.type}'"
"to be of type '#{another_graphql_name}', but it was '#{graphql_name}'"
)
end

Expand Down
Loading

0 comments on commit 39f0429

Please sign in to comment.