diff --git a/jsonapi-deserializable.gemspec b/jsonapi-deserializable.gemspec index d47611f..4a5780c 100644 --- a/jsonapi-deserializable.gemspec +++ b/jsonapi-deserializable.gemspec @@ -16,4 +16,5 @@ Gem::Specification.new do |spec| spec.add_development_dependency 'rake', '>=0.9' spec.add_development_dependency 'rspec', '~>3.4' + spec.add_development_dependency 'codecov', '~> 0.1' end diff --git a/lib/jsonapi/deserializable/relationship.rb b/lib/jsonapi/deserializable/relationship.rb index eb2192f..deb87fd 100644 --- a/lib/jsonapi/deserializable/relationship.rb +++ b/lib/jsonapi/deserializable/relationship.rb @@ -33,16 +33,26 @@ module JSONAPI def deserialize! @hash = {} if @data.is_a?(Array) - ids = @data.map { |ri| ri['id'] } - types = @data.map { |ri| ri['type'] } - instance_exec(@document, ids, types, &self.class.has_many_block) + deserialize_has_many! else - id = @data && @data['id'] - type = @data && @data['type'] - instance_exec(@document, id, type, &self.class.has_one_block) + deserialize_has_one! end end + def deserialize_has_one! + return unless self.class.has_one_block && @document.key?('data') + id = @data && @data['id'] + type = @data && @data['type'] + instance_exec(@document, id, type, &self.class.has_one_block) + end + + def deserialize_has_many! + return unless self.class.has_many_block + ids = @data.map { |ri| ri['id'] } + types = @data.map { |ri| ri['type'] } + instance_exec(@document, ids, types, &self.class.has_many_block) + end + def field(hash) @hash.merge!(hash) end diff --git a/lib/jsonapi/deserializable/relationship_dsl.rb b/lib/jsonapi/deserializable/relationship_dsl.rb index f49b196..92adf2a 100644 --- a/lib/jsonapi/deserializable/relationship_dsl.rb +++ b/lib/jsonapi/deserializable/relationship_dsl.rb @@ -7,12 +7,12 @@ module JSONAPI module ClassMethods def has_one(&block) - block ||= proc { |rel| field key.to_sym => rel } + block ||= proc { |rel| field relationship: rel } self.has_one_block = block end def has_many(&block) - block ||= proc { |rel| field key.to_sym => rel } + block ||= proc { |rel| field relationship: rel } self.has_many_block = block end end diff --git a/spec/deserializable_relationships_spec.rb b/spec/deserializable_relationships_spec.rb deleted file mode 100644 index 5c1d1eb..0000000 --- a/spec/deserializable_relationships_spec.rb +++ /dev/null @@ -1,44 +0,0 @@ -require 'jsonapi/deserializable' - -describe JSONAPI::Deserializable::Relationship, '#to_h' do - it 'deserializes has_one relationships' do - deserializable_klass = Class.new(JSONAPI::Deserializable::Relationship) do - has_one do |rel| - field sponsor_id: (rel['data'] && rel['data']['id']) - end - end - - payload = { - 'data' => { - 'type' => 'users', - 'id' => '1' - } - } - - actual = deserializable_klass.(payload) - expected = { sponsor_id: '1' } - - expect(actual).to eq(expected) - end - - it 'deserializes has_many relationships' do - deserializable_klass = Class.new(JSONAPI::Deserializable::Relationship) do - has_many do |rel| - field post_ids: rel['data'].map { |ri| ri['id'] } - end - end - - payload = { - 'data' => [ - { 'type' => 'postd', 'id' => '1' }, - { 'type' => 'postd', 'id' => '2' }, - { 'type' => 'postd', 'id' => '3' } - ] - } - - actual = deserializable_klass.(payload) - expected = { post_ids: %w(1 2 3) } - - expect(actual).to eq(expected) - end -end diff --git a/spec/deserializable_resource_spec.rb b/spec/deserializable_resource_spec.rb deleted file mode 100644 index ea11d9c..0000000 --- a/spec/deserializable_resource_spec.rb +++ /dev/null @@ -1,132 +0,0 @@ -require 'jsonapi/deserializable' - -describe JSONAPI::Deserializable::Resource, '#to_h' do - before(:all) do - @payload = { - 'data' => { - 'id' => '1', - 'type' => 'users', - 'attributes' => { - 'name' => 'Name', - 'address' => 'Address' - }, - 'relationships' => { - 'sponsor' => { - 'data' => { 'type' => 'users', 'id' => '1337' } - }, - 'posts' => { - 'data' => [ - { 'type' => 'posts', 'id' => '123' }, - { 'type' => 'posts', 'id' => '234' }, - { 'type' => 'posts', 'id' => '345' } - ] - } - } - } - } - end - - it 'deserializes primary type' do - deserializable_klass = Class.new(JSONAPI::Deserializable::Resource) do - type { |type| field type: type } - end - - actual = deserializable_klass.(@payload) - expected = { type: 'users' } - - expect(actual).to eq(expected) - end - - it 'deserializes primary id when present' do - deserializable_klass = Class.new(JSONAPI::Deserializable::Resource) do - id { |id| field id: id } - end - - actual = deserializable_klass.(@payload) - expected = { id: '1' } - - expect(actual).to eq(expected) - end - - it 'does not deserialize primary id when absent' do - deserializable_klass = Class.new(JSONAPI::Deserializable::Resource) do - id { |id| field id: id } - end - - payload = { - 'data' => { 'type' => 'users' } - } - actual = deserializable_klass.(payload) - expected = {} - - expect(actual).to eq(expected) - end - - it 'handles attributes' do - deserializable_klass = Class.new(JSONAPI::Deserializable::Resource) do - attribute(:name) { |name| field username: name } - attribute(:address) { |address| field address: address } - end - - actual = deserializable_klass.(@payload) - expected = { - username: 'Name', - address: 'Address' - } - - expect(actual).to eq(expected) - end - - it 'handles has_one relationships' do - deserializable_klass = Class.new(JSONAPI::Deserializable::Resource) do - has_one(:sponsor) { |rel| field sponsor_id: rel['data']['id'] } - end - - actual = deserializable_klass.(@payload) - expected = { - sponsor_id: '1337' - } - - expect(actual).to eq(expected) - end - - it 'handles has_many relationships' do - deserializable_klass = Class.new(JSONAPI::Deserializable::Resource) do - has_many(:posts) do |rel| - field post_ids: rel['data'].map { |ri| ri['id'] } - end - end - - actual = deserializable_klass.(@payload) - expected = { - post_ids: %w(123 234 345) - } - - expect(actual).to eq(expected) - end - - it 'works' do - deserializable_klass = Class.new(JSONAPI::Deserializable::Resource) do - id - attribute(:name) { |name| field username: name } - attribute :address - has_one :sponsor do |_, id| - field sponsor_id: id - end - has_many :posts do |_, ids| - field post_ids: ids - end - end - - actual = deserializable_klass.(@payload) - expected = { - id: '1', - username: 'Name', - address: 'Address', - sponsor_id: '1337', - post_ids: %w(123 234 345) - } - - expect(actual).to eq(expected) - end -end diff --git a/spec/relationship/has_many_spec.rb b/spec/relationship/has_many_spec.rb new file mode 100644 index 0000000..d13085d --- /dev/null +++ b/spec/relationship/has_many_spec.rb @@ -0,0 +1,72 @@ +require 'spec_helper' + +describe JSONAPI::Deserializable::Relationship, '.has_many' do + let(:deserializable_foo) do + Class.new(JSONAPI::Deserializable::Relationship) do + has_many do |rel, ids, types| + field foo_ids: ids + field foo_types: types + field foo_rel: rel + end + end + end + + context 'relationship is not empty' do + let(:payload) do + { + 'data' => [ + { 'type' => 'foo', 'id' => 'bar' }, + { 'type' => 'foo', 'id' => 'baz' } + ] + } + end + + it 'creates corresponding fields' do + actual = deserializable_foo.call(payload) + expected = { foo_ids: %w(bar baz), foo_types: %w(foo foo), + foo_rel: payload } + + expect(actual).to eq(expected) + end + + it 'defaults to creating a relationship field' do + klass = Class.new(JSONAPI::Deserializable::Relationship) do + has_many + end + actual = klass.call(payload) + expected = { relationship: payload } + + expect(actual).to eq(expected) + end + end + + context 'relationship is empty' do + it 'creates corresponding fields' do + payload = { 'data' => [] } + actual = deserializable_foo.call(payload) + expected = { foo_ids: [], foo_types: [], foo_rel: payload } + + expect(actual).to eq(expected) + end + end + + context 'data is absent' do + it 'does not create corresponding fields' do + payload = {} + actual = deserializable_foo.call(payload) + expected = {} + + expect(actual).to eq(expected) + end + end + + context 'relationship is not to-many' do + it 'does not create corresponding fields' do + payload = { 'data' => nil } + actual = deserializable_foo.call(payload) + expected = {} + + expect(actual).to eq(expected) + end + end +end diff --git a/spec/relationship/has_one_spec.rb b/spec/relationship/has_one_spec.rb new file mode 100644 index 0000000..fe9f0c6 --- /dev/null +++ b/spec/relationship/has_one_spec.rb @@ -0,0 +1,66 @@ +require 'spec_helper' + +describe JSONAPI::Deserializable::Relationship, '.has_one' do + let(:deserializable_foo) do + Class.new(JSONAPI::Deserializable::Relationship) do + has_one do |rel, id, type| + field foo_id: id + field foo_type: type + field foo_rel: rel + end + end + end + + context 'relationship is not nil' do + let(:payload) do + { 'data' => { 'type' => 'foo', 'id' => 'bar' } } + end + + it 'creates corresponding fields' do + actual = deserializable_foo.call(payload) + expected = { foo_id: 'bar', foo_type: 'foo', foo_rel: payload } + + expect(actual).to eq(expected) + end + + it 'defaults to creating a relationship field' do + klass = Class.new(JSONAPI::Deserializable::Relationship) do + has_one + end + actual = klass.call(payload) + expected = { relationship: payload } + + expect(actual).to eq(expected) + end + end + + context 'relationship is nil' do + it 'creates corresponding fields' do + payload = { 'data' => nil } + actual = deserializable_foo.call(payload) + expected = { foo_id: nil, foo_type: nil, foo_rel: payload } + + expect(actual).to eq(expected) + end + end + + context 'data is absent' do + it 'does not create corresponding fields' do + payload = {} + actual = deserializable_foo.call(payload) + expected = {} + + expect(actual).to eq(expected) + end + end + + context 'relationship is not to-one' do + it 'does not create corresponding fields' do + payload = { 'data' => [] } + actual = deserializable_foo.call(payload) + expected = {} + + expect(actual).to eq(expected) + end + end +end diff --git a/spec/resource/DSL/attribute_spec.rb b/spec/resource/DSL/attribute_spec.rb new file mode 100644 index 0000000..da97f6f --- /dev/null +++ b/spec/resource/DSL/attribute_spec.rb @@ -0,0 +1,57 @@ +require 'spec_helper' + +describe JSONAPI::Deserializable::Resource, '.attribute' do + it 'creates corresponding field if attribute is present' do + payload = { + 'data' => { + 'type' => 'foo', + 'attributes' => { 'foo' => 'bar' } + } + } + klass = Class.new(JSONAPI::Deserializable::Resource) do + attribute(:foo) { |foo| field foo: foo } + end + actual = klass.call(payload) + expected = { foo: 'bar' } + + expect(actual).to eq(expected) + end + + it 'does not create corresponding field if attribute is absent' do + payload = { 'data' => { 'type' => 'foo' }, 'attributes' => {} } + klass = Class.new(JSONAPI::Deserializable::Resource) do + attribute(:foo) { |foo| field foo: foo } + end + actual = klass.call(payload) + expected = {} + + expect(actual).to eq(expected) + end + + it 'does not create corresponding field if no attribute specified' do + payload = { 'data' => { 'type' => 'foo' } } + klass = Class.new(JSONAPI::Deserializable::Resource) do + attribute(:foo) { |foo| field foo: foo } + end + actual = klass.call(payload) + expected = {} + + expect(actual).to eq(expected) + end + + it 'defaults to creating a field with same name' do + payload = { + 'data' => { + 'type' => 'foo', + 'attributes' => { 'foo' => 'bar' } + } + } + klass = Class.new(JSONAPI::Deserializable::Resource) do + attribute :foo + end + actual = klass.call(payload) + expected = { foo: 'bar' } + + expect(actual).to eq(expected) + end +end diff --git a/spec/resource/DSL/has_many_spec.rb b/spec/resource/DSL/has_many_spec.rb new file mode 100644 index 0000000..29c9a38 --- /dev/null +++ b/spec/resource/DSL/has_many_spec.rb @@ -0,0 +1,118 @@ +require 'spec_helper' + +describe JSONAPI::Deserializable::Resource, '.has_many' do + let(:deserializable_foo) do + Class.new(JSONAPI::Deserializable::Resource) do + has_many :foo do |rel, ids, types| + field foo_ids: ids + field foo_types: types + field foo_rel: rel + end + end + end + + context 'relationship is not empty' do + let(:payload) do + { + 'data' => { + 'type' => 'foo', + 'relationships' => { + 'foo' => { + 'data' => [ + { 'type' => 'foo', 'id' => 'bar' }, + { 'type' => 'foo', 'id' => 'baz' } + ] + } + } + } + } + end + + it 'creates corresponding fields' do + actual = deserializable_foo.call(payload) + expected = { foo_ids: %w(bar baz), foo_types: %w(foo foo), + foo_rel: payload['data']['relationships']['foo'] } + + expect(actual).to eq(expected) + end + + it 'defaults to creating a field of the same name' do + klass = Class.new(JSONAPI::Deserializable::Resource) do + has_many :foo + end + actual = klass.call(payload) + expected = { foo: payload['data']['relationships']['foo'] } + + expect(actual).to eq(expected) + end + end + + context 'relationship is empty' do + it 'creates corresponding fields' do + payload = { + 'data' => { + 'type' => 'foo', + 'relationships' => { + 'foo' => { + 'data' => [] + } + } + } + } + actual = deserializable_foo.call(payload) + expected = { foo_ids: [], foo_types: [], + foo_rel: payload['data']['relationships']['foo'] } + + expect(actual).to eq(expected) + end + end + + context 'relationship is absent' do + it 'does not create corresponding fields' do + payload = { + 'data' => { + 'type' => 'foo', + 'relationships' => {} + } + } + actual = deserializable_foo.call(payload) + expected = {} + + expect(actual).to eq(expected) + end + end + + context 'there is no relationships member' do + it 'does not create corresponding fields' do + payload = { + 'data' => { + 'type' => 'foo' + } + } + actual = deserializable_foo.call(payload) + expected = {} + + expect(actual).to eq(expected) + end + end + + context 'relationship is not to-many' do + it 'does not create corresponding fields' do + payload = { + 'data' => { + 'type' => 'foo', + 'relationships' => { + 'foo' => { + 'data' => { 'type' => 'foo', 'id' => 'bar' } + } + } + } + } + + actual = deserializable_foo.call(payload) + expected = {} + + expect(actual).to eq(expected) + end + end +end diff --git a/spec/resource/DSL/has_one_spec.rb b/spec/resource/DSL/has_one_spec.rb new file mode 100644 index 0000000..4869e23 --- /dev/null +++ b/spec/resource/DSL/has_one_spec.rb @@ -0,0 +1,114 @@ +require 'spec_helper' + +describe JSONAPI::Deserializable::Resource, '.has_one' do + let(:deserializable_foo) do + Class.new(JSONAPI::Deserializable::Resource) do + has_one :foo do |rel, id, type| + field foo_id: id + field foo_type: type + field foo_rel: rel + end + end + end + + context 'relationship is not nil' do + let(:payload) do + { + 'data' => { + 'type' => 'foo', + 'relationships' => { + 'foo' => { + 'data' => { 'type' => 'foo', 'id' => 'bar' } + } + } + } + } + end + + it 'creates corresponding fields' do + actual = deserializable_foo.call(payload) + expected = { foo_id: 'bar', foo_type: 'foo', + foo_rel: payload['data']['relationships']['foo'] } + + expect(actual).to eq(expected) + end + + it 'defaults to creating a field of the same name' do + klass = Class.new(JSONAPI::Deserializable::Resource) do + has_one :foo + end + actual = klass.call(payload) + expected = { foo: payload['data']['relationships']['foo'] } + + expect(actual).to eq(expected) + end + end + + context 'relationship is nil' do + it 'creates corresponding fields' do + payload = { + 'data' => { + 'type' => 'foo', + 'relationships' => { + 'foo' => { + 'data' => nil + } + } + } + } + actual = deserializable_foo.call(payload) + expected = { foo_id: nil, foo_type: nil, + foo_rel: payload['data']['relationships']['foo'] } + + expect(actual).to eq(expected) + end + end + + context 'relationship is absent' do + it 'does not create corresponding fields' do + payload = { + 'data' => { + 'type' => 'foo', + 'relationships' => {} + } + } + actual = deserializable_foo.call(payload) + expected = {} + + expect(actual).to eq(expected) + end + end + + context 'there is no relationships member' do + it 'does not create corresponding fields' do + payload = { + 'data' => { + 'type' => 'foo' + } + } + actual = deserializable_foo.call(payload) + expected = {} + + expect(actual).to eq(expected) + end + end + + context 'relationship is not to-one' do + it 'does not create corresponding fields' do + payload = { + 'data' => { + 'type' => 'foo', + 'relationships' => { + 'foo' => { + 'data' => [] + } + } + } + } + actual = deserializable_foo.call(payload) + expected = {} + + expect(actual).to eq(expected) + end + end +end diff --git a/spec/resource/DSL/id_spec.rb b/spec/resource/DSL/id_spec.rb new file mode 100644 index 0000000..1a02b44 --- /dev/null +++ b/spec/resource/DSL/id_spec.rb @@ -0,0 +1,36 @@ +require 'spec_helper' + +describe JSONAPI::Deserializable::Resource, '.id' do + it 'creates corresponding field if id is present' do + payload = { 'data' => { 'type' => 'foo', 'id' => 'bar' } } + klass = Class.new(JSONAPI::Deserializable::Resource) do + id { |i| field id: i } + end + actual = klass.call(payload) + expected = { id: 'bar' } + + expect(actual).to eq(expected) + end + + it 'does not create corresponding field if id is absent' do + payload = { 'data' => { 'type' => 'foo' } } + klass = Class.new(JSONAPI::Deserializable::Resource) do + id { |i| field id: i } + end + actual = klass.call(payload) + expected = {} + + expect(actual).to eq(expected) + end + + it 'defaults to creating an id field' do + payload = { 'data' => { 'type' => 'foo', 'id' => 'bar' } } + klass = Class.new(JSONAPI::Deserializable::Resource) do + id + end + actual = klass.call(payload) + expected = { id: 'bar' } + + expect(actual).to eq(expected) + end +end diff --git a/spec/resource/DSL/type_spec.rb b/spec/resource/DSL/type_spec.rb new file mode 100644 index 0000000..eabcb32 --- /dev/null +++ b/spec/resource/DSL/type_spec.rb @@ -0,0 +1,25 @@ +require 'spec_helper' + +describe JSONAPI::Deserializable::Resource, '.type' do + it 'creates corresponding field' do + payload = { 'data' => { 'type' => 'foo' } } + klass = Class.new(JSONAPI::Deserializable::Resource) do + type { |t| field type: t } + end + actual = klass.call(payload) + expected = { type: 'foo' } + + expect(actual).to eq(expected) + end + + it 'defaults to creating a type field' do + payload = { 'data' => { 'type' => 'foo' } } + klass = Class.new(JSONAPI::Deserializable::Resource) do + type + end + actual = klass.call(payload) + expected = { type: 'foo' } + + expect(actual).to eq(expected) + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..95576cd --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,7 @@ +require 'simplecov' +SimpleCov.start + +require 'codecov' +SimpleCov.formatter = SimpleCov::Formatter::Codecov + +require 'jsonapi/deserializable'