diff --git a/.rspec b/.rspec new file mode 100644 index 0000000..c7a3d35 --- /dev/null +++ b/.rspec @@ -0,0 +1 @@ +--require ./spec/spec_helper.rb diff --git a/.rubocop.yml b/.rubocop.yml index ef28b33..1972ae6 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,3 +1,7 @@ +Metrics/BlockLength: + Exclude: + - 'spec/**/*_spec.rb' + require: - rubocop-performance @@ -6,3 +10,43 @@ Style/FrozenStringLiteralComment: Style/Documentation: Enabled: false + +Layout/EmptyLinesAroundAttributeAccessor: + Enabled: true + +Layout/SpaceAroundMethodCallOperator: + Enabled: true + +Lint/DeprecatedOpenSSLConstant: + Enabled: true + +Lint/MixedRegexpCaptureTypes: + Enabled: true + +Lint/RaiseException: + Enabled: true + +Lint/StructNewOverride: + Enabled: true + +Style/ExponentialNotation: + Enabled: true + +Style/HashEachMethods: + Enabled: true + +Style/HashTransformKeys: + Enabled: true + +Style/HashTransformValues: + Enabled: true + +Style/RedundantRegexpCharacterClass: + Enabled: true + +Style/RedundantRegexpEscape: + Enabled: true + +Style/SlicingWithRange: + Enabled: true + diff --git a/README.md b/README.md index 6c6983f..4f2f7f8 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,17 @@ Available matchers: * `expect(document).to have_jsonapi_object` * `expect(document).to have_jsonapi_object('version' => '1.0')` +### Indifferent Matching +```ruby +# spec/spec_helpers.rb + +RSpec.configure do |config| + # ... + config.allow_symbolized_jsonapi = true +end +``` +The configuration above allows the RSpec matchers to work with a symbolized jsonapi source document. + ## Advanced examples Checking for an included resource: diff --git a/lib/jsonapi/rspec.rb b/lib/jsonapi/rspec.rb index af70a34..7414de2 100644 --- a/lib/jsonapi/rspec.rb +++ b/lib/jsonapi/rspec.rb @@ -7,6 +7,10 @@ require 'jsonapi/rspec/meta' require 'jsonapi/rspec/jsonapi_object' +RSpec.configure do |c| + c.add_setting :allow_symbolized_jsonapi, default: false +end + module JSONAPI module RSpec include Id @@ -16,5 +20,12 @@ module RSpec include Links include Meta include JsonapiObject + + def self.as_indifferent_hash(doc) + return doc unless ::RSpec.configuration.allow_symbolized_jsonapi + return doc.with_indifferent_access if doc.respond_to?(:with_indifferent_access) + + JSON.parse(JSON.generate(doc)) + end end end diff --git a/lib/jsonapi/rspec/attributes.rb b/lib/jsonapi/rspec/attributes.rb index 09b064f..abddb1e 100644 --- a/lib/jsonapi/rspec/attributes.rb +++ b/lib/jsonapi/rspec/attributes.rb @@ -3,6 +3,7 @@ module RSpec module Attributes ::RSpec::Matchers.define :have_attribute do |attr| match do |actual| + actual = JSONAPI::RSpec.as_indifferent_hash(actual) (actual['attributes'] || {}).key?(attr.to_s) && (!@val_set || actual['attributes'][attr.to_s] == @val) end @@ -15,6 +16,7 @@ module Attributes ::RSpec::Matchers.define :have_jsonapi_attributes do |*attrs| match do |actual| + actual = JSONAPI::RSpec.as_indifferent_hash(actual) return false unless actual.key?('attributes') counted = (attrs.size == actual['attributes'].size) if @exactly diff --git a/lib/jsonapi/rspec/id.rb b/lib/jsonapi/rspec/id.rb index 3130610..504df38 100644 --- a/lib/jsonapi/rspec/id.rb +++ b/lib/jsonapi/rspec/id.rb @@ -2,7 +2,9 @@ module JSONAPI module RSpec module Id ::RSpec::Matchers.define :have_id do |expected| - match { |actual| actual['id'] == expected } + match do |actual| + JSONAPI::RSpec.as_indifferent_hash(actual)['id'] == expected + end end end end diff --git a/lib/jsonapi/rspec/jsonapi_object.rb b/lib/jsonapi/rspec/jsonapi_object.rb index 8916bc2..e6c252e 100644 --- a/lib/jsonapi/rspec/jsonapi_object.rb +++ b/lib/jsonapi/rspec/jsonapi_object.rb @@ -3,6 +3,7 @@ module RSpec module JsonapiObject ::RSpec::Matchers.define :have_jsonapi_object do |val| match do |actual| + actual = JSONAPI::RSpec.as_indifferent_hash(actual) actual.key?('jsonapi') && (!val || actual['jsonapi'] == val) end diff --git a/lib/jsonapi/rspec/links.rb b/lib/jsonapi/rspec/links.rb index 3d8265a..62fb940 100644 --- a/lib/jsonapi/rspec/links.rb +++ b/lib/jsonapi/rspec/links.rb @@ -3,6 +3,7 @@ module RSpec module Links ::RSpec::Matchers.define :have_link do |link| match do |actual| + actual = JSONAPI::RSpec.as_indifferent_hash(actual) actual.key?('links') && actual['links'].key?(link.to_s) && (!@val_set || actual['links'][link.to_s] == @val) end @@ -15,6 +16,7 @@ module Links ::RSpec::Matchers.define :have_links do |*links| match do |actual| + actual = JSONAPI::RSpec.as_indifferent_hash(actual) return false unless actual.key?('links') links.all? { |link| actual['links'].key?(link.to_s) } diff --git a/lib/jsonapi/rspec/meta.rb b/lib/jsonapi/rspec/meta.rb index ac33e8c..c8983fa 100644 --- a/lib/jsonapi/rspec/meta.rb +++ b/lib/jsonapi/rspec/meta.rb @@ -3,6 +3,7 @@ module RSpec module Meta ::RSpec::Matchers.define :have_meta do |val| match do |actual| + actual = JSONAPI::RSpec.as_indifferent_hash(actual) actual.key?('meta') && (!val || actual['meta'] == val) end diff --git a/lib/jsonapi/rspec/relationships.rb b/lib/jsonapi/rspec/relationships.rb index fbbe0da..50441e8 100644 --- a/lib/jsonapi/rspec/relationships.rb +++ b/lib/jsonapi/rspec/relationships.rb @@ -3,6 +3,7 @@ module RSpec module Relationships ::RSpec::Matchers.define :have_relationship do |rel| match do |actual| + actual = JSONAPI::RSpec.as_indifferent_hash(actual) return false unless (actual['relationships'] || {}).key?(rel.to_s) !@data_set || actual['relationships'][rel.to_s]['data'] == @data_val @@ -25,6 +26,7 @@ module Relationships ::RSpec::Matchers.define :have_relationships do |*rels| match do |actual| + actual = JSONAPI::RSpec.as_indifferent_hash(actual) return false unless actual.key?('relationships') rels.all? { |rel| actual['relationships'].key?(rel) } diff --git a/lib/jsonapi/rspec/type.rb b/lib/jsonapi/rspec/type.rb index 71d6a18..a2272ac 100644 --- a/lib/jsonapi/rspec/type.rb +++ b/lib/jsonapi/rspec/type.rb @@ -2,7 +2,9 @@ module JSONAPI module RSpec module Type ::RSpec::Matchers.define :have_type do |expected| - match { |actual| actual['type'] == expected } + match do |actual| + JSONAPI::RSpec.as_indifferent_hash(actual)['type'] == expected + end end end end diff --git a/spec/jsonapi/attributes_spec.rb b/spec/jsonapi/attributes_spec.rb index fec83ac..63add50 100644 --- a/spec/jsonapi/attributes_spec.rb +++ b/spec/jsonapi/attributes_spec.rb @@ -1,7 +1,5 @@ -require 'spec_helper' - RSpec.describe JSONAPI::RSpec do - let(:doc) do + json_doc = { 'attributes' => { 'one' => 1, @@ -9,17 +7,28 @@ 'four' => 3 } } - end describe '#have_attribute' do - it { expect(doc).to have_attribute(:one) } - it { expect(doc).not_to have_attribute(:five) } + context 'when attributes is present' do + it { expect(json_doc).to have_attribute(:one) } + it { expect(json_doc).not_to have_attribute(:five) } + end + + context 'when attributes is not present' do + it { expect({}).not_to have_attribute(:one) } + end end describe '#have_jsonapi_attributes' do - it { expect(doc).to have_jsonapi_attributes(:one, :two) } - it { expect(doc).not_to have_jsonapi_attributes(:two, :five) } - it { expect(doc).to have_jsonapi_attributes(:one, :two, :four).exactly } - it { expect(doc).not_to have_jsonapi_attributes(:one).exactly } + context 'when attributes is present' do + it { expect(json_doc).to have_jsonapi_attributes(:one, 'two') } + it { expect(json_doc).not_to have_jsonapi_attributes(:two, 'five') } + it { expect(json_doc).to have_jsonapi_attributes(:one, :two, 'four').exactly } + it { expect(json_doc).not_to have_jsonapi_attributes(:one).exactly } + end + + context 'when attributes is not present' do + it { expect({}).not_to have_jsonapi_attributes(:one, 'two') } + end end end diff --git a/spec/jsonapi/id_spec.rb b/spec/jsonapi/id_spec.rb index 96a42d0..bb1d587 100644 --- a/spec/jsonapi/id_spec.rb +++ b/spec/jsonapi/id_spec.rb @@ -1,15 +1,16 @@ -require 'spec_helper' - RSpec.describe JSONAPI::RSpec, '#have_id' do - it 'succeeds when id matches' do - expect('id' => 'foo').to have_id('foo') - end - - it 'fails when id mismatches' do - expect('id' => 'foo').not_to have_id('bar') - end + json_doc = + { + 'id' => 'foo' + } - it 'fails when id is absent' do - expect({}).not_to have_id('foo') + describe '#have_id' do + context 'when id is present' do + it { expect(json_doc).to have_id('foo') } + it { expect(json_doc).not_to have_id('bar') } + end + context 'when id is not present' do + it { expect({}).not_to have_id('foo') } + end end end diff --git a/spec/jsonapi/jsonapi_object_spec.rb b/spec/jsonapi/jsonapi_object_spec.rb index 9f4539d..1121fca 100644 --- a/spec/jsonapi/jsonapi_object_spec.rb +++ b/spec/jsonapi/jsonapi_object_spec.rb @@ -1,29 +1,21 @@ -require 'spec_helper' +RSpec.describe JSONAPI::RSpec do + json_doc = + { + 'jsonapi' => { + 'version' => '1.0' + } + } -RSpec.describe JSONAPI::RSpec, '#have_jsonapi_object' do - context 'when providing no value' do - it 'succeeds when jsonapi object is present' do - expect('jsonapi' => { 'version' => '1.0' }).to have_jsonapi_object + describe '#have_jsonapi_object' do + context 'when jsonapi is present' do + it { expect(json_doc).to have_jsonapi_object } + it { expect(json_doc).to have_jsonapi_object('version' => '1.0') } + it { expect(json_doc).not_to have_jsonapi_object('version' => '2.0') } end - it 'fails when jsonapi object is absent' do - expect({}).not_to have_jsonapi_object - end - end - - context 'when providing a value' do - it 'succeeds when jsonapi object matches' do - expect('jsonapi' => { 'version' => '1.0' }) - .to have_jsonapi_object('version' => '1.0') - end - - it 'fails when jsonapi object mismatches' do - expect('jsonapi' => { 'version' => '2.0' }) - .not_to have_jsonapi_object('version' => '1.0') - end - - it 'fails when jsonapi object is absent' do - expect({}).not_to have_jsonapi_object('version' => '1.0') + context 'when jsonapi is not present' do + it { expect({}).not_to have_jsonapi_object } + it { expect({}).not_to have_jsonapi_object('version' => '1.0') } end end end diff --git a/spec/jsonapi/links_spec.rb b/spec/jsonapi/links_spec.rb index cbca90e..293fefd 100644 --- a/spec/jsonapi/links_spec.rb +++ b/spec/jsonapi/links_spec.rb @@ -1,24 +1,33 @@ -require 'spec_helper' - RSpec.describe JSONAPI::RSpec do - let(:doc) do + json_doc = { 'links' => { 'self' => 'self_link', 'related' => 'related_link' } } - end - context '#have_link' do - it { expect(doc).to have_link(:self) } - it { expect(doc).to have_link(:self).with_value('self_link') } - it { expect(doc).not_to have_link(:self).with_value('any_link') } - it { expect(doc).not_to have_link(:any) } + describe '#have_link' do + context 'when links is present' do + it { expect(json_doc).to have_link(:self) } + it { expect(json_doc).to have_link(:self).with_value('self_link') } + it { expect(json_doc).not_to have_link(:self).with_value('any_link') } + it { expect(json_doc).not_to have_link(:any) } + end + + context 'when links is not present' do + it { expect({}).not_to have_link(:self) } + end end - context '#have_links' do - it { expect(doc).to have_links(:self, :related) } - it { expect(doc).not_to have_links(:self, :other) } + describe '#have_links' do + context 'when links is present' do + it { expect(json_doc).to have_links(:self, 'related') } + it { expect(json_doc).not_to have_links(:self, 'other') } + end + + context 'when links is not present' do + it { expect({}).not_to have_links(:self, 'related') } + end end end diff --git a/spec/jsonapi/meta_spec.rb b/spec/jsonapi/meta_spec.rb index 9a9b134..15bd01a 100644 --- a/spec/jsonapi/meta_spec.rb +++ b/spec/jsonapi/meta_spec.rb @@ -1,27 +1,21 @@ -require 'spec_helper' +RSpec.describe JSONAPI::RSpec do + json_doc = + { + 'meta' => { + 'foo' => 'bar' + } + } -RSpec.describe JSONAPI::RSpec, '#have_meta' do - context 'when providing no value' do - it 'succeeds when meta is present' do - expect('meta' => {}).to have_meta + describe '#have_meta' do + context 'when meta is present' do + it { expect(json_doc).to have_meta } + it { expect(json_doc).to have_meta('foo' => 'bar') } + it { expect(json_doc).not_to have_meta('foo' => 'baz') } end - it 'fails when meta is absent' do - expect({}).not_to have_meta - end - end - - context 'when providing a value' do - it 'succeeds when meta matches' do - expect('meta' => { foo: 'bar' }).to have_meta(foo: 'bar') - end - - it 'fails when meta mismatches' do - expect('meta' => { foo: 'bar' }).not_to have_meta(bar: 'baz') - end - - it 'fails when meta is absent' do - expect({}).not_to have_meta(foo: 'bar') + context 'when meta is not present' do + it { expect({}).not_to have_meta } + it { expect({}).not_to have_meta('foo' => 'bar') } end end end diff --git a/spec/jsonapi/relationships_spec.rb b/spec/jsonapi/relationships_spec.rb new file mode 100644 index 0000000..c5c7a3b --- /dev/null +++ b/spec/jsonapi/relationships_spec.rb @@ -0,0 +1,50 @@ +RSpec.describe JSONAPI::RSpec do + json_doc = + { + 'relationships' => { + 'posts' => { + 'data' => { + 'id' => '1', + 'type' => 'posts' + } + }, + 'comments' => { + 'data' => [{ + 'id' => '1', + 'type' => 'posts' + }, { + 'id' => '2', + 'type' => 'hides' + }] + } + } + } + + describe '#have_relationship' do + context 'when relationships is present' do + it { expect(json_doc).to have_relationship('posts') } + it { expect(json_doc).not_to have_relationship('mails') } + it { expect(json_doc).to have_relationship('posts').with_data({ 'id' => '1', 'type' => 'posts' }) } + it do + expect(json_doc).to have_relationship('comments').with_data( + [{ 'id' => '1', 'type' => 'posts' }, { 'id' => '2', 'type' => 'hides' }] + ) + end + end + + context 'when relationships is not present' do + it { expect({}).not_to have_relationship('posts') } + end + end + + describe '#have_relationships' do + context 'when relationships is present' do + it { expect(json_doc).to have_relationships('posts', 'comments') } + it { expect(json_doc).not_to have_relationships('posts', 'comments', 'mails') } + end + + context 'when relationships is not present' do + it { expect({}).not_to have_relationships('posts', 'comments') } + end + end +end diff --git a/spec/jsonapi/type_spec.rb b/spec/jsonapi/type_spec.rb index 1b23233..e6639db 100644 --- a/spec/jsonapi/type_spec.rb +++ b/spec/jsonapi/type_spec.rb @@ -1,15 +1,17 @@ -require 'spec_helper' - RSpec.describe JSONAPI::RSpec, '#have_type' do - it 'succeeds when type matches' do - expect('type' => 'foo').to have_type('foo') - end + json_doc = + { + 'type' => 'foo' + } - it 'fails when type mismatches' do - expect('type' => 'foo').not_to have_type('bar') - end + describe '#have_tyoe' do + context 'when type is present' do + it { expect(json_doc).to have_type('foo') } + it { expect(json_doc).not_to have_type('bar') } + end - it 'fails when type is absent' do - expect({}).not_to have_type('foo') + context 'when type is not present' do + it { expect({}).not_to have_type('foo') } + end end end diff --git a/spec/rspec_spec.rb b/spec/rspec_spec.rb new file mode 100644 index 0000000..4164215 --- /dev/null +++ b/spec/rspec_spec.rb @@ -0,0 +1,47 @@ +require 'support/sample_jsonapi' + +RSpec.describe JSONAPI::RSpec do + include SampleJsonapi + + before(:each) { RSpec.configuration.allow_symbolized_jsonapi = enable } + + describe 'with allow_symbolized_jsonapi true' do + let(:enable) { true } + + it 'returns a string keyed hash unchanged' do + expect(JSONAPI::RSpec.as_indifferent_hash(document)).to eql(document) + end + + it 'returns an indifferent hash unchanged' do + expect( + JSONAPI::RSpec.as_indifferent_hash( + indifferent_document + ).respond_to?(:with_indifferent_access) + ).to be_truthy + end + + it 'returns a symbolized hash with stringifyed keys' do + expect(JSONAPI::RSpec.as_indifferent_hash(symbolized_document)).to eql(document) + end + end + + describe 'with allow_symbolized_jsonapi false' do + let(:enable) { false } + + it 'returns a string keyed hash unchanged' do + expect(JSONAPI::RSpec.as_indifferent_hash(document)).to eql(document) + end + + it 'returns an indifferent hash unchanged' do + expect( + JSONAPI::RSpec.as_indifferent_hash( + indifferent_document + ).respond_to?(:with_indifferent_access) + ).to be_truthy + end + + it 'returns a symbolized hash unchanged' do + expect(JSONAPI::RSpec.as_indifferent_hash(symbolized_document)).to eql(symbolized_document) + end + end +end diff --git a/spec/support/sample_jsonapi.rb b/spec/support/sample_jsonapi.rb new file mode 100644 index 0000000..3757b04 --- /dev/null +++ b/spec/support/sample_jsonapi.rb @@ -0,0 +1,144 @@ +require 'active_support/core_ext/hash/indifferent_access' + +module SampleJsonapi # rubocop:disable Metrics/ModuleLength + SAMPLE_JSONAPI = { + 'jsonapi' => { + 'version' => '1.0' + }, + 'links' => { + 'self' => 'http://example.com/articles', + 'next' => 'http://example.com/articles?page[offset]=2', + 'last' => 'http://example.com/articles?page[offset]=10' + }, + 'data' => [ + { + 'type' => 'articles', + 'id' => '1', + 'attributes' => { + 'title' => 'JSON API paints my bikeshed!' + }, + 'relationships' => { + 'author' => { + 'links' => { + 'self' => 'http://example.com/articles/1/relationships/author', + 'related' => 'http://example.com/articles/1/author' + }, + 'data' => { + 'type' => 'people', + 'id' => '9' + } + }, + 'comments' => { + 'links' => { + 'self' => 'http://example.com/articles/1/relationships/comments', + 'related' => 'http://example.com/articles/1/comments' + }, + 'data' => [ + { + 'type' => 'comments', + 'id' => '5' + }, + { + 'type' => 'comments', + 'id' => '12' + } + ] + } + }, + 'links' => { + 'self' => 'http://example.com/articles/1' + } + } + ], + 'included' => [ + { + 'type' => 'people', + 'id' => '9', + 'attributes' => { + 'first-name' => 'Dan', + 'last-name' => 'Gebhardt', + 'twitter' => 'dgeb' + }, + 'links' => { + 'self' => 'http://example.com/people/9' + } + }, + { + 'type' => 'comments', + 'id' => '5', + 'attributes' => { + 'body' => 'First!' + }, + 'relationships' => { + 'author' => { + 'data' => { + 'type' => 'people', + 'id' => '2' + } + } + }, + 'links' => { + 'self' => 'http://example.com/comments/5' + } + }, + { + 'type' => 'comments', + 'id' => '12', + 'attributes' => { + 'body' => 'I like XML better' + }, + 'relationships' => { + 'author' => { + 'data' => { + 'type' => 'people', + 'id' => '9' + } + } + }, + 'links' => { + 'self' => 'http://example.com/comments/12' + } + } + ], + 'meta' => { + 'totalPages' => 13, + 'numberOfViews' => 25 + } + }.freeze + + # document formats for rspec as_indifferent_hash testing + + def document + SAMPLE_JSONAPI + end + + def symbolized_document + symbolize_keys(SAMPLE_JSONAPI) + end + + def indifferent_document + SAMPLE_JSONAPI.with_indifferent_access + end + + def symbolize_keys(doc) + newdoc = doc.transform_keys(&:to_sym) + newdoc.transform_values! do |val| + if val.respond_to?('transform_keys!') + symbolize_keys(val) + elsif val.is_a?(Array) + symbolize_array(val) + else + val + end + end + end + + def symbolize_array(array) + array.map do |element| + next unless element.respond_to?('transform_keys!') || + element.respond_to?('each') + + symbolize_keys(element) + end + end +end