Skip to content

Commit dbd774e

Browse files
committed
add unrestricted deserializer for backward compatibility
1 parent 3c8c6e4 commit dbd774e

File tree

5 files changed

+220
-3
lines changed

5 files changed

+220
-3
lines changed

.rubocop.yml

+5
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ AllCops:
44
- !ruby/regexp /(vendor|bundle|bin|db|tmp)\/.*/
55
DisplayCopNames: true
66
DisplayStyleGuide: true
7+
TargetRubyVersion: 2.3
78

89
Lint/AssignmentInCondition:
910
Enabled: false
@@ -34,6 +35,10 @@ Metrics/LineLength:
3435
Metrics/MethodLength:
3536
Max: 25
3637

38+
Metrics/BlockLength:
39+
Exclude:
40+
- spec/**/*
41+
3742
Metrics/PerceivedComplexity:
3843
Max: 9 # TODO: Lower to 7
3944

README.md

+6-2
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,14 @@ require 'jsonapi'
2626
Then, parse a JSON API document:
2727

2828
```ruby
29-
hash = JSONAPI.to_active_record_hash(json_api_params, options: {}, klass: nil)
29+
hash = JSONAPI::Rails.to_active_record_hash(json_api_params, options: {}, klass: nil)
3030
```
3131

32-
Note that klass is optional, and defaults to nil, but will infer the type from the json api document.
32+
Notes
33+
- that klass is optional, and defaults to nil, but will infer the type from the json api document.
34+
- this will not do any key transforms -- casing will be consistent from input to output
35+
- this does not perform validations. (see jsonapi/validations)
36+
3337

3438
### Available Options
3539

lib/jsonapi/rails/deserializable_resource.rb

+17-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
# frozen_string_literal: true
12
require 'jsonapi/deserializable'
23

34
module JSONAPI
@@ -20,6 +21,7 @@ module Rails
2021
# jsonapi/deserializable
2122
class DeserializableResource
2223
require_relative 'deserializable_resource/builder'
24+
require_relative 'deserializable_resource/unrestricted'
2325

2426
class << self
2527
def deserializable_cache
@@ -39,12 +41,16 @@ def deserializable_for(klass)
3941
DeserializableResource::Builder.for_class(klass)
4042
end
4143

44+
def unrestricted_deserialization(hash)
45+
DeserializableResource::Unrestricted.to_active_record_hash(hash)
46+
end
47+
4248
def deserializable_class(type, klass)
4349
klass || type_to_model(type)
4450
end
4551

4652
def type_to_model(type)
47-
type.classify.constantize
53+
type.classify.safe_constantize
4854
end
4955
end
5056

@@ -56,6 +62,11 @@ def type_to_model(type)
5662
# then when to_hash is called, the class will be derived, and
5763
# a class will be used for deserialization as if the
5864
# user specified the deserialization target class.
65+
#
66+
# Note that by specifying klass to false, no class will be used.
67+
# This means that every part of the JSONAPI Document will be
68+
# deserialized, and none of it will be whitelisted against any
69+
# class
5970
def initialize(hash, options: {}, klass: nil)
6071
@_hash = hash
6172
@_options = options
@@ -66,6 +77,11 @@ def to_hash
6677
type = _hash['data']['type']
6778
klass = self.class.deserializable_class(type, _klass)
6879

80+
if _klass == false || klass.nil?
81+
puts "WARNING: class not found for type of `#{type}` or specified _klass `#{_klass&.name}`"
82+
return self.class.unrestricted_deserialization(_hash)
83+
end
84+
6985
self.class[klass].call(_hash).with_indifferent_access
7086
end
7187
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
module JSONAPI
2+
module Rails
3+
class DeserializableResource
4+
# Taken and stripped down from ActiveModelSerializers
5+
# 0.10.2
6+
#
7+
# For use when a type in ruby is not present.
8+
# - it is dangerous to use this, as it may lead to corrupt data
9+
# (nothing is whitelisted based on type)
10+
# - Strongy encourage the use of strang parameters in conjunction with this
11+
# - the relationship type is always returned, because there is no way
12+
# to test if the relationsihp is polymorphic or not
13+
module Unrestricted
14+
module_function
15+
16+
def to_active_record_hash(document)
17+
primary_data = document['data']
18+
attributes = primary_data['attributes'] || {}
19+
attributes['id'] = primary_data['id'] if primary_data['id']
20+
relationships = primary_data['relationships'] || {}
21+
22+
hash = {}
23+
hash.merge!(parse_attributes(attributes))
24+
hash.merge!(parse_relationships(relationships))
25+
26+
hash.with_indifferent_access
27+
end
28+
29+
def parse_attributes(attributes)
30+
attributes
31+
# .map { |(k, v)| { k => v } }
32+
# .reduce({}, :merge)
33+
end
34+
35+
36+
# Given an association name, and a relationship data attribute, build a hash
37+
# mapping the corresponding ActiveRecord attribute to the corresponding value.
38+
#
39+
# @example
40+
# parse_relationship(:comments, [{ 'id' => '1', 'type' => 'comments' },
41+
# { 'id' => '2', 'type' => 'comments' }],
42+
# {})
43+
# # => { :comment_ids => ['1', '2'] }
44+
# parse_relationship(:author, { 'id' => '1', 'type' => 'users' }, {})
45+
# # => { :author_id => '1' }
46+
# parse_relationship(:author, nil, {})
47+
# # => { :author_id => nil }
48+
# @param [Symbol] assoc_name
49+
# @param [Hash] assoc_data
50+
# @param [Hash] options
51+
# @return [Hash{Symbol, Object}]
52+
#
53+
# @api private
54+
def parse_relationship(assoc_name, assoc_data)
55+
prefix_key = assoc_name.to_s.singularize
56+
hash =
57+
if assoc_data.is_a?(Array)
58+
{ "#{prefix_key}_ids".to_sym => assoc_data.map { |ri| ri['id'] } }
59+
else
60+
{ "#{prefix_key}_id".to_sym => assoc_data ? assoc_data['id'] : nil }
61+
end
62+
63+
unless assoc_data.is_a?(Array)
64+
hash["#{prefix_key}_type".to_sym] = assoc_data.present? ? assoc_data['type'] : nil
65+
end
66+
67+
hash
68+
end
69+
70+
# @api private
71+
def parse_relationships(relationships)
72+
relationships
73+
.map { |(k, v)| parse_relationship(k, v['data']) }
74+
.reduce({}, :merge)
75+
end
76+
end # Unrestricted
77+
end # DeserializableResource
78+
end # Rails
79+
end # JSONAPI

spec/integration/unrestricted_spec.rb

+113
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
# frozen_string_literal: true
2+
describe JSONAPI::Rails::DeserializableResource::Unrestricted do
3+
let(:klass) { JSONAPI::Rails::DeserializableResource::Unrestricted }
4+
5+
describe '.to_active_record_hash' do
6+
it 'uses unrestricted deserialization when a type is not found' do
7+
hash = {
8+
'data' => {
9+
'type' => 'restraints',
10+
'relationships' => {
11+
'restriction_for' => {
12+
'data' => {
13+
'type' => 'discounts',
14+
'id' => '67'
15+
}
16+
}
17+
}
18+
}
19+
}
20+
actual = JSONAPI::Rails.to_active_record_hash(hash)
21+
22+
expected = {
23+
'restriction_for_id' => '67',
24+
'restriction_for_type' => 'discounts'
25+
}
26+
expect(actual).to eq expected
27+
end
28+
29+
it 'deserializes just the relationships' do
30+
hash = {
31+
'data' => {
32+
'type' => 'restraints',
33+
'relationships' => {
34+
'restriction_for' => {
35+
'data' => {
36+
'type' => 'discounts',
37+
'id' => '67'
38+
}
39+
},
40+
'restricted_to' => {
41+
'data' => nil
42+
}
43+
}
44+
}
45+
}
46+
47+
48+
expected = {
49+
'restriction_for_id' => '67',
50+
'restriction_for_type' => 'discounts',
51+
'restricted_to_id' => nil,
52+
'restricted_to_type' => nil
53+
}
54+
actual = klass.to_active_record_hash(hash)
55+
expect(actual).to eq expected
56+
end
57+
58+
it 'deserializes attributes and relationships' do
59+
hash = {
60+
'data' => {
61+
'type' => 'photos',
62+
'id' => 'zorglub',
63+
'attributes' => {
64+
'title' => 'Ember Hamster',
65+
'src' => 'http://example.com/images/productivity.png',
66+
'image_width' => '200',
67+
'image_height' => '200',
68+
'image_size' => '1024'
69+
},
70+
'relationships' => {
71+
'author' => {
72+
'data' => nil
73+
},
74+
'photographer' => {
75+
'data' => { 'type' => 'people', 'id' => '9' }
76+
},
77+
'comments' => {
78+
'data' => [
79+
{ 'type' => 'comments', 'id' => '1' },
80+
{ 'type' => 'comments', 'id' => '2' }
81+
]
82+
},
83+
'related_images' => {
84+
'data' => [
85+
{ 'type' => 'image', 'id' => '7' },
86+
{ 'type' => 'image', 'id' => '8' }
87+
]
88+
}
89+
}
90+
}
91+
}
92+
93+
actual = klass.to_active_record_hash(hash)
94+
95+
expected = {
96+
'id' => 'zorglub',
97+
'title' => 'Ember Hamster',
98+
'src' => 'http://example.com/images/productivity.png',
99+
'image_width' => '200',
100+
'image_height' => '200',
101+
'image_size' => '1024',
102+
'author_id' => nil,
103+
'author_type' => nil,
104+
'photographer_id' => '9',
105+
'photographer_type' => 'people',
106+
'comment_ids' => %w(1 2),
107+
'related_image_ids' => %w(7 8)
108+
}
109+
110+
expect(actual).to eq expected
111+
end
112+
end
113+
end

0 commit comments

Comments
 (0)