From 538e09c5b325a75464caaa92069d375b7a1e2ed8 Mon Sep 17 00:00:00 2001 From: Lucas Hosseini Date: Sun, 27 Nov 2016 03:56:42 +0100 Subject: [PATCH] Configurable deserialization + reverse mapping (#10) * Deserialize all fields. * Make blocks return hash instead of using fields method. * Ensure valid payload. * Make reverse mapping available. --- jsonapi-deserializable.gemspec | 2 + lib/jsonapi/deserializable/relationship.rb | 49 ++-- .../deserializable/relationship/dsl.rb | 15 ++ .../deserializable/relationship_dsl.rb | 21 -- lib/jsonapi/deserializable/resource.rb | 157 +++++++----- .../deserializable/resource/configuration.rb | 28 +++ lib/jsonapi/deserializable/resource/dsl.rb | 27 ++ lib/jsonapi/deserializable/resource_dsl.rb | 39 --- spec/relationship/has_many_spec.rb | 23 +- spec/relationship/has_one_spec.rb | 23 +- spec/resource/DSL/attribute_spec.rb | 20 +- spec/resource/DSL/has_many_spec.rb | 42 +--- spec/resource/DSL/has_one_spec.rb | 44 +--- spec/resource/DSL/id_spec.rb | 14 +- spec/resource/DSL/type_spec.rb | 6 +- spec/resource/configuration_spec.rb | 108 ++++++++ spec/resource/reverse_mapping_spec.rb | 231 ++++++++++++++++++ 17 files changed, 596 insertions(+), 253 deletions(-) create mode 100644 lib/jsonapi/deserializable/relationship/dsl.rb delete mode 100644 lib/jsonapi/deserializable/relationship_dsl.rb create mode 100644 lib/jsonapi/deserializable/resource/configuration.rb create mode 100644 lib/jsonapi/deserializable/resource/dsl.rb delete mode 100644 lib/jsonapi/deserializable/resource_dsl.rb create mode 100644 spec/resource/configuration_spec.rb create mode 100644 spec/resource/reverse_mapping_spec.rb diff --git a/jsonapi-deserializable.gemspec b/jsonapi-deserializable.gemspec index 4a5780c..564ae2b 100644 --- a/jsonapi-deserializable.gemspec +++ b/jsonapi-deserializable.gemspec @@ -14,6 +14,8 @@ Gem::Specification.new do |spec| spec.files = Dir['README.md', 'lib/**/*'] spec.require_path = 'lib' + spec.add_dependency 'jsonapi-parser', '0.1.1.beta3' + spec.add_development_dependency 'rake', '>=0.9' spec.add_development_dependency 'rspec', '~>3.4' spec.add_development_dependency 'codecov', '~> 0.1' diff --git a/lib/jsonapi/deserializable/relationship.rb b/lib/jsonapi/deserializable/relationship.rb index 56e809e..5e2b321 100644 --- a/lib/jsonapi/deserializable/relationship.rb +++ b/lib/jsonapi/deserializable/relationship.rb @@ -1,15 +1,17 @@ -require 'jsonapi/deserializable/relationship_dsl' +require 'jsonapi/deserializable/relationship/dsl' +require 'jsonapi/parser/relationship' module JSONAPI module Deserializable class Relationship - include RelationshipDSL + extend DSL class << self attr_accessor :has_one_block, :has_many_block end def self.inherited(klass) + super klass.has_one_block = has_one_block klass.has_many_block = has_many_block end @@ -19,9 +21,11 @@ module JSONAPI end def initialize(payload) + Parser::Relationship.parse!(payload) @document = payload @data = payload['data'] deserialize! + freeze end def to_hash @@ -32,31 +36,32 @@ module JSONAPI private def deserialize! - @hash = {} - return unless @document.key?('data') - if @data.is_a?(Array) - deserialize_has_many! - elsif @data.nil? || @data.is_a?(Hash) - deserialize_has_one! + @hash = + if @data.is_a?(Array) + deserialize_has_many + elsif @data.nil? || @data.is_a?(Hash) + deserialize_has_one + end + end + + def deserialize_has_one + id = @data && @data['id'] + type = @data && @data['type'] + if self.class.has_one_block + self.class.has_one_block.call(@document, id, type) + else + { id: id, type: type } end end - def deserialize_has_one! - return unless self.class.has_one_block - 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 + def deserialize_has_many 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) + if self.class.has_many_block + self.class.has_many_block.call(@document, ids, types) + else + { ids: ids, types: types } + end end end end diff --git a/lib/jsonapi/deserializable/relationship/dsl.rb b/lib/jsonapi/deserializable/relationship/dsl.rb new file mode 100644 index 0000000..f7592f6 --- /dev/null +++ b/lib/jsonapi/deserializable/relationship/dsl.rb @@ -0,0 +1,15 @@ +module JSONAPI + module Deserializable + class Relationship + module DSL + def has_one(&block) + self.has_one_block = block + end + + def has_many(&block) + self.has_many_block = block + end + end + end + end +end diff --git a/lib/jsonapi/deserializable/relationship_dsl.rb b/lib/jsonapi/deserializable/relationship_dsl.rb deleted file mode 100644 index 92adf2a..0000000 --- a/lib/jsonapi/deserializable/relationship_dsl.rb +++ /dev/null @@ -1,21 +0,0 @@ -module JSONAPI - module Deserializable - module RelationshipDSL - def self.included(base) - base.extend(ClassMethods) - end - - module ClassMethods - def has_one(&block) - block ||= proc { |rel| field relationship: rel } - self.has_one_block = block - end - - def has_many(&block) - block ||= proc { |rel| field relationship: rel } - self.has_many_block = block - end - end - end - end -end diff --git a/lib/jsonapi/deserializable/resource.rb b/lib/jsonapi/deserializable/resource.rb index e08944a..7a348f4 100644 --- a/lib/jsonapi/deserializable/resource.rb +++ b/lib/jsonapi/deserializable/resource.rb @@ -1,33 +1,49 @@ -require 'jsonapi/deserializable/resource_dsl' +require 'jsonapi/deserializable/resource/configuration' +require 'jsonapi/deserializable/resource/dsl' +require 'jsonapi/parser/resource' module JSONAPI module Deserializable class Resource - include ResourceDSL + extend DSL class << self attr_accessor :type_block, :id_block, :attr_blocks, - :has_one_rel_blocks, :has_many_rel_blocks + :has_one_rel_blocks, :has_many_rel_blocks, + :configuration end - self.attr_blocks = {} + @class_cache = {} + + self.configuration = Configuration.new + self.attr_blocks = {} self.has_one_rel_blocks = {} self.has_many_rel_blocks = {} def self.inherited(klass) super - klass.type_block = type_block - klass.id_block = id_block - klass.attr_blocks = attr_blocks.dup + klass.configuration = configuration.dup + klass.type_block = type_block + klass.id_block = id_block + klass.attr_blocks = attr_blocks.dup klass.has_one_rel_blocks = has_one_rel_blocks.dup klass.has_many_rel_blocks = has_many_rel_blocks.dup end + def self.configure + yield(configuration) + end + + def self.[](name) + @class_cache[name] ||= Class.new(self) + end + def self.call(payload) new(payload).to_h end def initialize(payload) + Parser::Resource.parse!(payload) @document = payload @data = @document['data'] @type = @data['type'] @@ -35,6 +51,7 @@ module JSONAPI @attributes = @data['attributes'] || {} @relationships = @data['relationships'] || {} deserialize! + freeze end def to_hash @@ -42,69 +59,97 @@ module JSONAPI end alias to_h to_hash + attr_reader :reverse_mapping + private + def configuration + self.class.configuration + end + + def register_mappings(keys, path) + keys.each do |k| + @reverse_mapping[k] = path + end + end + def deserialize! - @hash = {} - deserialize_type! - deserialize_id! - deserialize_attrs! - deserialize_rels! + @reverse_mapping = {} + hashes = [deserialize_type, deserialize_id, + deserialize_attrs, deserialize_rels] + @hash = hashes.reduce({}, :merge) end - def deserialize_type! - return unless @type && self.class.type_block - instance_exec(@type, &self.class.type_block) + def deserialize_type + block = self.class.type_block || configuration.default_type + hash = block.call(@type) + register_mappings(hash.keys, '/data/type') + hash end - def deserialize_id! - return unless @id && self.class.id_block - instance_exec(@id, &self.class.id_block) + def deserialize_id + return {} unless @id + block = self.class.id_block || configuration.default_id + hash = block.call(@id) + register_mappings(hash.keys, '/data/id') + hash end - def deserialize_attrs! - self.class.attr_blocks.each do |attr, block| - next unless @attributes.key?(attr) - instance_exec(@attributes[attr], &block) + def deserialize_attrs + @attributes + .map { |key, val| deserialize_attr(key, val) } + .reduce({}, :merge) + end + + def deserialize_attr(key, val) + hash = if self.class.attr_blocks.key?(key) + self.class.attr_blocks[key].call(val) + else + configuration.default_attribute.call(key, val) + end + register_mappings(hash.keys, "/data/attributes/#{key}") + hash + end + + def deserialize_rels + @relationships + .map { |key, val| deserialize_rel(key, val) } + .reduce({}, :merge) + end + + def deserialize_rel(key, val) + hash = if val['data'].is_a?(Array) + deserialize_has_many_rel(key, val) + else + deserialize_has_one_rel(key, val) + end + register_mappings(hash.keys, "/data/relationships/#{key}") + hash + end + + # rubocop: disable Metrics/AbcSize + def deserialize_has_one_rel(key, val) + id = val['data'] && val['data']['id'] + type = val['data'] && val['data']['type'] + if self.class.has_one_rel_blocks.key?(key) + self.class.has_one_rel_blocks[key].call(val, id, type) + else + configuration.default_has_one.call(key, val, id, type) end end + # rubocop: enable Metrics/AbcSize - def deserialize_rels! - deserialize_has_one_rels! - deserialize_has_many_rels! - end - - def deserialize_has_one_rels! - self.class.has_one_rel_blocks.each do |key, block| - rel = @relationships[key] - next unless rel && (rel['data'].nil? || rel['data'].is_a?(Hash)) - deserialize_has_one_rel!(rel, &block) + # rubocop: disable Metrics/AbcSize + def deserialize_has_many_rel(key, val) + ids = val['data'].map { |ri| ri['id'] } + types = val['data'].map { |ri| ri['type'] } + if self.class.has_many_rel_blocks.key?(key) + self.class.has_many_rel_blocks[key].call(val, ids, types) + else + configuration.default_has_many.call(key, val, ids, types) end end - - def deserialize_has_one_rel!(rel, &block) - id = rel['data'] && rel['data']['id'] - type = rel['data'] && rel['data']['type'] - instance_exec(rel, id, type, &block) - end - - def deserialize_has_many_rels! - self.class.has_many_rel_blocks.each do |key, block| - rel = @relationships[key] - next unless rel && rel['data'].is_a?(Array) - deserialize_has_many_rel!(rel, &block) - end - end - - def deserialize_has_many_rel!(rel, &block) - ids = rel['data'].map { |ri| ri['id'] } - types = rel['data'].map { |ri| ri['type'] } - instance_exec(rel, ids, types, &block) - end - - def field(hash) - @hash.merge!(hash) - end + # rubocop: enable Metrics/AbcSize end end end diff --git a/lib/jsonapi/deserializable/resource/configuration.rb b/lib/jsonapi/deserializable/resource/configuration.rb new file mode 100644 index 0000000..bfd2a78 --- /dev/null +++ b/lib/jsonapi/deserializable/resource/configuration.rb @@ -0,0 +1,28 @@ +module JSONAPI + module Deserializable + class Resource + class Configuration + DEFAULT_TYPE_BLOCK = proc { |t| { type: t } } + DEFAULT_ID_BLOCK = proc { |i| { id: i } } + DEFAULT_ATTR_BLOCK = proc { |k, v| { k.to_sym => v } } + DEFAULT_HAS_ONE_BLOCK = proc do |k, _, i, t| + { "#{k}_id".to_sym => i, "#{k}_type".to_sym => t } + end + DEFAULT_HAS_MANY_BLOCK = proc do |k, _, i, t| + { "#{k}_ids".to_sym => i, "#{k}_types".to_sym => t } + end + + attr_accessor :default_type, :default_id, :default_attribute, + :default_has_one, :default_has_many + + def initialize + self.default_type = DEFAULT_TYPE_BLOCK + self.default_id = DEFAULT_ID_BLOCK + self.default_attribute = DEFAULT_ATTR_BLOCK + self.default_has_one = DEFAULT_HAS_ONE_BLOCK + self.default_has_many = DEFAULT_HAS_MANY_BLOCK + end + end + end + end +end diff --git a/lib/jsonapi/deserializable/resource/dsl.rb b/lib/jsonapi/deserializable/resource/dsl.rb new file mode 100644 index 0000000..88cf357 --- /dev/null +++ b/lib/jsonapi/deserializable/resource/dsl.rb @@ -0,0 +1,27 @@ +module JSONAPI + module Deserializable + class Resource + module DSL + def type(&block) + self.type_block = block + end + + def id(&block) + self.id_block = block + end + + def attribute(key, &block) + attr_blocks[key.to_s] = block + end + + def has_one(key, &block) + has_one_rel_blocks[key.to_s] = block + end + + def has_many(key, &block) + has_many_rel_blocks[key.to_s] = block + end + end + end + end +end diff --git a/lib/jsonapi/deserializable/resource_dsl.rb b/lib/jsonapi/deserializable/resource_dsl.rb deleted file mode 100644 index ce8194a..0000000 --- a/lib/jsonapi/deserializable/resource_dsl.rb +++ /dev/null @@ -1,39 +0,0 @@ -module JSONAPI - module Deserializable - module ResourceDSL - def self.included(base) - base.extend(ClassMethods) - end - - module ClassMethods - def type(&block) - block ||= proc { |type| field type: type } - self.type_block = block - end - - def id(&block) - block ||= proc { |id| field id: id } - self.id_block = block - end - - def attribute(key, options = {}, &block) - unless block - options[:key] ||= key.to_sym - block = proc { |attr| field key => attr } - end - attr_blocks[key.to_s] = block - end - - def has_one(key, &block) - block ||= proc { |rel| field key.to_sym => rel } - has_one_rel_blocks[key.to_s] = block - end - - def has_many(key, &block) - block ||= proc { |rel| field key.to_sym => rel } - has_many_rel_blocks[key.to_s] = block - end - end - end - end -end diff --git a/spec/relationship/has_many_spec.rb b/spec/relationship/has_many_spec.rb index d13085d..87c25ad 100644 --- a/spec/relationship/has_many_spec.rb +++ b/spec/relationship/has_many_spec.rb @@ -4,9 +4,7 @@ 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 + { foo_ids: ids, foo_types: types, foo_rel: rel } end end end @@ -29,12 +27,10 @@ describe JSONAPI::Deserializable::Relationship, '.has_many' do expect(actual).to eq(expected) end - it 'defaults to creating a relationship field' do - klass = Class.new(JSONAPI::Deserializable::Relationship) do - has_many - end + it 'defaults to creating ids and types fields' do + klass = Class.new(JSONAPI::Deserializable::Relationship) actual = klass.call(payload) - expected = { relationship: payload } + expected = { ids: %w(bar baz), types: %w(foo foo) } expect(actual).to eq(expected) end @@ -51,20 +47,19 @@ describe JSONAPI::Deserializable::Relationship, '.has_many' do end context 'data is absent' do - it 'does not create corresponding fields' do + it 'raises InvalidDocument' do payload = {} - actual = deserializable_foo.call(payload) - expected = {} - expect(actual).to eq(expected) + expect { deserializable_foo.call(payload) } + .to raise_error(JSONAPI::Parser::InvalidDocument) end end context 'relationship is not to-many' do - it 'does not create corresponding fields' do + it 'falls back to default to-one deserialization scheme' do payload = { 'data' => nil } actual = deserializable_foo.call(payload) - expected = {} + expected = { id: nil, type: nil } expect(actual).to eq(expected) end diff --git a/spec/relationship/has_one_spec.rb b/spec/relationship/has_one_spec.rb index fe9f0c6..d000ac3 100644 --- a/spec/relationship/has_one_spec.rb +++ b/spec/relationship/has_one_spec.rb @@ -4,9 +4,7 @@ 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 + { foo_id: id, foo_type: type, foo_rel: rel } end end end @@ -23,12 +21,10 @@ describe JSONAPI::Deserializable::Relationship, '.has_one' do expect(actual).to eq(expected) end - it 'defaults to creating a relationship field' do - klass = Class.new(JSONAPI::Deserializable::Relationship) do - has_one - end + it 'defaults to creating id and type fields' do + klass = Class.new(JSONAPI::Deserializable::Relationship) actual = klass.call(payload) - expected = { relationship: payload } + expected = { id: 'bar', type: 'foo' } expect(actual).to eq(expected) end @@ -45,20 +41,19 @@ describe JSONAPI::Deserializable::Relationship, '.has_one' do end context 'data is absent' do - it 'does not create corresponding fields' do + it 'raises InvalidDocument' do payload = {} - actual = deserializable_foo.call(payload) - expected = {} - expect(actual).to eq(expected) + expect { deserializable_foo.call(payload) } + .to raise_error(JSONAPI::Parser::InvalidDocument) end end context 'relationship is not to-one' do - it 'does not create corresponding fields' do + it 'falls back to default has_many deserialization scheme ' do payload = { 'data' => [] } actual = deserializable_foo.call(payload) - expected = {} + expected = { ids: [], types: [] } expect(actual).to eq(expected) end diff --git a/spec/resource/DSL/attribute_spec.rb b/spec/resource/DSL/attribute_spec.rb index da97f6f..1e4d48e 100644 --- a/spec/resource/DSL/attribute_spec.rb +++ b/spec/resource/DSL/attribute_spec.rb @@ -9,21 +9,21 @@ describe JSONAPI::Deserializable::Resource, '.attribute' do } } klass = Class.new(JSONAPI::Deserializable::Resource) do - attribute(:foo) { |foo| field foo: foo } + attribute(:foo) { |foo| Hash[foo: foo] } end actual = klass.call(payload) - expected = { foo: 'bar' } + expected = { foo: 'bar', type: 'foo' } expect(actual).to eq(expected) end it 'does not create corresponding field if attribute is absent' do - payload = { 'data' => { 'type' => 'foo' }, 'attributes' => {} } + payload = { 'data' => { 'type' => 'foo', 'attributes' => {} } } klass = Class.new(JSONAPI::Deserializable::Resource) do - attribute(:foo) { |foo| field foo: foo } + attribute(:foo) { |foo| Hash[foo: foo] } end actual = klass.call(payload) - expected = {} + expected = { type: 'foo' } expect(actual).to eq(expected) end @@ -31,10 +31,10 @@ describe JSONAPI::Deserializable::Resource, '.attribute' do 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 } + attribute(:foo) { |foo| Hash[foo: foo] } end actual = klass.call(payload) - expected = {} + expected = { type: 'foo' } expect(actual).to eq(expected) end @@ -46,11 +46,9 @@ describe JSONAPI::Deserializable::Resource, '.attribute' do 'attributes' => { 'foo' => 'bar' } } } - klass = Class.new(JSONAPI::Deserializable::Resource) do - attribute :foo - end + klass = JSONAPI::Deserializable::Resource actual = klass.call(payload) - expected = { foo: 'bar' } + expected = { foo: 'bar', type: 'foo' } expect(actual).to eq(expected) end diff --git a/spec/resource/DSL/has_many_spec.rb b/spec/resource/DSL/has_many_spec.rb index 29c9a38..e6ab544 100644 --- a/spec/resource/DSL/has_many_spec.rb +++ b/spec/resource/DSL/has_many_spec.rb @@ -4,9 +4,7 @@ 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 + Hash[foo_ids: ids, foo_types: types, foo_rel: rel] end end end @@ -31,17 +29,16 @@ describe JSONAPI::Deserializable::Resource, '.has_many' do 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'] } + foo_rel: payload['data']['relationships']['foo'], + type: '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 + it 'defaults to creating a #{name}_ids and #{name}_types fields' do + klass = JSONAPI::Deserializable::Resource actual = klass.call(payload) - expected = { foo: payload['data']['relationships']['foo'] } + expected = { foo_ids: %w(bar baz), foo_types: %w(foo foo), type: 'foo' } expect(actual).to eq(expected) end @@ -61,7 +58,8 @@ describe JSONAPI::Deserializable::Resource, '.has_many' do } actual = deserializable_foo.call(payload) expected = { foo_ids: [], foo_types: [], - foo_rel: payload['data']['relationships']['foo'] } + foo_rel: payload['data']['relationships']['foo'], + type: 'foo' } expect(actual).to eq(expected) end @@ -76,7 +74,7 @@ describe JSONAPI::Deserializable::Resource, '.has_many' do } } actual = deserializable_foo.call(payload) - expected = {} + expected = { type: 'foo' } expect(actual).to eq(expected) end @@ -90,27 +88,7 @@ describe JSONAPI::Deserializable::Resource, '.has_many' do } } 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 = {} + expected = { type: 'foo' } expect(actual).to eq(expected) end diff --git a/spec/resource/DSL/has_one_spec.rb b/spec/resource/DSL/has_one_spec.rb index 4869e23..c283fe9 100644 --- a/spec/resource/DSL/has_one_spec.rb +++ b/spec/resource/DSL/has_one_spec.rb @@ -4,9 +4,7 @@ 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 + Hash[foo_id: id, foo_type: type, foo_rel: rel] end end end @@ -28,23 +26,22 @@ describe JSONAPI::Deserializable::Resource, '.has_one' do it 'creates corresponding fields' do actual = deserializable_foo.call(payload) expected = { foo_id: 'bar', foo_type: 'foo', - foo_rel: payload['data']['relationships']['foo'] } + foo_rel: payload['data']['relationships']['foo'], + type: '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 + it 'defaults to creating #{name}_id and #{name}_type' do + klass = JSONAPI::Deserializable::Resource actual = klass.call(payload) - expected = { foo: payload['data']['relationships']['foo'] } + expected = { foo_id: 'bar', foo_type: 'foo', type: 'foo' } expect(actual).to eq(expected) end end - context 'relationship is nil' do + context 'relationship value is nil' do it 'creates corresponding fields' do payload = { 'data' => { @@ -56,9 +53,11 @@ describe JSONAPI::Deserializable::Resource, '.has_one' do } } } + actual = deserializable_foo.call(payload) expected = { foo_id: nil, foo_type: nil, - foo_rel: payload['data']['relationships']['foo'] } + foo_rel: payload['data']['relationships']['foo'], + type: 'foo' } expect(actual).to eq(expected) end @@ -73,7 +72,7 @@ describe JSONAPI::Deserializable::Resource, '.has_one' do } } actual = deserializable_foo.call(payload) - expected = {} + expected = { type: 'foo' } expect(actual).to eq(expected) end @@ -87,26 +86,7 @@ describe JSONAPI::Deserializable::Resource, '.has_one' do } } 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 = {} + expected = { type: 'foo' } expect(actual).to eq(expected) end diff --git a/spec/resource/DSL/id_spec.rb b/spec/resource/DSL/id_spec.rb index 1a02b44..31e872d 100644 --- a/spec/resource/DSL/id_spec.rb +++ b/spec/resource/DSL/id_spec.rb @@ -4,10 +4,10 @@ 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 } + id { |i| Hash[id: i] } end actual = klass.call(payload) - expected = { id: 'bar' } + expected = { id: 'bar', type: 'foo' } expect(actual).to eq(expected) end @@ -15,21 +15,19 @@ describe JSONAPI::Deserializable::Resource, '.id' do 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 } + id { |i| Hash[id: i] } end actual = klass.call(payload) - expected = {} + expected = { type: 'foo' } 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 + klass = JSONAPI::Deserializable::Resource actual = klass.call(payload) - expected = { id: 'bar' } + expected = { id: 'bar', type: 'foo' } expect(actual).to eq(expected) end diff --git a/spec/resource/DSL/type_spec.rb b/spec/resource/DSL/type_spec.rb index eabcb32..ed6b031 100644 --- a/spec/resource/DSL/type_spec.rb +++ b/spec/resource/DSL/type_spec.rb @@ -4,7 +4,7 @@ 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 } + type { |t| Hash[type: t] } end actual = klass.call(payload) expected = { type: 'foo' } @@ -14,9 +14,7 @@ describe JSONAPI::Deserializable::Resource, '.type' do it 'defaults to creating a type field' do payload = { 'data' => { 'type' => 'foo' } } - klass = Class.new(JSONAPI::Deserializable::Resource) do - type - end + klass = JSONAPI::Deserializable::Resource actual = klass.call(payload) expected = { type: 'foo' } diff --git a/spec/resource/configuration_spec.rb b/spec/resource/configuration_spec.rb new file mode 100644 index 0000000..ede5de5 --- /dev/null +++ b/spec/resource/configuration_spec.rb @@ -0,0 +1,108 @@ +require 'spec_helper' + +describe JSONAPI::Deserializable::Resource, '.configure' do + it 'overrides global default attribute deserialization scheme' do + payload = { + 'data' => { + 'type' => 'foo', + 'attributes' => { + 'foo' => 'bar', + 'baz' => 'foo' + } + } + } + begin + JSONAPI::Deserializable::Resource.configure do |cfg| + cfg.default_attribute = proc do |key, value| + { "custom_#{key}".to_sym => value } + end + end + klass = JSONAPI::Deserializable::Resource + actual = klass.call(payload) + expected = { custom_foo: 'bar', custom_baz: 'foo', type: 'foo' } + + expect(actual).to eq(expected) + ensure + JSONAPI::Deserializable::Resource.configuration = + JSONAPI::Deserializable::Resource::Configuration.new + end + end + + it 'overrides default attribute deserialization scheme' do + payload = { + 'data' => { + 'type' => 'foo', + 'attributes' => { + 'foo' => 'bar', + 'baz' => 'foo' + } + } + } + JSONAPI::Deserializable::Resource[:c1].configure do |cfg| + cfg.default_attribute = proc do |key, value| + { "custom_#{key}".to_sym => value } + end + end + klass = JSONAPI::Deserializable::Resource[:c1] + actual = klass.call(payload) + expected = { custom_foo: 'bar', custom_baz: 'foo', type: 'foo' } + + expect(actual).to eq(expected) + end + + it 'overrides the default has_many relationship deserialization scheme' do + payload = { + 'data' => { + 'type' => 'foo', + 'relationships' => { + 'foo' => { + 'data' => [{ 'type' => 'bar', 'id' => 'baz' }, + { 'type' => 'foo', 'id' => 'bar' }] + }, + 'bar' => { + 'data' => [{ 'type' => 'baz', 'id' => 'foo' }, + { 'type' => 'baz', 'id' => 'buz' }] + } + } + } + } + JSONAPI::Deserializable::Resource[:c1].configure do |cfg| + cfg.default_has_many = proc do |name, _value, ids, types| + { "custom_#{name}_ids".to_sym => ids, + "custom_#{name}_types".to_sym => types } + end + end + klass = JSONAPI::Deserializable::Resource[:c1] + actual = klass.call(payload) + expected = { custom_foo_ids: %w(baz bar), custom_foo_types: %w(bar foo), + custom_bar_ids: %w(foo buz), custom_bar_types: %w(baz baz), + type: 'foo' } + + expect(actual).to eq(expected) + end + + it 'overrides the default has_one relationship deserialization scheme' do + payload = { + 'data' => { + 'type' => 'foo', + 'relationships' => { + 'foo' => { 'data' => { 'type' => 'bar', 'id' => 'baz' } }, + 'bar' => { 'data' => { 'type' => 'foo', 'id' => 'bar' } } + } + } + } + JSONAPI::Deserializable::Resource[:c1].configure do |cfg| + cfg.default_has_one = proc do |name, _value, id, type| + { "custom_#{name}_id".to_sym => id, + "custom_#{name}_type".to_sym => type } + end + end + klass = JSONAPI::Deserializable::Resource[:c1] + actual = klass.call(payload) + expected = { custom_foo_id: 'baz', custom_foo_type: 'bar', + custom_bar_id: 'bar', custom_bar_type: 'foo', + type: 'foo' } + + expect(actual).to eq(expected) + end +end diff --git a/spec/resource/reverse_mapping_spec.rb b/spec/resource/reverse_mapping_spec.rb new file mode 100644 index 0000000..dc16011 --- /dev/null +++ b/spec/resource/reverse_mapping_spec.rb @@ -0,0 +1,231 @@ +require 'spec_helper' + +describe JSONAPI::Deserializable::Resource, '#reverse_mapping' do + it 'generates reverse mapping for default type' do + payload = { 'data' => { 'type' => 'foo' } } + klass = JSONAPI::Deserializable::Resource + actual = klass.new(payload).reverse_mapping + expected = { type: '/data/type' } + + expect(actual).to eq(expected) + end + + it 'generates reverse mapping for overriden type' do + payload = { 'data' => { 'type' => 'foo' } } + klass = Class.new(JSONAPI::Deserializable::Resource) do + type { |t| { custom_type: t } } + end + actual = klass.new(payload).reverse_mapping + expected = { custom_type: '/data/type' } + + expect(actual).to eq(expected) + end + + it 'generates reverse mapping for default id' do + payload = { 'data' => { 'type' => 'foo', 'id' => 'bar' } } + klass = JSONAPI::Deserializable::Resource + actual = klass.new(payload).reverse_mapping + expected = { id: '/data/id', type: '/data/type' } + + expect(actual).to eq(expected) + end + + it 'generates reverse mapping for overriden id' do + payload = { 'data' => { 'type' => 'foo', 'id' => 'bar' } } + klass = Class.new(JSONAPI::Deserializable::Resource) do + id { |i| { custom_id: i } } + end + actual = klass.new(payload).reverse_mapping + expected = { custom_id: '/data/id', type: '/data/type' } + + expect(actual).to eq(expected) + end + + it 'generates reverse mapping for default attributes' do + payload = { + 'data' => { + 'type' => 'foo', + 'attributes' => { + 'foo' => 'bar', + 'baz' => 'fiz' + } + } + } + klass = JSONAPI::Deserializable::Resource + actual = klass.new(payload).reverse_mapping + expected = { type: '/data/type', + foo: '/data/attributes/foo', + baz: '/data/attributes/baz' } + + expect(actual).to eq(expected) + end + + it 'generates reverse mapping for locally overriden attributes' do + payload = { + 'data' => { + 'type' => 'foo', + 'attributes' => { + 'foo' => 'bar', + 'baz' => 'fiz' + } + } + } + klass = Class.new(JSONAPI::Deserializable::Resource) do + attribute(:foo) { |foo| { custom_foo: foo } } + end + actual = klass.new(payload).reverse_mapping + expected = { type: '/data/type', + custom_foo: '/data/attributes/foo', + baz: '/data/attributes/baz' } + + expect(actual).to eq(expected) + end + + it 'generates reverse mapping for globally overriden attributes' do + payload = { + 'data' => { + 'type' => 'foo', + 'attributes' => { + 'foo' => 'bar', + 'baz' => 'fiz' + } + } + } + klass = Class.new(JSONAPI::Deserializable::Resource) do + attribute(:foo) { |foo| { other_foo: foo } } + end + klass.configure do |config| + config.default_attribute = proc do |key, value| + { "custom_#{key}".to_sym => value } + end + end + actual = klass.new(payload).reverse_mapping + expected = { type: '/data/type', + other_foo: '/data/attributes/foo', + custom_baz: '/data/attributes/baz' } + + expect(actual).to eq(expected) + end + + it 'generates reverse mapping for default has_many' do + payload = { + 'data' => { + 'type' => 'foo', + 'relationships' => { + 'foo' => { + 'data' => nil + }, + 'baz' => { + 'data' => nil + } + } + } + } + klass = JSONAPI::Deserializable::Resource + actual = klass.new(payload).reverse_mapping + expected = { type: '/data/type', + foo_id: '/data/relationships/foo', + foo_type: '/data/relationships/foo', + baz_id: '/data/relationships/baz', + baz_type: '/data/relationships/baz' } + + expect(actual).to eq(expected) + end + + it 'generates reverse mapping for overriden has_one' do + payload = { + 'data' => { + 'type' => 'foo', + 'relationships' => { + 'foo' => { + 'data' => nil + }, + 'baz' => { + 'data' => nil + } + } + } + } + klass = Class.new(JSONAPI::Deserializable::Resource) do + has_one(:foo) do |_val, id, type| + { other_foo_id: id, + other_foo_type: type } + end + end + klass.configure do |config| + config.default_has_one = proc do |key, _val, id, type| + { "custom_#{key}_id".to_sym => id, + "custom_#{key}_type".to_sym => type } + end + end + actual = klass.new(payload).reverse_mapping + expected = { type: '/data/type', + other_foo_id: '/data/relationships/foo', + other_foo_type: '/data/relationships/foo', + custom_baz_id: '/data/relationships/baz', + custom_baz_type: '/data/relationships/baz' } + + expect(actual).to eq(expected) + end + + it 'generates reverse mapping for default has_many' do + payload = { + 'data' => { + 'type' => 'foo', + 'relationships' => { + 'foo' => { + 'data' => [] + }, + 'baz' => { + 'data' => [] + } + } + } + } + klass = JSONAPI::Deserializable::Resource + actual = klass.new(payload).reverse_mapping + expected = { type: '/data/type', + foo_ids: '/data/relationships/foo', + foo_types: '/data/relationships/foo', + baz_ids: '/data/relationships/baz', + baz_types: '/data/relationships/baz' } + + expect(actual).to eq(expected) + end + + it 'generates reverse mapping for overriden has_many' do + payload = { + 'data' => { + 'type' => 'foo', + 'relationships' => { + 'foo' => { + 'data' => [] + }, + 'baz' => { + 'data' => [] + } + } + } + } + klass = Class.new(JSONAPI::Deserializable::Resource) do + has_many(:foo) do |_val, ids, types| + { other_foo_ids: ids, + other_foo_types: types } + end + end + klass.configure do |config| + config.default_has_many = proc do |key, _val, ids, types| + { "custom_#{key}_ids".to_sym => ids, + "custom_#{key}_types".to_sym => types } + end + end + actual = klass.new(payload).reverse_mapping + expected = { type: '/data/type', + other_foo_ids: '/data/relationships/foo', + other_foo_types: '/data/relationships/foo', + custom_baz_ids: '/data/relationships/baz', + custom_baz_types: '/data/relationships/baz' } + + expect(actual).to eq(expected) + end +end