├── VERSION ├── .gitignore ├── Gemfile ├── Rakefile ├── .github ├── ISSUE_TEMPLATE.md ├── workflows │ └── ci.yml └── PULL_REQUEST_TEMPLATE.md ├── lib └── jsonapi │ ├── rspec │ ├── id.rb │ ├── type.rb │ ├── jsonapi_object.rb │ ├── errors.rb │ ├── meta.rb │ ├── links.rb │ ├── relationships.rb │ └── attributes.rb │ └── rspec.rb ├── spec ├── jsonapi │ ├── id_spec.rb │ ├── type_spec.rb │ ├── links_spec.rb │ ├── rspec_spec.rb │ ├── errors_spec.rb │ ├── jsonapi_object_spec.rb │ ├── meta_spec.rb │ ├── relationships_spec.rb │ └── attributes_spec.rb └── spec_helper.rb ├── .rubocop.yml ├── jsonapi-rspec.gemspec ├── LICENSE └── README.md /VERSION: -------------------------------------------------------------------------------- 1 | 0.0.11 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.lock 2 | coverage 3 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'rspec/core/rake_task' 3 | require 'rubocop/rake_task' 4 | 5 | RSpec::Core::RakeTask.new 6 | RuboCop::RakeTask.new 7 | 8 | task default: %i[rubocop spec] 9 | task test: :spec 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Expected Behavior 2 | 3 | 4 | ## Actual Behavior 5 | 6 | 7 | ## Steps to Reproduce the Problem 8 | 9 | 1. 10 | 2. 11 | 3. 12 | 13 | ## Specifications 14 | 15 | - Version: 16 | - Ruby version: 17 | -------------------------------------------------------------------------------- /lib/jsonapi/rspec/id.rb: -------------------------------------------------------------------------------- 1 | module JSONAPI 2 | module RSpec 3 | module Id 4 | ::RSpec::Matchers.define :have_id do |expected| 5 | match do |actual| 6 | JSONAPI::RSpec.as_indifferent_hash(actual)['id'] == expected 7 | end 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/jsonapi/rspec/type.rb: -------------------------------------------------------------------------------- 1 | module JSONAPI 2 | module RSpec 3 | module Type 4 | ::RSpec::Matchers.define :have_type do |expected| 5 | match do |actual| 6 | JSONAPI::RSpec.as_indifferent_hash(actual)['type'].to_s == expected.to_s 7 | end 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/jsonapi/id_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe JSONAPI::RSpec, '#have_id' do 4 | it 'succeeds when id matches' do 5 | expect('id' => 'foo').to have_id('foo') 6 | end 7 | 8 | it 'fails when id mismatches' do 9 | expect('id' => 'foo').not_to have_id('bar') 10 | end 11 | 12 | it 'fails when id is absent' do 13 | expect({}).not_to have_id('foo') 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | require: 2 | - rubocop-performance 3 | 4 | AllCops: 5 | NewCops: enable 6 | 7 | Style/FrozenStringLiteralComment: 8 | Enabled: false 9 | 10 | Style/Documentation: 11 | Enabled: false 12 | 13 | Style/IfUnlessModifier: 14 | Enabled: false 15 | 16 | Metrics/BlockLength: 17 | Max: 30 18 | Exclude: 19 | - 'spec/*/*_spec.rb' 20 | 21 | Gemspec/RequiredRubyVersion: 22 | Enabled: false 23 | -------------------------------------------------------------------------------- /lib/jsonapi/rspec/jsonapi_object.rb: -------------------------------------------------------------------------------- 1 | module JSONAPI 2 | module RSpec 3 | module JsonapiObject 4 | ::RSpec::Matchers.define :have_jsonapi_object do |val| 5 | match do |actual| 6 | actual = JSONAPI::RSpec.as_indifferent_hash(actual) 7 | val = JSONAPI::RSpec.as_indifferent_hash(val) 8 | 9 | actual.key?('jsonapi') && (!val || actual['jsonapi'] == val) 10 | end 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | ruby_rails_test_matrix: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | matrix: 11 | ruby: [2.4, 2.7, '3.0', 3.1, 3.2, ruby-head] 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | 16 | - uses: ruby/setup-ruby@v1 17 | with: 18 | ruby-version: ${{ matrix.ruby }} 19 | bundler-cache: true # 'bundle install' and cache 20 | - name: Runs code QA and tests 21 | run: bundle exec rake 22 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## What is the current behavior? 2 | 3 | 5 | 6 | ## What is the new behavior? 7 | 8 | 9 | 10 | ## Checklist 11 | 12 | Please make sure the following requirements are complete: 13 | 14 | - [ ] Tests for the changes have been added (for bug fixes / features) 15 | - [ ] Docs have been reviewed and added / updated if needed (for bug fixes / 16 | features) 17 | - [ ] All automated checks pass (CI/CD) 18 | -------------------------------------------------------------------------------- /lib/jsonapi/rspec/errors.rb: -------------------------------------------------------------------------------- 1 | module JSONAPI 2 | module RSpec 3 | module Meta 4 | ::RSpec::Matchers.define :have_error do |error| 5 | match do |actual| 6 | actual = JSONAPI::RSpec.as_indifferent_hash(actual) 7 | return false unless actual.key?('errors') 8 | return false unless actual['errors'].is_a?(Array) 9 | 10 | return true if actual['errors'].any? && error.nil? 11 | 12 | error = JSONAPI::RSpec.as_indifferent_hash(error) 13 | 14 | actual['errors'].any? { |actual_error| error <= actual_error } 15 | end 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/jsonapi/type_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe JSONAPI::RSpec, '#have_type' do 4 | it 'succeeds when type matches' do 5 | expect('type' => 'foo').to have_type('foo') 6 | end 7 | 8 | it 'succeeds when expectation is symbol' do 9 | expect('type' => 'foo').to have_type(:foo) 10 | end 11 | 12 | it 'succeeds when type is a symbol' do 13 | expect('type' => :foo).to have_type('foo') 14 | end 15 | 16 | it 'fails when type mismatches' do 17 | expect('type' => 'foo').not_to have_type('bar') 18 | end 19 | 20 | it 'fails when type is absent' do 21 | expect({}).not_to have_type('foo') 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/jsonapi/rspec/meta.rb: -------------------------------------------------------------------------------- 1 | module JSONAPI 2 | module RSpec 3 | module Meta 4 | ::RSpec::Matchers.define :have_meta do |val| 5 | match do |actual| 6 | actual = JSONAPI::RSpec.as_indifferent_hash(actual) 7 | return false unless actual.key?('meta') 8 | return true unless val 9 | 10 | val = JSONAPI::RSpec.as_indifferent_hash(val) 11 | return false unless val <= actual['meta'] 12 | 13 | !@exactly || (@exactly && val.size == actual['meta'].size) 14 | end 15 | 16 | chain :exactly do 17 | @exactly = true 18 | end 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'simplecov' 2 | require 'jsonapi/rspec' 3 | 4 | SimpleCov.start do 5 | add_filter '/spec/' 6 | end 7 | SimpleCov.minimum_coverage 90 8 | 9 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration 10 | RSpec.configure do |config| 11 | config.expect_with :rspec do |expectations| 12 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 13 | end 14 | 15 | config.mock_with :rspec do |mocks| 16 | mocks.verify_partial_doubles = true 17 | end 18 | 19 | config.order = :random 20 | config.shared_context_metadata_behavior = :apply_to_host_groups 21 | 22 | Kernel.srand config.seed 23 | end 24 | -------------------------------------------------------------------------------- /spec/jsonapi/links_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe JSONAPI::RSpec do 4 | let(:doc) do 5 | { 6 | 'links' => { 7 | 'self' => 'self_link', 8 | 'related' => 'related_link' 9 | } 10 | } 11 | end 12 | 13 | context '#have_link' do 14 | it { expect(doc).to have_link(:self) } 15 | it { expect(doc).to have_link(:self).with_value('self_link') } 16 | it { expect(doc).not_to have_link(:self).with_value('any_link') } 17 | it { expect(doc).not_to have_link(:any) } 18 | end 19 | 20 | context '#have_links' do 21 | it { expect(doc).to have_links(:self, :related) } 22 | it { expect(doc).not_to have_links(:self, :other) } 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/jsonapi/rspec/links.rb: -------------------------------------------------------------------------------- 1 | module JSONAPI 2 | module RSpec 3 | module Links 4 | ::RSpec::Matchers.define :have_link do |link| 5 | match do |actual| 6 | actual = JSONAPI::RSpec.as_indifferent_hash(actual) 7 | actual.key?('links') && actual['links'].key?(link.to_s) && 8 | (!@val_set || actual['links'][link.to_s] == @val) 9 | end 10 | 11 | chain :with_value do |val| 12 | @val_set = true 13 | @val = val 14 | end 15 | end 16 | 17 | ::RSpec::Matchers.define :have_links do |*links| 18 | match do |actual| 19 | actual = JSONAPI::RSpec.as_indifferent_hash(actual) 20 | return false unless actual.key?('links') 21 | 22 | links.all? { |link| actual['links'].key?(link.to_s) } 23 | end 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/jsonapi/rspec_spec.rb: -------------------------------------------------------------------------------- 1 | require 'securerandom' 2 | require 'spec_helper' 3 | 4 | RSpec.describe JSONAPI::RSpec, '#as_indifferent_hash' do 5 | let(:doc) do 6 | { 7 | id: SecureRandom.uuid, 8 | list: [ 9 | { one: 1, 'one + one' => :two } 10 | ], 11 | hash: { key: :value, 'another key' => 'another value' } 12 | } 13 | end 14 | 15 | it do 16 | expect(JSONAPI::RSpec.as_indifferent_hash(doc)).not_to eq( 17 | JSON.parse(JSON.generate(doc)) 18 | ) 19 | end 20 | 21 | context 'with jsonapi indifferent hash enabled' do 22 | before(:all) { RSpec.configuration.jsonapi_indifferent_hash = true } 23 | after(:all) { RSpec.configuration.jsonapi_indifferent_hash = false } 24 | 25 | it do 26 | expect(JSONAPI::RSpec.as_indifferent_hash(doc)).to eq( 27 | JSON.parse(JSON.generate(doc)) 28 | ) 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /jsonapi-rspec.gemspec: -------------------------------------------------------------------------------- 1 | version = File.read(File.expand_path('VERSION', __dir__)).strip 2 | 3 | Gem::Specification.new do |spec| 4 | spec.name = 'jsonapi-rspec' 5 | spec.version = version 6 | spec.author = ['Lucas Hosseini'] 7 | spec.email = ['lucas.hosseini@gmail.com'] 8 | spec.summary = 'RSpec matchers for JSON API.' 9 | spec.description = 'Helpers for validating JSON API payloads' 10 | spec.homepage = 'https://github.com/jsonapi-rb/jsonapi-rspec' 11 | spec.license = 'MIT' 12 | 13 | spec.files = Dir['README.md', 'lib/**/*'] 14 | spec.require_path = 'lib' 15 | 16 | spec.add_dependency 'rspec-core' 17 | spec.add_dependency 'rspec-expectations' 18 | 19 | spec.add_development_dependency 'rake' 20 | spec.add_development_dependency 'rspec' 21 | spec.add_development_dependency 'rubocop-performance' 22 | spec.add_development_dependency 'simplecov' 23 | 24 | spec.metadata = { 25 | 'rubygems_mfa_required' => 'true' 26 | } 27 | end 28 | -------------------------------------------------------------------------------- /lib/jsonapi/rspec.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | require 'rspec/matchers' 3 | require 'rspec/core' 4 | require 'jsonapi/rspec/id' 5 | require 'jsonapi/rspec/type' 6 | require 'jsonapi/rspec/attributes' 7 | require 'jsonapi/rspec/relationships' 8 | require 'jsonapi/rspec/links' 9 | require 'jsonapi/rspec/meta' 10 | require 'jsonapi/rspec/jsonapi_object' 11 | require 'jsonapi/rspec/errors' 12 | 13 | RSpec.configure do |c| 14 | c.add_setting :jsonapi_indifferent_hash, default: false 15 | end 16 | 17 | module JSONAPI 18 | module RSpec 19 | include Id 20 | include Type 21 | include Attributes 22 | include Relationships 23 | include Links 24 | include Meta 25 | include JsonapiObject 26 | 27 | def self.as_indifferent_hash(doc) 28 | return doc unless ::RSpec.configuration.jsonapi_indifferent_hash 29 | 30 | if doc.respond_to?(:with_indifferent_access) 31 | return doc.with_indifferent_access 32 | end 33 | 34 | JSON.parse(JSON.generate(doc)) 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/jsonapi/errors_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe JSONAPI::RSpec, '#have_error' do 4 | let(:doc) do 5 | { 6 | 'errors' => [ 7 | { 8 | 'status' => '422', 9 | 'source' => { 'pointer' => '/data/attributes/firstName' }, 10 | 'title' => 'Invalid Attribute', 11 | 'detail' => 'First name must contain at least three characters.' 12 | } 13 | ] 14 | } 15 | end 16 | 17 | context 'when providing no value' do 18 | it 'succeeds when errors are present' do 19 | expect(doc).to have_error 20 | end 21 | 22 | it 'fails when errors are missing' do 23 | expect({}).not_to have_error 24 | expect({ 'errors' => [] }).not_to have_error 25 | end 26 | end 27 | 28 | context 'when providing a value' do 29 | it do 30 | expect(doc).to have_error( 31 | 'status' => '422', 32 | 'source' => { 'pointer' => '/data/attributes/firstName' } 33 | ) 34 | end 35 | 36 | it 'fails when meta is absent' do 37 | expect(doc).not_to have_error({ 'status' => '500' }) 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 jsonapi-rb 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /spec/jsonapi/jsonapi_object_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe JSONAPI::RSpec, '#have_jsonapi_object' do 4 | context 'when providing no value' do 5 | it 'succeeds when jsonapi object is present' do 6 | expect('jsonapi' => { 'version' => '1.0' }).to have_jsonapi_object 7 | end 8 | 9 | it 'fails when jsonapi object is absent' do 10 | expect({}).not_to have_jsonapi_object 11 | end 12 | end 13 | 14 | context 'when providing a value' do 15 | context 'with jsonapi indifferent hash enabled' do 16 | before(:all) { RSpec.configuration.jsonapi_indifferent_hash = true } 17 | after(:all) { RSpec.configuration.jsonapi_indifferent_hash = false } 18 | 19 | it do 20 | expect('jsonapi' => { 'version' => '1.0' }) 21 | .to have_jsonapi_object(version: '1.0') 22 | end 23 | end 24 | 25 | it 'succeeds when jsonapi object matches' do 26 | expect('jsonapi' => { 'version' => '1.0' }) 27 | .to have_jsonapi_object('version' => '1.0') 28 | end 29 | 30 | it 'fails when jsonapi object mismatches' do 31 | expect('jsonapi' => { 'version' => '2.0' }) 32 | .not_to have_jsonapi_object('version' => '1.0') 33 | end 34 | 35 | it 'fails when jsonapi object is absent' do 36 | expect({}).not_to have_jsonapi_object('version' => '1.0') 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/jsonapi/rspec/relationships.rb: -------------------------------------------------------------------------------- 1 | module JSONAPI 2 | module RSpec 3 | module Relationships 4 | ::RSpec::Matchers.define :have_relationship do |rel| 5 | match do |actual| 6 | actual = JSONAPI::RSpec.as_indifferent_hash(actual) 7 | return false unless (actual['relationships'] || {}).key?(rel.to_s) 8 | 9 | !@data_set || actual['relationships'][rel.to_s]['data'] == @data_val 10 | end 11 | 12 | chain :with_data do |val| 13 | @data_set = true 14 | @data_val = JSONAPI::RSpec.as_indifferent_hash(val) 15 | end 16 | 17 | failure_message do |actual| 18 | if (actual['relationships'] || {}).key?(rel.to_s) 19 | "expected #{actual['relationships'][rel.to_s]} " \ 20 | "to have data #{@data_val}" 21 | else 22 | "expected #{actual} to have relationship #{rel}" 23 | end 24 | end 25 | end 26 | 27 | ::RSpec::Matchers.define :have_relationships do |*rels| 28 | match do |actual| 29 | actual = JSONAPI::RSpec.as_indifferent_hash(actual) 30 | return false unless actual.key?('relationships') 31 | 32 | counted = (rels.size == actual['relationships'].keys.size) if @exactly 33 | 34 | rels.map(&:to_s).all? { |rel| actual['relationships'].key?(rel) } \ 35 | && (counted == @exactly) 36 | end 37 | 38 | chain :exactly do 39 | @exactly = true 40 | end 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /spec/jsonapi/meta_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe JSONAPI::RSpec, '#have_meta' do 4 | let(:doc) do 5 | { 6 | 'meta' => { 7 | 'one' => 'I', 8 | 'two' => 'II', 9 | 'three' => 'III' 10 | } 11 | } 12 | end 13 | context 'when providing no value' do 14 | it 'succeeds when meta is present' do 15 | expect(doc).to have_meta 16 | end 17 | 18 | it 'fails when meta is absent' do 19 | expect({}).not_to have_meta 20 | end 21 | end 22 | 23 | context 'when providing a value' do 24 | context 'with jsonapi indifferent hash enabled' do 25 | before(:all) { RSpec.configuration.jsonapi_indifferent_hash = true } 26 | after(:all) { RSpec.configuration.jsonapi_indifferent_hash = false } 27 | 28 | it do 29 | expect(doc).to have_meta(one: 'I') 30 | end 31 | end 32 | 33 | it 'succeeds when meta includes the value' do 34 | expect(doc).to have_meta('one' => 'I') 35 | end 36 | 37 | it 'fails when meta does not include the value' do 38 | expect(doc).not_to have_meta('one' => 'II') 39 | end 40 | 41 | it 'succeeds when meta exactly matches the value' do 42 | expect(doc).to have_meta({ 'one' => 'I', 'two' => 'II', 'three' => 'III' }).exactly 43 | end 44 | 45 | it 'succeeds when meta does not exactly match the value' do 46 | expect(doc).not_to have_meta({ 'one' => 'foo', 'two' => 'II', 'three' => 'III' }).exactly 47 | end 48 | 49 | it 'fails when meta is absent' do 50 | expect({}).not_to have_meta(foo: 'bar') 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /spec/jsonapi/relationships_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe JSONAPI::RSpec, '#have_relationship(s)' do 4 | let(:doc) do 5 | { 6 | 'relationships' => { 7 | 'user' => { 8 | 'data' => { 'id' => '1', 'type' => 'user' } 9 | }, 10 | 'comments' => { 11 | 'data' => [ 12 | { 'id' => '1', 'type' => 'comment' }, 13 | { 'id' => '2', 'type' => 'comment' } 14 | ] 15 | } 16 | } 17 | } 18 | end 19 | 20 | it { expect(doc).not_to have_relationships('user', 'comments', 'authors') } 21 | it { expect(doc).to have_relationships('user', 'comments') } 22 | 23 | it { expect(doc).not_to have_relationship('authors') } 24 | it { expect(doc).to have_relationship('user') } 25 | 26 | it { expect(doc).to have_relationships('user', 'comments').exactly } 27 | it { expect(doc).not_to have_relationships('comments').exactly } 28 | 29 | it do 30 | expect(doc).to have_relationship('user').with_data( 31 | { 'id' => '1', 'type' => 'user' } 32 | ) 33 | end 34 | 35 | it do 36 | expect(doc).to have_relationship('comments').with_data( 37 | [ 38 | { 'id' => '1', 'type' => 'comment' }, 39 | { 'id' => '2', 'type' => 'comment' } 40 | ] 41 | ) 42 | end 43 | 44 | context 'with jsonapi indifferent hash enabled' do 45 | before(:all) { RSpec.configuration.jsonapi_indifferent_hash = true } 46 | after(:all) { RSpec.configuration.jsonapi_indifferent_hash = false } 47 | 48 | it { expect(doc).to have_relationships(:user, :comments) } 49 | 50 | it do 51 | expect(doc).to have_relationship('user').with_data(id: '1', type: :user) 52 | end 53 | 54 | it do 55 | expect(doc).to have_relationship('comments').with_data( 56 | [ 57 | { id: '1', type: 'comment' }, 58 | { id: '2', type: 'comment' } 59 | ] 60 | ) 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /lib/jsonapi/rspec/attributes.rb: -------------------------------------------------------------------------------- 1 | module JSONAPI 2 | module RSpec 3 | module Attributes 4 | ::RSpec::Matchers.define :have_attribute do |attr_name| 5 | match do |doc| 6 | doc = JSONAPI::RSpec.as_indifferent_hash(doc) 7 | attributes_node = doc['attributes'] 8 | 9 | return false unless attributes_node 10 | 11 | @existing_attributes = attributes_node.keys 12 | @has_attribute = attributes_node.key?(attr_name.to_s) 13 | @actual = attributes_node[attr_name.to_s] 14 | 15 | return @actual == @expected if @has_attribute && @should_match_value 16 | 17 | @has_attribute 18 | end 19 | 20 | chain :with_value do |expected_value| 21 | @should_match_value = true 22 | @expected = expected_value 23 | end 24 | 25 | description do 26 | result = "have attribute #{attr_name.inspect}" 27 | result << " with value #{@expected.inspect}" if @should_match_value 28 | result 29 | end 30 | 31 | failure_message do |_doc| 32 | if @has_attribute 33 | "expected `#{attr_name}` attribute " \ 34 | "to have value `#{@expected}` but was `#{@actual}`" 35 | else 36 | "expected attributes to include `#{attr_name}`. " \ 37 | "Actual attributes were #{@existing_attributes}" 38 | end 39 | end 40 | 41 | diffable 42 | end 43 | 44 | ::RSpec::Matchers.define :have_jsonapi_attributes do |*attrs| 45 | match do |actual| 46 | actual = JSONAPI::RSpec.as_indifferent_hash(actual) 47 | return false unless actual.key?('attributes') 48 | 49 | counted = (attrs.size == actual['attributes'].size) if @exactly 50 | 51 | (attrs.map(&:to_s) - actual['attributes'].keys).empty? && 52 | (counted == @exactly) 53 | end 54 | 55 | chain :exactly do 56 | @exactly = true 57 | end 58 | end 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /spec/jsonapi/attributes_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe JSONAPI::RSpec do 4 | let(:doc) do 5 | { 6 | 'attributes' => { 7 | 'one' => 1, 8 | 'two' => 2, 9 | 'four' => 3, 10 | 'six' => { foo: 'bar' } 11 | } 12 | } 13 | end 14 | 15 | describe '#have_attribute' do 16 | it { expect(doc).to have_attribute(:one) } 17 | it { expect(doc).not_to have_attribute(:five) } 18 | it { expect(doc).to have_attribute(:one).with_value(1) } 19 | it { expect(doc).not_to have_attribute(:one).with_value(2) } 20 | it { expect(doc).to have_attribute(:six).with_value(foo: 'bar') } 21 | 22 | it 'rejects with an appropriate failure message' do 23 | expect { expect(doc).to have_attribute(:three) } 24 | .to raise_error( 25 | RSpec::Expectations::ExpectationNotMetError, 26 | 'expected attributes to include `three`. ' \ 27 | 'Actual attributes were ["one", "two", "four", "six"]' 28 | ) 29 | end 30 | 31 | it 'fails with a failure message for chained with_value' do 32 | expect { expect(doc).to have_attribute(:one).with_value(2) } 33 | .to raise_error( 34 | RSpec::Expectations::ExpectationNotMetError, 35 | /expected `one` attribute to have value `2` but was `1`/m 36 | ) 37 | end 38 | 39 | it 'fails with a failure message and diff for chained with_value' do 40 | expect { expect(doc).to have_attribute(:six).with_value(bar: 'baz') } 41 | .to raise_error( 42 | RSpec::Expectations::ExpectationNotMetError, 43 | /expected `six` .* `{:bar=>"baz"}` but was `{:foo=>"bar"}`.*Diff:/m 44 | ) 45 | end 46 | end 47 | 48 | describe '#have_jsonapi_attributes' do 49 | it { expect(doc).to have_jsonapi_attributes(:one, :two) } 50 | it { expect(doc).not_to have_jsonapi_attributes(:two, :five) } 51 | it { expect(doc).to have_jsonapi_attributes(:one, :two, :four, :six).exactly } 52 | it { expect(doc).not_to have_jsonapi_attributes(:one).exactly } 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jsonapi-rspec 2 | 3 | RSpec matchers for [JSON API](http://jsonapi.org). 4 | 5 | ## Installation 6 | 7 | Add the following to your application's Gemfile: 8 | ```ruby 9 | gem 'jsonapi-rspec' 10 | ``` 11 | And then execute: 12 | ``` 13 | $ bundle 14 | ``` 15 | 16 | Add to your `spec/spec_helpers.rb`: 17 | 18 | ```ruby 19 | # spec/spec_helpers.rb 20 | require 'jsonapi/rspec' 21 | 22 | RSpec.configure do |config| 23 | config.include JSONAPI::RSpec 24 | 25 | # Support for documents with mixed string/symbol keys. Disabled by default. 26 | config.jsonapi_indifferent_hash = true 27 | end 28 | ``` 29 | 30 | ## Usage and documentation 31 | 32 | Available matchers: 33 | 34 | * `expect(document['data']).to have_id('12')` 35 | * `expect(document['data']).to have_type('users')` 36 | * `expect(document['data']).to have_jsonapi_attributes(:name, :email)` 37 | * `expect(document['data']).to have_jsonapi_attributes(:name, :email, :country).exactly` 38 | * `expect(document['data']).to have_attribute(:name).with_value('Lucas')` 39 | * `expect(document['data']).to have_relationships(:posts, :comments)` 40 | * `expect(document['data']).to have_relationships(:posts, :comments, :likes).exactly` 41 | * `expect(document['data']).to have_relationship(:posts).with_data([{ 'id' => '1', 'type' => 'posts' }])` 42 | * `expect(document['data']['relationships']['posts']).to have_links(:self, :related)` 43 | * `expect(document['data']).to have_link(:self).with_value('http://api.example.com/users/12')` 44 | * `expect(document).to have_meta` 45 | * `expect(document).to have_meta('foo' => 'bar')` 46 | * `expect(document).to have_meta('foo' => 'bar', 'fum' => 'baz').exactly` 47 | * `expect(document).to have_jsonapi_object` 48 | * `expect(document).to have_jsonapi_object('version' => '1.0')` 49 | * `expect(document).to have_error('status' => '422')` 50 | 51 | ### On matcher arguments... 52 | 53 | **Note**: JSON:API spec requires JSON documents, thus attribute, relationship 54 | and link matcher arguments will always be converted into strings for 55 | consistency!!! 56 | 57 | Basically, the tests bellow are absolutely equal: 58 | 59 | ```ruby 60 | expect(document['data']).to have_id(12) 61 | expect(document['data']).to have_id('12') 62 | 63 | expect(document['data']).to have_type(:users) 64 | expect(document['data']).to have_type('users') 65 | 66 | expect(document['data']).to have_jsonapi_attributes(:name, :email) 67 | expect(document['data']).to have_jsonapi_attributes('name', 'email') 68 | ``` 69 | 70 | The JSON:API spec also requires the `id` and `type` to be strings, so any other 71 | argument passed will also be converted into a string. 72 | 73 | If the document you are trying to test has mixed string/symbol keys, just 74 | configure matchers to be indifferent in that regard, using the 75 | `jsonapi_indifferent_hash = true` configuration option. 76 | 77 | ## Advanced examples 78 | 79 | Checking for an included resource: 80 | 81 | ```ruby 82 | expect(response_body['included']) 83 | .to include(have_type('posts').and have_id('1')) 84 | ``` 85 | ## Contributing 86 | 87 | Bug reports and pull requests are welcome on GitHub at 88 | https://github.com/jsonapi-rb/jsonapi-rspec 89 | 90 | This project is intended to be a safe, welcoming space for collaboration, and 91 | contributors are expected to adhere to the 92 | [Contributor Covenant](http://contributor-covenant.org) code of conduct. 93 | 94 | ## License 95 | 96 | jsonapi-rspec is released under the [MIT License](http://www.opensource.org/licenses/MIT). 97 | --------------------------------------------------------------------------------