├── .gitignore ├── .hound.yml ├── .rubocop.yml ├── .travis.yml ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── VERSION ├── jsonapi-deserializable.gemspec ├── lib └── jsonapi │ ├── deserializable.rb │ └── deserializable │ ├── relationship.rb │ ├── relationship │ └── dsl.rb │ ├── resource.rb │ └── resource │ └── dsl.rb └── spec ├── relationship ├── has_many_spec.rb └── has_one_spec.rb ├── resource ├── DSL │ ├── attribute_spec.rb │ ├── attributes_spec.rb │ ├── has_many_spec.rb │ ├── has_one_spec.rb │ ├── id_spec.rb │ ├── key_format_spec.rb │ └── type_spec.rb └── reverse_mapping_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | /.config 4 | /coverage/ 5 | /InstalledFiles 6 | /pkg/ 7 | /spec/reports/ 8 | /spec/examples.txt 9 | /test/tmp/ 10 | /test/version_tmp/ 11 | /tmp/ 12 | 13 | ## Specific to RubyMotion: 14 | .dat* 15 | .repl_history 16 | build/ 17 | 18 | ## Documentation cache and generated files: 19 | /.yardoc/ 20 | /_yardoc/ 21 | /doc/ 22 | /rdoc/ 23 | 24 | ## Environment normalization: 25 | /.bundle/ 26 | /vendor/bundle 27 | /lib/bundler/man/ 28 | 29 | Gemfile.lock 30 | .ruby-version 31 | .ruby-gemset 32 | 33 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 34 | .rvmrc 35 | -------------------------------------------------------------------------------- /.hound.yml: -------------------------------------------------------------------------------- 1 | ruby: 2 | config_file: .rubocop.yml 3 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | Style/PredicateName: 2 | NameWhitelist: 3 | - has_one 4 | - has_many 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | sudo: false 3 | before_install: 4 | - bundle update 5 | rvm: 6 | - 2.1 7 | - 2.2 8 | - 2.3.0 9 | - ruby-head 10 | matrix: 11 | allow_failures: 12 | - rvm: ruby-head 13 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Lucas Hosseini 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jsonapi-deserializable 2 | Ruby gem for deserializing [JSON API](http://jsonapi.org) payloads into custom 3 | hashes. 4 | 5 | ## Status 6 | 7 | [![Gem Version](https://badge.fury.io/rb/jsonapi-deserializable.svg)](https://badge.fury.io/rb/jsonapi-deserializable) 8 | [![Build Status](https://secure.travis-ci.org/jsonapi-rb/jsonapi-deserializable.svg?branch=master)](http://travis-ci.org/jsonapi-rb/deserializable?branch=master) 9 | [![codecov](https://codecov.io/gh/jsonapi-rb/jsonapi-deserializable/branch/master/graph/badge.svg)](https://codecov.io/gh/jsonapi-rb/deserializable) 10 | [![Gitter chat](https://badges.gitter.im/gitterHQ/gitter.png)](https://gitter.im/jsonapi-rb/Lobby) 11 | 12 | ## Resources 13 | 14 | * Chat: [gitter](http://gitter.im/jsonapi-rb) 15 | * Twitter: [@jsonapirb](http://twitter.com/jsonapirb) 16 | * Docs: [jsonapi-rb.org](http://jsonapi-rb.org) 17 | 18 | ## Usage and documentation 19 | 20 | See [jsonapi-rb.org/guides/deserialization](http://jsonapi-rb.org/guides/deserialization). 21 | 22 | ## License 23 | 24 | jsonapi-deserializable is released under the [MIT License](http://www.opensource.org/licenses/MIT). 25 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'rspec/core/rake_task' 3 | 4 | RSpec::Core::RakeTask.new(:spec) do |t| 5 | t.pattern = Dir.glob('spec/**/*_spec.rb') 6 | end 7 | 8 | task default: :test 9 | task test: :spec 10 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.2.0 2 | -------------------------------------------------------------------------------- /jsonapi-deserializable.gemspec: -------------------------------------------------------------------------------- 1 | version = File.read(File.expand_path('../VERSION', __FILE__)).strip 2 | 3 | Gem::Specification.new do |spec| 4 | spec.name = 'jsonapi-deserializable' 5 | spec.version = version 6 | spec.author = 'Lucas Hosseini' 7 | spec.email = 'lucas.hosseini@gmail.com' 8 | spec.summary = 'Deserialize JSON API payloads.' 9 | spec.description = 'DSL for deserializing incoming JSON API payloads ' \ 10 | 'into custom hashes.' 11 | spec.homepage = 'https://github.com/jsonapi-rb/jsonapi-deserializable' 12 | spec.license = 'MIT' 13 | 14 | spec.files = Dir['README.md', 'lib/**/*'] 15 | spec.require_path = 'lib' 16 | 17 | spec.add_development_dependency 'rake', '~> 11.3' 18 | spec.add_development_dependency 'rspec', '~> 3.4' 19 | spec.add_development_dependency 'codecov', '~> 0.1' 20 | end 21 | -------------------------------------------------------------------------------- /lib/jsonapi/deserializable.rb: -------------------------------------------------------------------------------- 1 | require 'jsonapi/deserializable/relationship' 2 | require 'jsonapi/deserializable/resource' 3 | -------------------------------------------------------------------------------- /lib/jsonapi/deserializable/relationship.rb: -------------------------------------------------------------------------------- 1 | require 'jsonapi/deserializable/relationship/dsl' 2 | 3 | module JSONAPI 4 | module Deserializable 5 | class Relationship 6 | extend DSL 7 | 8 | class << self 9 | attr_accessor :has_one_block, :has_many_block 10 | end 11 | 12 | def self.inherited(klass) 13 | super 14 | klass.has_one_block = has_one_block 15 | klass.has_many_block = has_many_block 16 | end 17 | 18 | def self.call(payload) 19 | new(payload).to_h 20 | end 21 | 22 | def initialize(payload) 23 | @document = payload 24 | @data = payload['data'] 25 | deserialize! 26 | 27 | freeze 28 | end 29 | 30 | def to_hash 31 | @hash 32 | end 33 | alias to_h to_hash 34 | 35 | private 36 | 37 | def deserialize! 38 | @hash = 39 | if @data.is_a?(Array) 40 | deserialize_has_many 41 | elsif @data.nil? || @data.is_a?(Hash) 42 | deserialize_has_one 43 | end 44 | end 45 | 46 | def deserialize_has_one 47 | block = self.class.has_one_block 48 | return {} unless block 49 | id = @data && @data['id'] 50 | type = @data && @data['type'] 51 | block.call(@document, id, type) 52 | end 53 | 54 | def deserialize_has_many 55 | block = self.class.has_many_block 56 | return {} unless block && @data.is_a?(Array) 57 | ids = @data.map { |ri| ri['id'] } 58 | types = @data.map { |ri| ri['type'] } 59 | block.call(@document, ids, types) 60 | end 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /lib/jsonapi/deserializable/relationship/dsl.rb: -------------------------------------------------------------------------------- 1 | module JSONAPI 2 | module Deserializable 3 | class Relationship 4 | module DSL 5 | DEFAULT_HAS_ONE_REL_BLOCK = proc do |_val, id, type| 6 | { type: type, id: id } 7 | end 8 | DEFAULT_HAS_MANY_REL_BLOCK = proc do |_val, ids, types| 9 | { types: types, ids: ids } 10 | end 11 | 12 | def has_one(&block) 13 | self.has_one_block = block || DEFAULT_HAS_ONE_REL_BLOCK 14 | end 15 | 16 | def has_many(&block) 17 | self.has_many_block = block || DEFAULT_HAS_MANY_REL_BLOCK 18 | end 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/jsonapi/deserializable/resource.rb: -------------------------------------------------------------------------------- 1 | require 'jsonapi/deserializable/resource/dsl' 2 | 3 | module JSONAPI 4 | module Deserializable 5 | class Resource 6 | extend DSL 7 | 8 | class << self 9 | attr_accessor :type_block, :id_block, :attr_blocks, 10 | :has_one_rel_blocks, :has_many_rel_blocks, 11 | :default_attr_block, :default_has_one_rel_block, 12 | :default_has_many_rel_block, 13 | :key_formatter 14 | end 15 | 16 | self.attr_blocks = {} 17 | self.has_one_rel_blocks = {} 18 | self.has_many_rel_blocks = {} 19 | self.key_formatter = proc { |k| k } 20 | 21 | def self.inherited(klass) 22 | super 23 | klass.type_block = type_block 24 | klass.id_block = id_block 25 | klass.attr_blocks = attr_blocks.dup 26 | klass.has_one_rel_blocks = has_one_rel_blocks.dup 27 | klass.has_many_rel_blocks = has_many_rel_blocks.dup 28 | klass.default_attr_block = default_attr_block 29 | klass.default_has_one_rel_block = default_has_one_rel_block 30 | klass.default_has_many_rel_block = default_has_many_rel_block 31 | klass.key_formatter = key_formatter 32 | end 33 | 34 | def self.call(payload) 35 | new(payload).to_h 36 | end 37 | 38 | def initialize(payload, root: '/data') 39 | @data = payload || {} 40 | @root = root 41 | @type = @data['type'] 42 | @id = @data['id'] 43 | @attributes = @data['attributes'] || {} 44 | @relationships = @data['relationships'] || {} 45 | deserialize! 46 | 47 | freeze 48 | end 49 | 50 | def to_hash 51 | @hash 52 | end 53 | alias to_h to_hash 54 | 55 | attr_reader :reverse_mapping 56 | 57 | private 58 | 59 | def register_mappings(keys, path) 60 | keys.each do |k| 61 | @reverse_mapping[k] = @root + path 62 | end 63 | end 64 | 65 | def deserialize! 66 | @reverse_mapping = {} 67 | hashes = [deserialize_type, deserialize_id, 68 | deserialize_attrs, deserialize_rels] 69 | @hash = hashes.reduce({}, :merge) 70 | end 71 | 72 | def deserialize_type 73 | block = self.class.type_block 74 | return {} unless block 75 | 76 | hash = block.call(@type) 77 | register_mappings(hash.keys, '/type') 78 | hash 79 | end 80 | 81 | def deserialize_id 82 | block = self.class.id_block 83 | return {} unless @id && block 84 | 85 | hash = block.call(@id) 86 | register_mappings(hash.keys, '/id') 87 | hash 88 | end 89 | 90 | def deserialize_attrs 91 | @attributes 92 | .map { |key, val| deserialize_attr(key, val) } 93 | .reduce({}, :merge) 94 | end 95 | 96 | def deserialize_attr(key, val) 97 | block = self.class.attr_blocks[key] || self.class.default_attr_block 98 | return {} unless block 99 | 100 | hash = block.call(val, self.class.key_formatter.call(key)) 101 | register_mappings(hash.keys, "/attributes/#{key}") 102 | hash 103 | end 104 | 105 | def deserialize_rels 106 | @relationships 107 | .map { |key, val| deserialize_rel(key, val) } 108 | .reduce({}, :merge) 109 | end 110 | 111 | def deserialize_rel(key, val) 112 | if val['data'].is_a?(Array) 113 | deserialize_has_many_rel(key, val) 114 | else 115 | deserialize_has_one_rel(key, val) 116 | end 117 | end 118 | 119 | # rubocop: disable Metrics/AbcSize 120 | def deserialize_has_one_rel(key, val) 121 | block = self.class.has_one_rel_blocks[key] || 122 | self.class.default_has_one_rel_block 123 | return {} unless block 124 | 125 | id = val['data'] && val['data']['id'] 126 | type = val['data'] && val['data']['type'] 127 | hash = block.call(val, id, type, self.class.key_formatter.call(key)) 128 | register_mappings(hash.keys, "/relationships/#{key}") 129 | hash 130 | end 131 | # rubocop: enable Metrics/AbcSize 132 | 133 | # rubocop: disable Metrics/AbcSize 134 | def deserialize_has_many_rel(key, val) 135 | block = self.class.has_many_rel_blocks[key] || 136 | self.class.default_has_many_rel_block 137 | return {} unless block && val['data'].is_a?(Array) 138 | 139 | ids = val['data'].map { |ri| ri['id'] } 140 | types = val['data'].map { |ri| ri['type'] } 141 | hash = block.call(val, ids, types, self.class.key_formatter.call(key)) 142 | register_mappings(hash.keys, "/relationships/#{key}") 143 | hash 144 | end 145 | # rubocop: enable Metrics/AbcSize 146 | end 147 | end 148 | end 149 | -------------------------------------------------------------------------------- /lib/jsonapi/deserializable/resource/dsl.rb: -------------------------------------------------------------------------------- 1 | module JSONAPI 2 | module Deserializable 3 | class Resource 4 | module DSL 5 | DEFAULT_TYPE_BLOCK = proc { |t| { type: t } } 6 | DEFAULT_ID_BLOCK = proc { |i| { id: i } } 7 | DEFAULT_ATTR_BLOCK = proc { |v, k| { k.to_sym => v } } 8 | DEFAULT_HAS_ONE_BLOCK = proc do |_, i, t, k| 9 | { "#{k}_id".to_sym => i, "#{k}_type".to_sym => t } 10 | end 11 | DEFAULT_HAS_MANY_BLOCK = proc do |_, i, t, k| 12 | { "#{k}_ids".to_sym => i, "#{k}_types".to_sym => t } 13 | end 14 | 15 | def type(&block) 16 | self.type_block = block || DEFAULT_TYPE_BLOCK 17 | end 18 | 19 | def id(&block) 20 | self.id_block = block || DEFAULT_ID_BLOCK 21 | end 22 | 23 | def attribute(key, &block) 24 | attr_blocks[key.to_s] = block || DEFAULT_ATTR_BLOCK 25 | end 26 | 27 | def attributes(*keys, &block) 28 | if keys.empty? 29 | self.default_attr_block = block || DEFAULT_ATTR_BLOCK 30 | else 31 | keys.each { |k| attribute(k, &block) } 32 | end 33 | end 34 | 35 | def has_one(key = nil, &block) 36 | if key 37 | has_one_rel_blocks[key.to_s] = block || DEFAULT_HAS_ONE_BLOCK 38 | else 39 | self.default_has_one_rel_block = block || DEFAULT_HAS_ONE_BLOCK 40 | end 41 | end 42 | 43 | def has_many(key = nil, &block) 44 | if key 45 | has_many_rel_blocks[key.to_s] = block || DEFAULT_HAS_MANY_BLOCK 46 | else 47 | self.default_has_many_rel_block = block || DEFAULT_HAS_MANY_BLOCK 48 | end 49 | end 50 | 51 | def key_format(callable = nil, &block) 52 | self.key_formatter = callable || block 53 | end 54 | end 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /spec/relationship/has_many_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe JSONAPI::Deserializable::Relationship, '.has_many' do 4 | let(:deserializable_foo) do 5 | Class.new(JSONAPI::Deserializable::Relationship) do 6 | has_many do |rel, ids, types| 7 | { foo_ids: ids, foo_types: types, foo_rel: rel } 8 | end 9 | end 10 | end 11 | 12 | context 'relationship is not empty' do 13 | let(:payload) do 14 | { 15 | 'data' => [ 16 | { 'type' => 'foo', 'id' => 'bar' }, 17 | { 'type' => 'foo', 'id' => 'baz' } 18 | ] 19 | } 20 | end 21 | 22 | it 'creates corresponding fields' do 23 | actual = deserializable_foo.call(payload) 24 | expected = { foo_ids: %w(bar baz), foo_types: %w(foo foo), 25 | foo_rel: payload } 26 | 27 | expect(actual).to eq(expected) 28 | end 29 | 30 | it 'defaults to creating ids and types fields' do 31 | klass = Class.new(JSONAPI::Deserializable::Relationship) do 32 | has_many 33 | end 34 | actual = klass.call(payload) 35 | expected = { ids: %w(bar baz), types: %w(foo foo) } 36 | 37 | expect(actual).to eq(expected) 38 | end 39 | end 40 | 41 | context 'relationship is empty' do 42 | it 'creates corresponding fields' do 43 | payload = { 'data' => [] } 44 | actual = deserializable_foo.call(payload) 45 | expected = { foo_ids: [], foo_types: [], foo_rel: payload } 46 | 47 | expect(actual).to eq(expected) 48 | end 49 | end 50 | 51 | context 'data is absent' do 52 | it 'creates an empty hash' do 53 | payload = {} 54 | actual = deserializable_foo.call(payload) 55 | expected = {} 56 | 57 | expect(actual).to eq(expected) 58 | end 59 | end 60 | 61 | context 'relationship is not to-many' do 62 | it 'does not deserialize relationship' do 63 | payload = { 'data' => nil } 64 | actual = deserializable_foo.call(payload) 65 | expected = {} 66 | 67 | expect(actual).to eq(expected) 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /spec/relationship/has_one_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe JSONAPI::Deserializable::Relationship, '.has_one' do 4 | let(:deserializable_foo) do 5 | Class.new(JSONAPI::Deserializable::Relationship) do 6 | has_one do |rel, id, type| 7 | { foo_id: id, foo_type: type, foo_rel: rel } 8 | end 9 | end 10 | end 11 | 12 | context 'relationship is not nil' do 13 | let(:payload) do 14 | { 'data' => { 'type' => 'foo', 'id' => 'bar' } } 15 | end 16 | 17 | it 'creates corresponding fields' do 18 | actual = deserializable_foo.call(payload) 19 | expected = { foo_id: 'bar', foo_type: 'foo', foo_rel: payload } 20 | 21 | expect(actual).to eq(expected) 22 | end 23 | 24 | it 'defaults to creating id and type fields' do 25 | klass = Class.new(JSONAPI::Deserializable::Relationship) do 26 | has_one 27 | end 28 | actual = klass.call(payload) 29 | expected = { id: 'bar', type: 'foo' } 30 | 31 | expect(actual).to eq(expected) 32 | end 33 | end 34 | 35 | context 'relationship is nil' do 36 | it 'creates corresponding fields' do 37 | payload = { 'data' => nil } 38 | actual = deserializable_foo.call(payload) 39 | expected = { foo_id: nil, foo_type: nil, foo_rel: payload } 40 | 41 | expect(actual).to eq(expected) 42 | end 43 | end 44 | 45 | context 'data is absent' do 46 | it 'creates corresponding fields' do 47 | payload = {} 48 | actual = deserializable_foo.call(payload) 49 | expected = { foo_id: nil, foo_type: nil, foo_rel: payload } 50 | 51 | expect(actual).to eq(expected) 52 | end 53 | end 54 | 55 | context 'relationship is not to-one' do 56 | it 'does not deserialize relationship' do 57 | payload = { 'data' => [] } 58 | actual = deserializable_foo.call(payload) 59 | expected = {} 60 | 61 | expect(actual).to eq(expected) 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /spec/resource/DSL/attribute_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe JSONAPI::Deserializable::Resource, '.attribute' do 4 | context 'when attribute is present' do 5 | context 'when a block is specified' do 6 | it 'creates corresponding field' do 7 | payload = { 8 | 'type' => 'foo', 9 | 'attributes' => { 'foo' => 'bar' } 10 | } 11 | klass = Class.new(JSONAPI::Deserializable::Resource) do 12 | attribute(:foo) { |foo| Hash[foo: foo] } 13 | end 14 | actual = klass.call(payload) 15 | expected = { foo: 'bar' } 16 | 17 | expect(actual).to eq(expected) 18 | end 19 | end 20 | 21 | context 'when no block is specified' do 22 | it 'defaults to creating a field with same name' do 23 | payload = { 24 | 'type' => 'foo', 25 | 'attributes' => { 'foo' => 'bar' } 26 | } 27 | klass = Class.new(JSONAPI::Deserializable::Resource) do 28 | attribute(:foo) 29 | end 30 | actual = klass.call(payload) 31 | expected = { foo: 'bar' } 32 | 33 | expect(actual).to eq(expected) 34 | end 35 | end 36 | end 37 | 38 | context 'when attribute is absent' do 39 | it 'does not create corresponding field if attribute is absent' do 40 | payload = { 'type' => 'foo', 'attributes' => {} } 41 | klass = Class.new(JSONAPI::Deserializable::Resource) do 42 | attribute(:foo) { |foo| Hash[foo: foo] } 43 | end 44 | actual = klass.call(payload) 45 | expected = {} 46 | 47 | expect(actual).to eq(expected) 48 | end 49 | end 50 | 51 | context 'when attributes member is absent' do 52 | it 'does not create corresponding field if no attribute specified' do 53 | payload = { 'type' => 'foo' } 54 | klass = Class.new(JSONAPI::Deserializable::Resource) do 55 | attribute(:foo) { |foo| Hash[foo: foo] } 56 | end 57 | actual = klass.call(payload) 58 | expected = {} 59 | 60 | expect(actual).to eq(expected) 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /spec/resource/DSL/attributes_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe JSONAPI::Deserializable::Resource, '.attributes' do 4 | context 'when no block is specified' do 5 | context 'when no keys are specified' do 6 | it 'defaults to creating fields with same name' do 7 | payload = { 8 | 'type' => 'foo', 9 | 'attributes' => { 'foo' => 'bar', 'baz' => 'foo' } 10 | } 11 | klass = Class.new(JSONAPI::Deserializable::Resource) do 12 | attributes 13 | end 14 | actual = klass.call(payload) 15 | expected = { foo: 'bar', baz: 'foo' } 16 | 17 | expect(actual).to eq(expected) 18 | end 19 | end 20 | 21 | context 'when keys are specified' do 22 | it 'creates fields with same name for whitelisted attributes' do 23 | payload = { 24 | 'type' => 'foo', 25 | 'attributes' => { 'foo' => 'bar', 'baz' => 'foo', 'bar' => 'foo' } 26 | } 27 | klass = Class.new(JSONAPI::Deserializable::Resource) do 28 | attributes :foo, :baz 29 | end 30 | actual = klass.call(payload) 31 | expected = { foo: 'bar', baz: 'foo' } 32 | 33 | expect(actual).to eq(expected) 34 | end 35 | end 36 | end 37 | 38 | context 'when a block is specified' do 39 | context 'when no keys are specified' do 40 | it 'defaults to creating fields with same name' do 41 | payload = { 42 | 'type' => 'foo', 43 | 'attributes' => { 'foo' => 'bar', 'baz' => 'foo' } 44 | } 45 | klass = Class.new(JSONAPI::Deserializable::Resource) do 46 | attributes do |val, key| 47 | Hash["#{key}_attr".to_sym => val] 48 | end 49 | end 50 | actual = klass.call(payload) 51 | expected = { foo_attr: 'bar', baz_attr: 'foo' } 52 | 53 | expect(actual).to eq(expected) 54 | end 55 | end 56 | 57 | context 'when keys are specified' do 58 | it 'creates customized fields for whitelisted attributes' do 59 | payload = { 60 | 'type' => 'foo', 61 | 'attributes' => { 'foo' => 'bar', 'baz' => 'foo', 'bar' => 'foo' } 62 | } 63 | klass = Class.new(JSONAPI::Deserializable::Resource) do 64 | attributes(:foo, :baz) do |val, key| 65 | Hash["#{key}_attr".to_sym => val] 66 | end 67 | end 68 | actual = klass.call(payload) 69 | expected = { foo_attr: 'bar', baz_attr: 'foo' } 70 | 71 | expect(actual).to eq(expected) 72 | end 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /spec/resource/DSL/has_many_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe JSONAPI::Deserializable::Resource, '.has_many' do 4 | let(:deserializable_foo) do 5 | Class.new(JSONAPI::Deserializable::Resource) do 6 | has_many :foo do |rel, ids, types| 7 | Hash[foo_ids: ids, foo_types: types, foo_rel: rel] 8 | end 9 | end 10 | end 11 | 12 | context 'relationship is not empty' do 13 | let(:payload) do 14 | { 15 | 'type' => 'foo', 16 | 'relationships' => { 17 | 'foo' => { 18 | 'data' => [ 19 | { 'type' => 'foo', 'id' => 'bar' }, 20 | { 'type' => 'foo', 'id' => 'baz' } 21 | ] 22 | } 23 | } 24 | } 25 | end 26 | 27 | it 'creates corresponding fields' do 28 | actual = deserializable_foo.call(payload) 29 | expected = { foo_ids: %w(bar baz), foo_types: %w(foo foo), 30 | foo_rel: payload['relationships']['foo'] } 31 | 32 | expect(actual).to eq(expected) 33 | end 34 | 35 | it 'defaults to creating a #{name}_ids and #{name}_types fields' do 36 | klass = Class.new(JSONAPI::Deserializable::Resource) do 37 | has_many 38 | end 39 | actual = klass.call(payload) 40 | expected = { foo_ids: %w(bar baz), foo_types: %w(foo foo) } 41 | 42 | expect(actual).to eq(expected) 43 | end 44 | end 45 | 46 | context 'relationship is empty' do 47 | it 'creates corresponding fields' do 48 | payload = { 49 | 'type' => 'foo', 50 | 'relationships' => { 51 | 'foo' => { 52 | 'data' => [] 53 | } 54 | } 55 | } 56 | actual = deserializable_foo.call(payload) 57 | expected = { foo_ids: [], foo_types: [], 58 | foo_rel: payload['relationships']['foo'] } 59 | 60 | expect(actual).to eq(expected) 61 | end 62 | end 63 | 64 | context 'relationship is absent' do 65 | it 'does not create corresponding fields' do 66 | payload = { 67 | 'type' => 'foo', 68 | 'relationships' => {} 69 | } 70 | actual = deserializable_foo.call(payload) 71 | expected = {} 72 | 73 | expect(actual).to eq(expected) 74 | end 75 | end 76 | 77 | context 'there is no relationships member' do 78 | it 'does not create corresponding fields' do 79 | payload = { 'type' => 'foo' } 80 | actual = deserializable_foo.call(payload) 81 | expected = {} 82 | 83 | expect(actual).to eq(expected) 84 | end 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /spec/resource/DSL/has_one_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe JSONAPI::Deserializable::Resource, '.has_one' do 4 | let(:deserializable_foo) do 5 | Class.new(JSONAPI::Deserializable::Resource) do 6 | has_one :foo do |rel, id, type| 7 | Hash[foo_id: id, foo_type: type, foo_rel: rel] 8 | end 9 | end 10 | end 11 | 12 | context 'relationship is not nil' do 13 | let(:payload) do 14 | { 15 | 'type' => 'foo', 16 | 'relationships' => { 17 | 'foo' => { 18 | 'data' => { 'type' => 'foo', 'id' => 'bar' } 19 | } 20 | } 21 | } 22 | end 23 | 24 | it 'creates corresponding fields' do 25 | actual = deserializable_foo.call(payload) 26 | expected = { foo_id: 'bar', foo_type: 'foo', 27 | foo_rel: payload['relationships']['foo'] } 28 | 29 | expect(actual).to eq(expected) 30 | end 31 | 32 | it 'defaults to creating #{name}_id and #{name}_type' do 33 | klass = Class.new(JSONAPI::Deserializable::Resource) do 34 | has_one 35 | end 36 | actual = klass.call(payload) 37 | expected = { foo_id: 'bar', foo_type: 'foo' } 38 | 39 | expect(actual).to eq(expected) 40 | end 41 | end 42 | 43 | context 'relationship value is nil' do 44 | it 'creates corresponding fields' do 45 | payload = { 46 | 'type' => 'foo', 47 | 'relationships' => { 48 | 'foo' => { 49 | 'data' => nil 50 | } 51 | } 52 | } 53 | 54 | actual = deserializable_foo.call(payload) 55 | expected = { foo_id: nil, foo_type: nil, 56 | foo_rel: payload['relationships']['foo'] } 57 | 58 | expect(actual).to eq(expected) 59 | end 60 | end 61 | 62 | context 'relationship is absent' do 63 | it 'does not create corresponding fields' do 64 | payload = { 65 | 'type' => 'foo', 66 | 'relationships' => {} 67 | } 68 | actual = deserializable_foo.call(payload) 69 | expected = {} 70 | 71 | expect(actual).to eq(expected) 72 | end 73 | end 74 | 75 | context 'there is no relationships member' do 76 | it 'does not create corresponding fields' do 77 | payload = { 'type' => 'foo' } 78 | actual = deserializable_foo.call(payload) 79 | expected = {} 80 | 81 | expect(actual).to eq(expected) 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /spec/resource/DSL/id_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe JSONAPI::Deserializable::Resource, '.id' do 4 | it 'creates corresponding field if id is present' do 5 | payload = { 'type' => 'foo', 'id' => 'bar' } 6 | klass = Class.new(JSONAPI::Deserializable::Resource) do 7 | id { |i| Hash[id: i] } 8 | end 9 | actual = klass.call(payload) 10 | expected = { id: 'bar' } 11 | 12 | expect(actual).to eq(expected) 13 | end 14 | 15 | it 'does not create corresponding field if id is absent' do 16 | payload = { 'type' => 'foo' } 17 | klass = Class.new(JSONAPI::Deserializable::Resource) do 18 | id { |i| Hash[id: i] } 19 | end 20 | actual = klass.call(payload) 21 | expected = {} 22 | 23 | expect(actual).to eq(expected) 24 | end 25 | 26 | it 'defaults to creating an id field' do 27 | payload = { 'type' => 'foo', 'id' => 'bar' } 28 | klass = Class.new(JSONAPI::Deserializable::Resource) do 29 | id 30 | end 31 | actual = klass.call(payload) 32 | expected = { id: 'bar' } 33 | 34 | expect(actual).to eq(expected) 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /spec/resource/DSL/key_format_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe JSONAPI::Deserializable::Resource, '.key_format' do 4 | subject { klass.call(payload) } 5 | 6 | let(:payload) do 7 | { 8 | 'type' => 'foo', 9 | 'attributes' => { 'foo' => 'bar', 'foo-bar' => 'baz' }, 10 | 'relationships' => { 11 | 'baz' => { 12 | 'data' => nil 13 | }, 14 | 'bar-baz' => { 15 | 'data' => [] 16 | } 17 | } 18 | } 19 | end 20 | 21 | context 'when all fields are whitelisted' do 22 | context 'when a key formatter is provided as a block' do 23 | let(:klass) do 24 | Class.new(JSONAPI::Deserializable::Resource) do 25 | key_format { |k| k.capitalize } 26 | attributes 27 | has_many 28 | has_one 29 | end 30 | end 31 | 32 | it 'formats keys accordingly' do 33 | is_expected.to eq(Foo: 'bar', 'Foo-bar'.to_sym => 'baz', 34 | Baz_id: nil, Baz_type: nil, 35 | 'Bar-baz_ids'.to_sym => [], 36 | 'Bar-baz_types'.to_sym => []) 37 | end 38 | end 39 | 40 | context 'when a key formatter is provided as a callable' do 41 | let(:klass) do 42 | Class.new(JSONAPI::Deserializable::Resource) do 43 | key_format ->(k) { k.capitalize } 44 | attributes 45 | has_many 46 | has_one 47 | end 48 | end 49 | 50 | it 'formats keys accordingly' do 51 | is_expected.to eq(Foo: 'bar', 'Foo-bar'.to_sym => 'baz', 52 | Baz_id: nil, Baz_type: nil, 53 | 'Bar-baz_ids'.to_sym => [], 54 | 'Bar-baz_types'.to_sym => []) 55 | end 56 | end 57 | end 58 | 59 | context 'when certain fields are whitelisted' do 60 | let(:klass) do 61 | Class.new(JSONAPI::Deserializable::Resource) do 62 | key_format { |k| k.capitalize } 63 | attributes :foo 64 | has_one :baz 65 | end 66 | end 67 | 68 | it 'formats keys accordingly' do 69 | is_expected.to eq(Foo: 'bar', 70 | Baz_id: nil, Baz_type: nil) 71 | end 72 | end 73 | 74 | context 'when inheriting' do 75 | let(:klass) do 76 | superclass = Class.new(JSONAPI::Deserializable::Resource) do 77 | key_format { |k| k.capitalize } 78 | end 79 | 80 | Class.new(superclass) do 81 | attributes 82 | has_many 83 | has_one 84 | end 85 | end 86 | 87 | it 'formats keys accordingly' do 88 | is_expected.to eq(Foo: 'bar', 'Foo-bar'.to_sym => 'baz', 89 | Baz_id: nil, Baz_type: nil, 90 | 'Bar-baz_ids'.to_sym => [], 91 | 'Bar-baz_types'.to_sym => []) 92 | end 93 | end 94 | end 95 | -------------------------------------------------------------------------------- /spec/resource/DSL/type_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe JSONAPI::Deserializable::Resource, '.type' do 4 | it 'creates corresponding field' do 5 | payload = { 'type' => 'foo' } 6 | klass = Class.new(JSONAPI::Deserializable::Resource) do 7 | type { |t| Hash[type: t] } 8 | end 9 | actual = klass.call(payload) 10 | expected = { type: 'foo' } 11 | 12 | expect(actual).to eq(expected) 13 | end 14 | 15 | it 'defaults to creating a type field' do 16 | payload = { 'type' => 'foo' } 17 | klass = Class.new(JSONAPI::Deserializable::Resource) do 18 | type 19 | end 20 | actual = klass.call(payload) 21 | expected = { type: 'foo' } 22 | 23 | expect(actual).to eq(expected) 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/resource/reverse_mapping_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe JSONAPI::Deserializable::Resource, '#reverse_mapping' do 4 | it 'generates reverse mapping for default type' do 5 | payload = { 'type' => 'foo' } 6 | klass = Class.new(JSONAPI::Deserializable::Resource) do 7 | type 8 | end 9 | actual = klass.new(payload).reverse_mapping 10 | expected = { type: '/data/type' } 11 | 12 | expect(actual).to eq(expected) 13 | end 14 | 15 | it 'generates reverse mapping for overriden type' do 16 | payload = { 'type' => 'foo' } 17 | klass = Class.new(JSONAPI::Deserializable::Resource) do 18 | type { |t| { custom_type: t } } 19 | end 20 | actual = klass.new(payload).reverse_mapping 21 | expected = { custom_type: '/data/type' } 22 | 23 | expect(actual).to eq(expected) 24 | end 25 | 26 | it 'generates reverse mapping for default id' do 27 | payload = { 'type' => 'foo', 'id' => 'bar' } 28 | klass = Class.new(JSONAPI::Deserializable::Resource) do 29 | id 30 | end 31 | actual = klass.new(payload).reverse_mapping 32 | expected = { id: '/data/id' } 33 | 34 | expect(actual).to eq(expected) 35 | end 36 | 37 | it 'generates reverse mapping for overriden id' do 38 | payload = { 'type' => 'foo', 'id' => 'bar' } 39 | klass = Class.new(JSONAPI::Deserializable::Resource) do 40 | id { |i| { custom_id: i } } 41 | end 42 | actual = klass.new(payload).reverse_mapping 43 | expected = { custom_id: '/data/id' } 44 | 45 | expect(actual).to eq(expected) 46 | end 47 | 48 | it 'generates reverse mapping for default attributes' do 49 | payload = { 50 | 'type' => 'foo', 51 | 'attributes' => { 52 | 'foo' => 'bar', 53 | 'baz' => 'fiz' 54 | } 55 | } 56 | klass = Class.new(JSONAPI::Deserializable::Resource) do 57 | attributes 58 | end 59 | actual = klass.new(payload).reverse_mapping 60 | expected = { foo: '/data/attributes/foo', 61 | baz: '/data/attributes/baz' } 62 | 63 | expect(actual).to eq(expected) 64 | end 65 | 66 | it 'generates reverse mapping for locally overriden attributes' do 67 | payload = { 68 | 'type' => 'foo', 69 | 'attributes' => { 70 | 'foo' => 'bar', 71 | 'baz' => 'fiz' 72 | } 73 | } 74 | klass = Class.new(JSONAPI::Deserializable::Resource) do 75 | attribute(:foo) { |foo| { custom_foo: foo } } 76 | end 77 | actual = klass.new(payload).reverse_mapping 78 | expected = { custom_foo: '/data/attributes/foo' } 79 | 80 | expect(actual).to eq(expected) 81 | end 82 | 83 | it 'generates reverse mapping for globally overriden attributes' do 84 | payload = { 85 | 'type' => 'foo', 86 | 'attributes' => { 87 | 'foo' => 'bar', 88 | 'baz' => 'fiz' 89 | } 90 | } 91 | klass = Class.new(JSONAPI::Deserializable::Resource) do 92 | attributes do |value, key| 93 | { "custom_#{key}".to_sym => value } 94 | end 95 | attribute(:foo) { |foo| { other_foo: foo } } 96 | end 97 | actual = klass.new(payload).reverse_mapping 98 | expected = { other_foo: '/data/attributes/foo', 99 | custom_baz: '/data/attributes/baz' } 100 | 101 | expect(actual).to eq(expected) 102 | end 103 | 104 | it 'generates reverse mapping for default has_one' do 105 | payload = { 106 | 'type' => 'foo', 107 | 'relationships' => { 108 | 'foo' => { 109 | 'data' => nil 110 | }, 111 | 'baz' => { 112 | 'data' => nil 113 | } 114 | } 115 | } 116 | klass = Class.new(JSONAPI::Deserializable::Resource) do 117 | has_one 118 | end 119 | actual = klass.new(payload).reverse_mapping 120 | expected = { foo_id: '/data/relationships/foo', 121 | foo_type: '/data/relationships/foo', 122 | baz_id: '/data/relationships/baz', 123 | baz_type: '/data/relationships/baz' } 124 | 125 | expect(actual).to eq(expected) 126 | end 127 | 128 | it 'generates reverse mapping for overriden has_one' do 129 | payload = { 130 | 'type' => 'foo', 131 | 'relationships' => { 132 | 'foo' => { 133 | 'data' => nil 134 | }, 135 | 'baz' => { 136 | 'data' => nil 137 | } 138 | } 139 | } 140 | klass = Class.new(JSONAPI::Deserializable::Resource) do 141 | has_one do |_val, id, type, key| 142 | { "custom_#{key}_id".to_sym => id, 143 | "custom_#{key}_type".to_sym => type } 144 | end 145 | 146 | has_one(:foo) do |_val, id, type| 147 | { other_foo_id: id, 148 | other_foo_type: type } 149 | end 150 | end 151 | actual = klass.new(payload).reverse_mapping 152 | expected = { other_foo_id: '/data/relationships/foo', 153 | other_foo_type: '/data/relationships/foo', 154 | custom_baz_id: '/data/relationships/baz', 155 | custom_baz_type: '/data/relationships/baz' } 156 | 157 | expect(actual).to eq(expected) 158 | end 159 | 160 | it 'generates reverse mapping for default has_many' do 161 | payload = { 162 | 'type' => 'foo', 163 | 'relationships' => { 164 | 'foo' => { 165 | 'data' => [] 166 | }, 167 | 'baz' => { 168 | 'data' => [] 169 | } 170 | } 171 | } 172 | klass = Class.new(JSONAPI::Deserializable::Resource) do 173 | has_many 174 | end 175 | actual = klass.new(payload).reverse_mapping 176 | expected = { foo_ids: '/data/relationships/foo', 177 | foo_types: '/data/relationships/foo', 178 | baz_ids: '/data/relationships/baz', 179 | baz_types: '/data/relationships/baz' } 180 | 181 | expect(actual).to eq(expected) 182 | end 183 | 184 | it 'generates reverse mapping for overriden has_many' do 185 | payload = { 186 | 'type' => 'foo', 187 | 'relationships' => { 188 | 'foo' => { 189 | 'data' => [] 190 | }, 191 | 'baz' => { 192 | 'data' => [] 193 | } 194 | } 195 | } 196 | klass = Class.new(JSONAPI::Deserializable::Resource) do 197 | has_many do |_val, ids, types, key| 198 | { "custom_#{key}_ids".to_sym => ids, 199 | "custom_#{key}_types".to_sym => types } 200 | end 201 | has_many(:foo) do |_val, ids, types| 202 | { other_foo_ids: ids, 203 | other_foo_types: types } 204 | end 205 | end 206 | actual = klass.new(payload).reverse_mapping 207 | expected = { other_foo_ids: '/data/relationships/foo', 208 | other_foo_types: '/data/relationships/foo', 209 | custom_baz_ids: '/data/relationships/baz', 210 | custom_baz_types: '/data/relationships/baz' } 211 | 212 | expect(actual).to eq(expected) 213 | end 214 | end 215 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'simplecov' 2 | SimpleCov.start 3 | 4 | require 'codecov' 5 | SimpleCov.formatter = SimpleCov::Formatter::Codecov 6 | 7 | require 'jsonapi/deserializable' 8 | --------------------------------------------------------------------------------