jsonapi_compliable 0.5.7 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/gemfiles/rails_4.gemfile.lock +3 -3
- data/gemfiles/rails_5.gemfile.lock +3 -3
- data/lib/jsonapi_compliable.rb +5 -2
- data/lib/jsonapi_compliable/adapters/abstract.rb +4 -0
- data/lib/jsonapi_compliable/adapters/active_record.rb +33 -0
- data/lib/jsonapi_compliable/adapters/active_record_sideloading.rb +2 -2
- data/lib/jsonapi_compliable/adapters/null.rb +4 -0
- data/lib/jsonapi_compliable/base.rb +26 -29
- data/lib/jsonapi_compliable/deserializer.rb +139 -0
- data/lib/jsonapi_compliable/resource.rb +33 -0
- data/lib/jsonapi_compliable/sideload.rb +18 -4
- data/lib/jsonapi_compliable/util/hash.rb +5 -0
- data/lib/jsonapi_compliable/util/persistence.rb +114 -0
- data/lib/jsonapi_compliable/util/relationship_payload.rb +55 -0
- data/lib/jsonapi_compliable/util/validation_response.rb +20 -0
- data/lib/jsonapi_compliable/version.rb +1 -1
- data/lib/jsonapi_compliable/write.rb +93 -0
- metadata +7 -3
- data/lib/jsonapi_compliable/deserializable.rb +0 -127
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2f26c6ffeb63e0bd72ef36d3b2c86c6931a8f4fa
|
4
|
+
data.tar.gz: ae546cd488fe425c049efac300602efb8234ddce
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 595f742f0a782fddc1e161981d359fb9bce7976b014e556c8ce84f3f2c0365b0b035ff2ebb22a247b9776cb05b33e97bab180b613ed8167995004b3e258084a8
|
7
|
+
data.tar.gz: 5dea3994b495f6b1b455f23a10bf9169630b54ef0886082a0e99888cdf665f7dd621294dc6776a43c0e54c241f9fd983070ff432dd188f49a45d75cab745e1f4
|
data/lib/jsonapi_compliable.rb
CHANGED
@@ -4,6 +4,7 @@ require "jsonapi_compliable/resource"
|
|
4
4
|
require "jsonapi_compliable/query"
|
5
5
|
require "jsonapi_compliable/sideload"
|
6
6
|
require "jsonapi_compliable/scope"
|
7
|
+
require "jsonapi_compliable/deserializer"
|
7
8
|
require "jsonapi_compliable/scoping/base"
|
8
9
|
require "jsonapi_compliable/scoping/sort"
|
9
10
|
require "jsonapi_compliable/scoping/paginate"
|
@@ -18,6 +19,9 @@ require "jsonapi_compliable/stats/payload"
|
|
18
19
|
require "jsonapi_compliable/util/include_params"
|
19
20
|
require "jsonapi_compliable/util/field_params"
|
20
21
|
require "jsonapi_compliable/util/hash"
|
22
|
+
require "jsonapi_compliable/util/relationship_payload"
|
23
|
+
require "jsonapi_compliable/util/persistence"
|
24
|
+
require "jsonapi_compliable/util/validation_response"
|
21
25
|
|
22
26
|
# require correct jsonapi-rb before extensions
|
23
27
|
if defined?(Rails)
|
@@ -30,8 +34,7 @@ require "jsonapi_compliable/extensions/extra_attribute"
|
|
30
34
|
require "jsonapi_compliable/extensions/boolean_attribute"
|
31
35
|
|
32
36
|
module JsonapiCompliable
|
33
|
-
autoload :Base,
|
34
|
-
autoload :Deserializable, 'jsonapi_compliable/deserializable'
|
37
|
+
autoload :Base, 'jsonapi_compliable/base'
|
35
38
|
|
36
39
|
def self.included(klass)
|
37
40
|
klass.instance_eval do
|
@@ -39,9 +39,42 @@ module JsonapiCompliable
|
|
39
39
|
scope.to_a
|
40
40
|
end
|
41
41
|
|
42
|
+
def transaction(model_class)
|
43
|
+
model_class.transaction do
|
44
|
+
yield
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
42
48
|
def sideloading_module
|
43
49
|
JsonapiCompliable::Adapters::ActiveRecordSideloading
|
44
50
|
end
|
51
|
+
|
52
|
+
def associate(parent, child, association_name, association_type)
|
53
|
+
if association_type == :has_many
|
54
|
+
parent.association(association_name).loaded!
|
55
|
+
parent.association(association_name).add_to_target(child, :skip_callbacks)
|
56
|
+
else
|
57
|
+
child.send("#{association_name}=", parent)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def create(model_class, create_params)
|
62
|
+
instance = model_class.new(create_params)
|
63
|
+
instance.save
|
64
|
+
instance
|
65
|
+
end
|
66
|
+
|
67
|
+
def update(model_class, update_params)
|
68
|
+
instance = model_class.find(update_params.delete(:id))
|
69
|
+
instance.update_attributes(update_params)
|
70
|
+
instance
|
71
|
+
end
|
72
|
+
|
73
|
+
def destroy(model_class, id)
|
74
|
+
instance = model_class.find(id)
|
75
|
+
instance.destroy
|
76
|
+
instance
|
77
|
+
end
|
45
78
|
end
|
46
79
|
end
|
47
80
|
end
|
@@ -4,7 +4,7 @@ module JsonapiCompliable
|
|
4
4
|
def has_many(association_name, scope: nil, resource:, foreign_key:, primary_key: :id, &blk)
|
5
5
|
_scope = scope
|
6
6
|
|
7
|
-
allow_sideload association_name, resource: resource do
|
7
|
+
allow_sideload association_name, type: :has_many, resource: resource, foreign_key: foreign_key, primary_key: primary_key do
|
8
8
|
scope do |parents|
|
9
9
|
parent_ids = parents.map { |p| p.send(primary_key) }
|
10
10
|
_scope.call.where(foreign_key => parent_ids.uniq.compact)
|
@@ -27,7 +27,7 @@ module JsonapiCompliable
|
|
27
27
|
def belongs_to(association_name, scope: nil, resource:, foreign_key:, primary_key: :id, &blk)
|
28
28
|
_scope = scope
|
29
29
|
|
30
|
-
allow_sideload association_name, resource: resource do
|
30
|
+
allow_sideload association_name, type: :belongs_to, resource: resource, foreign_key: foreign_key, primary_key: primary_key do
|
31
31
|
scope do |parents|
|
32
32
|
parent_ids = parents.map { |p| p.send(foreign_key) }
|
33
33
|
_scope.call.where(primary_key => parent_ids.uniq.compact)
|
@@ -1,7 +1,6 @@
|
|
1
1
|
module JsonapiCompliable
|
2
2
|
module Base
|
3
3
|
extend ActiveSupport::Concern
|
4
|
-
include Deserializable
|
5
4
|
|
6
5
|
MAX_PAGE_SIZE = 1_000
|
7
6
|
|
@@ -32,7 +31,6 @@ module JsonapiCompliable
|
|
32
31
|
@query_hash ||= query.to_hash[resource.type]
|
33
32
|
end
|
34
33
|
|
35
|
-
# TODO pass controller and action name here to guard
|
36
34
|
def wrap_context
|
37
35
|
if self.class._jsonapi_compliable
|
38
36
|
resource.with_context(self, action_name.to_sym) do
|
@@ -45,6 +43,30 @@ module JsonapiCompliable
|
|
45
43
|
resource.build_scope(scope, query, opts)
|
46
44
|
end
|
47
45
|
|
46
|
+
def deserialized_params
|
47
|
+
@deserialized_params ||= JsonapiCompliable::Deserializer.new(params, request.env)
|
48
|
+
end
|
49
|
+
|
50
|
+
def jsonapi_create
|
51
|
+
created = resource.transaction do
|
52
|
+
resource.persist_with_relationships \
|
53
|
+
deserialized_params.meta,
|
54
|
+
deserialized_params.attributes,
|
55
|
+
deserialized_params.relationships
|
56
|
+
end
|
57
|
+
Util::ValidationResponse.new(created, deserialized_params)
|
58
|
+
end
|
59
|
+
|
60
|
+
def jsonapi_update
|
61
|
+
updated = resource.transaction do
|
62
|
+
resource.persist_with_relationships \
|
63
|
+
deserialized_params.meta,
|
64
|
+
deserialized_params.attributes,
|
65
|
+
deserialized_params.relationships
|
66
|
+
end
|
67
|
+
Util::ValidationResponse.new(updated, deserialized_params)
|
68
|
+
end
|
69
|
+
|
48
70
|
def perform_render_jsonapi(opts)
|
49
71
|
JSONAPI::Serializable::Renderer.render(opts.delete(:jsonapi), opts)
|
50
72
|
end
|
@@ -54,7 +76,7 @@ module JsonapiCompliable
|
|
54
76
|
opts = default_jsonapi_render_options.merge(opts)
|
55
77
|
opts = Util::RenderOptions.generate(scope, query_hash, opts)
|
56
78
|
opts[:expose][:context] = self
|
57
|
-
opts[:include] =
|
79
|
+
opts[:include] = deserialized_params.include_directive if force_includes?
|
58
80
|
perform_render_jsonapi(opts)
|
59
81
|
end
|
60
82
|
|
@@ -65,33 +87,8 @@ module JsonapiCompliable
|
|
65
87
|
end
|
66
88
|
end
|
67
89
|
|
68
|
-
# Legacy
|
69
|
-
# TODO: This nastiness likely goes away once jsonapi standardizes
|
70
|
-
# a spec for nested relationships.
|
71
|
-
# See: https://github.com/json-api/json-api/issues/1089
|
72
|
-
def forced_includes(data = nil)
|
73
|
-
data = raw_params[:data] unless data
|
74
|
-
|
75
|
-
{}.tap do |forced|
|
76
|
-
(data[:relationships] || {}).each_pair do |relation_name, relation|
|
77
|
-
if relation[:data].is_a?(Array)
|
78
|
-
forced[relation_name] = {}
|
79
|
-
relation[:data].each do |datum|
|
80
|
-
forced[relation_name].deep_merge!(forced_includes(datum))
|
81
|
-
end
|
82
|
-
else
|
83
|
-
forced[relation_name] = forced_includes(relation[:data])
|
84
|
-
end
|
85
|
-
end
|
86
|
-
end
|
87
|
-
end
|
88
|
-
|
89
|
-
# Legacy
|
90
90
|
def force_includes?
|
91
|
-
|
92
|
-
|
93
|
-
%w(PUT PATCH POST).include?(request.method) and
|
94
|
-
raw_params.try(:[], :data).try(:[], :relationships).present?
|
91
|
+
not params[:data].nil?
|
95
92
|
end
|
96
93
|
|
97
94
|
module ClassMethods
|
@@ -0,0 +1,139 @@
|
|
1
|
+
class JsonapiCompliable::Deserializer
|
2
|
+
def initialize(payload, env)
|
3
|
+
@payload = payload
|
4
|
+
@env = env
|
5
|
+
end
|
6
|
+
|
7
|
+
def data
|
8
|
+
@payload[:data]
|
9
|
+
end
|
10
|
+
|
11
|
+
def id
|
12
|
+
data[:id]
|
13
|
+
end
|
14
|
+
|
15
|
+
def attributes
|
16
|
+
@attributes ||= raw_attributes.tap do |hash|
|
17
|
+
hash.merge!(id: id) if id
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def attributes=(attrs)
|
22
|
+
@attributes = attrs
|
23
|
+
end
|
24
|
+
|
25
|
+
def method
|
26
|
+
case @env['REQUEST_METHOD']
|
27
|
+
when 'POST' then :create
|
28
|
+
when 'PUT', 'PATCH' then :update
|
29
|
+
when 'DELETE' then :destroy
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def meta
|
34
|
+
{
|
35
|
+
type: data[:type],
|
36
|
+
temp_id: data[:'temp-id'],
|
37
|
+
method: method
|
38
|
+
}
|
39
|
+
end
|
40
|
+
|
41
|
+
def relationships
|
42
|
+
@relationships ||= process_relationships(raw_relationships)
|
43
|
+
end
|
44
|
+
|
45
|
+
def included
|
46
|
+
@payload[:included] || []
|
47
|
+
end
|
48
|
+
|
49
|
+
def include_directive(memo = {}, relationship_node = nil)
|
50
|
+
relationship_node ||= relationships
|
51
|
+
|
52
|
+
relationship_node.each_pair do |name, relationship_payload|
|
53
|
+
arrayified = [relationship_payload].flatten
|
54
|
+
next if arrayified.all? { |rp| removed?(rp) }
|
55
|
+
|
56
|
+
memo[name] ||= {}
|
57
|
+
deep_merge!(memo[name], sub_directives(memo[name], arrayified))
|
58
|
+
end
|
59
|
+
|
60
|
+
memo
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def removed?(relationship_payload)
|
66
|
+
method = relationship_payload[:meta][:method]
|
67
|
+
[:disassociate, :destroy].include?(method)
|
68
|
+
end
|
69
|
+
|
70
|
+
def sub_directives(memo, relationship_payloads)
|
71
|
+
{}.tap do |subs|
|
72
|
+
relationship_payloads.each do |rp|
|
73
|
+
sub_directive = include_directive(memo, rp[:relationships])
|
74
|
+
deep_merge!(subs, sub_directive)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def deep_merge!(a, b)
|
80
|
+
JsonapiCompliable::Util::Hash.deep_merge!(a, b)
|
81
|
+
end
|
82
|
+
|
83
|
+
def process_relationships(relationship_hash)
|
84
|
+
{}.tap do |hash|
|
85
|
+
relationship_hash.each_pair do |name, relationship_payload|
|
86
|
+
name = name.to_sym
|
87
|
+
|
88
|
+
if relationship_payload[:data]
|
89
|
+
hash[name] = process_relationship(relationship_payload[:data])
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def process_relationship(relationship_data)
|
96
|
+
if relationship_data.is_a?(Array)
|
97
|
+
relationship_data.map do |rd|
|
98
|
+
process_relationship_datum(rd)
|
99
|
+
end
|
100
|
+
else
|
101
|
+
process_relationship_datum(relationship_data)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def process_relationship_datum(datum)
|
106
|
+
temp_id = datum[:'temp-id']
|
107
|
+
included_object = included.find do |i|
|
108
|
+
next unless i[:type] == datum[:type]
|
109
|
+
|
110
|
+
(i[:id] && i[:id] == datum[:id]) ||
|
111
|
+
(i[:'temp-id'] && i[:'temp-id'] == temp_id)
|
112
|
+
end
|
113
|
+
included_object ||= {}
|
114
|
+
included_object[:relationships] ||= {}
|
115
|
+
|
116
|
+
attributes = included_object[:attributes] || {}
|
117
|
+
attributes[:id] = datum[:id] if datum[:id]
|
118
|
+
relationships = process_relationships(included_object[:relationships] || {})
|
119
|
+
|
120
|
+
|
121
|
+
{
|
122
|
+
meta: {
|
123
|
+
jsonapi_type: datum[:type],
|
124
|
+
temp_id: temp_id,
|
125
|
+
method: datum[:method].try(:to_sym)
|
126
|
+
},
|
127
|
+
attributes: attributes,
|
128
|
+
relationships: relationships
|
129
|
+
}
|
130
|
+
end
|
131
|
+
|
132
|
+
def raw_attributes
|
133
|
+
data[:attributes] || {}
|
134
|
+
end
|
135
|
+
|
136
|
+
def raw_relationships
|
137
|
+
data[:relationships] || {}
|
138
|
+
end
|
139
|
+
end
|
@@ -50,6 +50,10 @@ module JsonapiCompliable
|
|
50
50
|
}
|
51
51
|
end
|
52
52
|
|
53
|
+
def self.model(klass)
|
54
|
+
config[:model] = klass
|
55
|
+
end
|
56
|
+
|
53
57
|
def self.sort(&blk)
|
54
58
|
config[:sorting] = blk
|
55
59
|
end
|
@@ -92,6 +96,7 @@ module JsonapiCompliable
|
|
92
96
|
stats: {},
|
93
97
|
sorting: nil,
|
94
98
|
pagination: nil,
|
99
|
+
model: nil,
|
95
100
|
adapter: Adapters::Abstract.new
|
96
101
|
}
|
97
102
|
end
|
@@ -115,6 +120,24 @@ module JsonapiCompliable
|
|
115
120
|
Scope.new(base, self, query, opts)
|
116
121
|
end
|
117
122
|
|
123
|
+
def create(create_params)
|
124
|
+
adapter.create(model, create_params)
|
125
|
+
end
|
126
|
+
|
127
|
+
def update(update_params)
|
128
|
+
adapter.update(model, update_params)
|
129
|
+
end
|
130
|
+
|
131
|
+
def destroy(id)
|
132
|
+
adapter.destroy(model, id)
|
133
|
+
end
|
134
|
+
|
135
|
+
def persist_with_relationships(meta, attributes, relationships)
|
136
|
+
persistence = JsonapiCompliable::Util::Persistence \
|
137
|
+
.new(self, meta, attributes, relationships)
|
138
|
+
persistence.run
|
139
|
+
end
|
140
|
+
|
118
141
|
def association_names
|
119
142
|
@association_names ||= begin
|
120
143
|
if sideloading
|
@@ -190,6 +213,10 @@ module JsonapiCompliable
|
|
190
213
|
self.class.config[:default_filters]
|
191
214
|
end
|
192
215
|
|
216
|
+
def model
|
217
|
+
self.class.config[:model]
|
218
|
+
end
|
219
|
+
|
193
220
|
def adapter
|
194
221
|
self.class.config[:adapter]
|
195
222
|
end
|
@@ -197,5 +224,11 @@ module JsonapiCompliable
|
|
197
224
|
def resolve(scope)
|
198
225
|
adapter.resolve(scope)
|
199
226
|
end
|
227
|
+
|
228
|
+
def transaction
|
229
|
+
adapter.transaction(model) do
|
230
|
+
yield
|
231
|
+
end
|
232
|
+
end
|
200
233
|
end
|
201
234
|
end
|
@@ -6,18 +6,28 @@ module JsonapiCompliable
|
|
6
6
|
:sideloads,
|
7
7
|
:scope_proc,
|
8
8
|
:assign_proc,
|
9
|
-
:grouper
|
9
|
+
:grouper,
|
10
|
+
:foreign_key,
|
11
|
+
:primary_key,
|
12
|
+
:type
|
10
13
|
|
11
|
-
def initialize(name,
|
14
|
+
def initialize(name, type: nil, resource: nil, polymorphic: false, primary_key: :id, foreign_key: nil)
|
12
15
|
@name = name
|
13
|
-
@resource_class = (
|
16
|
+
@resource_class = (resource || Class.new(Resource))
|
14
17
|
@sideloads = {}
|
15
|
-
@polymorphic = !!
|
18
|
+
@polymorphic = !!polymorphic
|
16
19
|
@polymorphic_groups = {} if polymorphic?
|
20
|
+
@primary_key = primary_key
|
21
|
+
@foreign_key = foreign_key
|
22
|
+
@type = type
|
17
23
|
|
18
24
|
extend @resource_class.config[:adapter].sideloading_module
|
19
25
|
end
|
20
26
|
|
27
|
+
def resource
|
28
|
+
@resource ||= resource_class.new
|
29
|
+
end
|
30
|
+
|
21
31
|
def polymorphic?
|
22
32
|
@polymorphic == true
|
23
33
|
end
|
@@ -30,6 +40,10 @@ module JsonapiCompliable
|
|
30
40
|
@assign_proc = blk
|
31
41
|
end
|
32
42
|
|
43
|
+
def associate(parent, child)
|
44
|
+
resource_class.config[:adapter].associate(parent, child, name, type)
|
45
|
+
end
|
46
|
+
|
33
47
|
def group_by(&grouper)
|
34
48
|
@grouper = grouper
|
35
49
|
end
|
@@ -10,6 +10,11 @@ module JsonapiCompliable
|
|
10
10
|
collection
|
11
11
|
end
|
12
12
|
|
13
|
+
def self.deep_merge!(hash, other)
|
14
|
+
merger = proc { |key, v1, v2| Hash === v1 && Hash === v2 ? v1.merge(v2, &merger) : v2 }
|
15
|
+
hash.merge!(other, &merger)
|
16
|
+
end
|
17
|
+
|
13
18
|
def self.deep_dup(hash)
|
14
19
|
if hash.respond_to?(:deep_dup)
|
15
20
|
hash.deep_dup
|
@@ -0,0 +1,114 @@
|
|
1
|
+
class JsonapiCompliable::Util::Persistence
|
2
|
+
def initialize(resource, meta, attributes, relationships)
|
3
|
+
@resource = resource
|
4
|
+
@meta = meta
|
5
|
+
@attributes = attributes
|
6
|
+
@relationships = relationships
|
7
|
+
end
|
8
|
+
|
9
|
+
# belongs_to must be processed before/separately from has_many -
|
10
|
+
# we need to know the primary key value of the parent before
|
11
|
+
# persisting the child
|
12
|
+
#
|
13
|
+
# Flow:
|
14
|
+
# * process parents
|
15
|
+
# * update attributes to reflect parent primary keys
|
16
|
+
# * persist current object
|
17
|
+
# * associate temp id with current object
|
18
|
+
# * associate parent objects with current object
|
19
|
+
# * process children
|
20
|
+
# * associate children
|
21
|
+
# * return current object
|
22
|
+
def run
|
23
|
+
parents = process_belongs_to(@relationships)
|
24
|
+
update_foreign_key_for_parents(parents)
|
25
|
+
|
26
|
+
persisted = persist_object(@meta[:method], @attributes)
|
27
|
+
assign_temp_id(persisted, @meta[:temp_id])
|
28
|
+
associate_parents(persisted, parents)
|
29
|
+
|
30
|
+
children = process_has_many(@relationships) do |x|
|
31
|
+
update_foreign_key(persisted, x[:attributes], x)
|
32
|
+
end
|
33
|
+
|
34
|
+
associate_children(persisted, children)
|
35
|
+
persisted unless @meta[:method] == :destroy
|
36
|
+
end
|
37
|
+
|
38
|
+
# The child's attributes should be modified to nil-out the
|
39
|
+
# foreign_key when the parent is being destroyed or disassociated
|
40
|
+
def update_foreign_key(parent_object, attrs, x)
|
41
|
+
if [:destroy, :disassociate].include?(x[:meta][:method])
|
42
|
+
attrs[x[:foreign_key]] = nil
|
43
|
+
else
|
44
|
+
attrs[x[:foreign_key]] = parent_object.send(x[:primary_key])
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def update_foreign_key_for_parents(parents)
|
49
|
+
parents.each do |x|
|
50
|
+
update_foreign_key(x[:object], @attributes, x)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def associate_parents(object, parents)
|
55
|
+
parents.each do |x|
|
56
|
+
x[:sideload].associate(x[:object], object) if x[:object] && object
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def associate_children(object, children)
|
61
|
+
children.each do |x|
|
62
|
+
x[:sideload].associate(object, x[:object]) if x[:object] && object
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def persist_object(method, attributes)
|
67
|
+
case method
|
68
|
+
when :destroy
|
69
|
+
@resource.destroy(attributes[:id])
|
70
|
+
when :disassociate, nil
|
71
|
+
@resource.update(attributes)
|
72
|
+
else
|
73
|
+
@resource.send(method, attributes)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def process_has_many(relationships)
|
78
|
+
[].tap do |processed|
|
79
|
+
iterate(except: [:belongs_to]) do |x|
|
80
|
+
yield x
|
81
|
+
x[:object] = x[:sideload].resource
|
82
|
+
.persist_with_relationships(x[:meta], x[:attributes], x[:relationships])
|
83
|
+
processed << x
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def process_belongs_to(relationships)
|
89
|
+
[].tap do |processed|
|
90
|
+
iterate(only: [:belongs_to]) do |x|
|
91
|
+
x[:object] = x[:sideload].resource
|
92
|
+
.persist_with_relationships(x[:meta], x[:attributes], x[:relationships])
|
93
|
+
processed << x
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def assign_temp_id(object, temp_id)
|
99
|
+
object.instance_variable_set(:@_jsonapi_temp_id, temp_id)
|
100
|
+
end
|
101
|
+
|
102
|
+
private
|
103
|
+
|
104
|
+
def iterate(only: [], except: [])
|
105
|
+
opts = {
|
106
|
+
resource: @resource,
|
107
|
+
relationships: @relationships,
|
108
|
+
}.merge(only: only, except: except)
|
109
|
+
|
110
|
+
JsonapiCompliable::Util::RelationshipPayload.iterate(opts) do |x|
|
111
|
+
yield x
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module JsonapiCompliable
|
2
|
+
module Util
|
3
|
+
class RelationshipPayload
|
4
|
+
attr_reader :resource, :payload
|
5
|
+
|
6
|
+
def self.iterate(resource:, relationships: {}, only: [], except: [])
|
7
|
+
instance = new(resource, relationships, only: only, except: except)
|
8
|
+
instance.iterate do |sideload, relationship_data, sub_relationships|
|
9
|
+
yield sideload, relationship_data, sub_relationships
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(resource, payload, only: [], except: [])
|
14
|
+
@resource = resource
|
15
|
+
@payload = payload
|
16
|
+
@only = only
|
17
|
+
@except = except
|
18
|
+
end
|
19
|
+
|
20
|
+
def iterate
|
21
|
+
payload.each_pair do |relationship_name, relationship_payload|
|
22
|
+
if sl = resource.sideload(relationship_name.to_sym)
|
23
|
+
if should_yield?(sl.type)
|
24
|
+
if relationship_payload.is_a?(Array)
|
25
|
+
relationship_payload.each do |rp|
|
26
|
+
yield payload_for(sl, rp)
|
27
|
+
end
|
28
|
+
else
|
29
|
+
yield payload_for(sl, relationship_payload)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def should_yield?(type)
|
39
|
+
(@only.length > 0 && @only.include?(type)) ||
|
40
|
+
(@except.length > 0 && !@except.include?(type))
|
41
|
+
end
|
42
|
+
|
43
|
+
def payload_for(sideload, relationship_payload)
|
44
|
+
{
|
45
|
+
sideload: sideload,
|
46
|
+
primary_key: sideload.primary_key,
|
47
|
+
foreign_key: sideload.foreign_key,
|
48
|
+
attributes: relationship_payload[:attributes],
|
49
|
+
meta: relationship_payload[:meta],
|
50
|
+
relationships: relationship_payload[:relationships]
|
51
|
+
}
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
class JsonapiCompliable::Util::ValidationResponse
|
2
|
+
attr_reader :object
|
3
|
+
|
4
|
+
def initialize(object, deserialized_params)
|
5
|
+
@object = object
|
6
|
+
@deserialized_params = deserialized_params
|
7
|
+
end
|
8
|
+
|
9
|
+
def success?
|
10
|
+
if object.respond_to?(:errors)
|
11
|
+
object.errors.blank?
|
12
|
+
else
|
13
|
+
true
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_a
|
18
|
+
[object, success?]
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
class JsonapiCompliable::Write
|
2
|
+
attr_reader :resource
|
3
|
+
|
4
|
+
def initialize(opts = {})
|
5
|
+
@resource = opts[:resource]
|
6
|
+
end
|
7
|
+
|
8
|
+
def persist
|
9
|
+
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
resource.persist(obj, persistence_query, opts)
|
14
|
+
|
15
|
+
p = Persistence.new(obj, resource: resource, params: params, opts: opts)
|
16
|
+
p.persist
|
17
|
+
|
18
|
+
|
19
|
+
allow_nested_write :books, resource: BookResource do
|
20
|
+
# rdefault to using BookResource in these
|
21
|
+
# hooks, but user can override to save
|
22
|
+
# via author
|
23
|
+
#
|
24
|
+
# assign temp id AFTER
|
25
|
+
# must handle NESTING genre
|
26
|
+
create do |author, book_params|
|
27
|
+
book_params[:author_id] = author.id
|
28
|
+
BookResource.create(book_params)
|
29
|
+
#book = Book.new(book_params)
|
30
|
+
#book.author_id = author.id
|
31
|
+
# instance variable set new id?
|
32
|
+
# # should be outside this method
|
33
|
+
#book.save!
|
34
|
+
#book
|
35
|
+
end
|
36
|
+
|
37
|
+
# HOW TO REUSE THIS ON /books
|
38
|
+
# should it be? i guess update, but not create
|
39
|
+
update do |author, book_params|
|
40
|
+
BookResource.update(book_params)
|
41
|
+
#book = Book.find(book_params[:id])
|
42
|
+
#book.update_attributes(book_params)
|
43
|
+
#book
|
44
|
+
end
|
45
|
+
|
46
|
+
# Might be fair to say you can remove rel
|
47
|
+
# but not destroy it
|
48
|
+
# if needed, add destroy hook later
|
49
|
+
delete do |author, book_params|
|
50
|
+
book_params[:author_id] = nil
|
51
|
+
BookResource.update(book_params)
|
52
|
+
end
|
53
|
+
|
54
|
+
destroy do |author, book_params|
|
55
|
+
BookResource.destroy(book_params)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
#def jsonapi_scope(scope, opts = {})
|
60
|
+
#resource.build_scope(scope, query, opts)
|
61
|
+
#end
|
62
|
+
|
63
|
+
# controller
|
64
|
+
#
|
65
|
+
# book = Book.new(params)
|
66
|
+
#
|
67
|
+
# if jsonapi_persist(book)
|
68
|
+
# render_jsonapi(book)
|
69
|
+
# else
|
70
|
+
# render_errors_for(book)
|
71
|
+
# end
|
72
|
+
|
73
|
+
|
74
|
+
|
75
|
+
class Writer
|
76
|
+
def initialize(parent)
|
77
|
+
end
|
78
|
+
|
79
|
+
def doit
|
80
|
+
relationships.each do |name, data|
|
81
|
+
if has_many?
|
82
|
+
created = Child.new(data, fk: fk)
|
83
|
+
created.TEMP_ID = data['temp_id']
|
84
|
+
created = created.save
|
85
|
+
mapping {data['temp_id'] => created.id}
|
86
|
+
parent.name 4<< created
|
87
|
+
parent.
|
88
|
+
else
|
89
|
+
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: jsonapi_compliable
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.6.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Lee Richmond
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: exe
|
11
11
|
cert_chain: []
|
12
|
-
date: 2017-
|
12
|
+
date: 2017-04-09 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: jsonapi-serializable
|
@@ -149,7 +149,7 @@ files:
|
|
149
149
|
- lib/jsonapi_compliable/adapters/active_record_sideloading.rb
|
150
150
|
- lib/jsonapi_compliable/adapters/null.rb
|
151
151
|
- lib/jsonapi_compliable/base.rb
|
152
|
-
- lib/jsonapi_compliable/
|
152
|
+
- lib/jsonapi_compliable/deserializer.rb
|
153
153
|
- lib/jsonapi_compliable/errors.rb
|
154
154
|
- lib/jsonapi_compliable/extensions/boolean_attribute.rb
|
155
155
|
- lib/jsonapi_compliable/extensions/extra_attribute.rb
|
@@ -170,8 +170,12 @@ files:
|
|
170
170
|
- lib/jsonapi_compliable/util/field_params.rb
|
171
171
|
- lib/jsonapi_compliable/util/hash.rb
|
172
172
|
- lib/jsonapi_compliable/util/include_params.rb
|
173
|
+
- lib/jsonapi_compliable/util/persistence.rb
|
174
|
+
- lib/jsonapi_compliable/util/relationship_payload.rb
|
173
175
|
- lib/jsonapi_compliable/util/render_options.rb
|
176
|
+
- lib/jsonapi_compliable/util/validation_response.rb
|
174
177
|
- lib/jsonapi_compliable/version.rb
|
178
|
+
- lib/jsonapi_compliable/write.rb
|
175
179
|
homepage:
|
176
180
|
licenses:
|
177
181
|
- MIT
|
@@ -1,127 +0,0 @@
|
|
1
|
-
# This will convert JSONAPI-compatible POST/PUT payloads
|
2
|
-
# into something Rails better understands. Example:
|
3
|
-
#
|
4
|
-
# {
|
5
|
-
# "data": {
|
6
|
-
# "type": "articles",
|
7
|
-
# "attributes": { "title": "the first article" },
|
8
|
-
# "relationships": {
|
9
|
-
# "tags": {
|
10
|
-
# "data": [{
|
11
|
-
# "type": "tags",
|
12
|
-
# "attributes": { "name": "One" }
|
13
|
-
# }, {
|
14
|
-
# "type": "tags",
|
15
|
-
# "attributes": { "name": "Two" }
|
16
|
-
# }]
|
17
|
-
# }
|
18
|
-
# }
|
19
|
-
# }
|
20
|
-
# }
|
21
|
-
#
|
22
|
-
# Into:
|
23
|
-
#
|
24
|
-
# {
|
25
|
-
# article: {
|
26
|
-
# title: 'the first article',
|
27
|
-
# tags_attributes: [
|
28
|
-
# { name: 'One' },
|
29
|
-
# { name: 'Two' },
|
30
|
-
# ]
|
31
|
-
# }
|
32
|
-
# }
|
33
|
-
#
|
34
|
-
# Why we don't use AMS deserialization - AMS will:
|
35
|
-
# * not support relationship data
|
36
|
-
# * override foreign key incorrectly, ie
|
37
|
-
# post_id incorrectly becomes nil if post relation is nil,
|
38
|
-
# even if it is in the attributes payload
|
39
|
-
#
|
40
|
-
# Usage:
|
41
|
-
#
|
42
|
-
# In controller:
|
43
|
-
#
|
44
|
-
# before_action :deserialize_jsonapi!, only: [:my_action]
|
45
|
-
|
46
|
-
module JsonapiCompliable
|
47
|
-
module Deserializable
|
48
|
-
extend ActiveSupport::Concern
|
49
|
-
|
50
|
-
included do
|
51
|
-
attr_accessor :raw_params
|
52
|
-
end
|
53
|
-
|
54
|
-
class Deserialization
|
55
|
-
def initialize(params, namespace: true)
|
56
|
-
@params = params
|
57
|
-
@namespace = namespace
|
58
|
-
end
|
59
|
-
|
60
|
-
def deserialize
|
61
|
-
hash = attributes
|
62
|
-
hash = hash.merge(relationships)
|
63
|
-
hash = @namespace ? { parsed_type => hash } : hash
|
64
|
-
hash.reverse_merge(@params.except(:data)).deep_symbolize_keys
|
65
|
-
end
|
66
|
-
|
67
|
-
private
|
68
|
-
|
69
|
-
def parsed_type
|
70
|
-
@params[:data][:type].underscore.singularize.to_sym
|
71
|
-
end
|
72
|
-
|
73
|
-
def attributes
|
74
|
-
attrs = {}
|
75
|
-
attrs[:id] = @params[:data].try(:[], :id) if @params[:data].try(:[], :id)
|
76
|
-
attrs.merge!(@params[:data].try(:[], :attributes) || {})
|
77
|
-
attrs
|
78
|
-
end
|
79
|
-
|
80
|
-
def relationships
|
81
|
-
return {} if @params[:data].try(:[], :relationships).blank?
|
82
|
-
|
83
|
-
{}.tap do |hash|
|
84
|
-
@params[:data][:relationships].each_pair do |relationship_name, payload|
|
85
|
-
parsed_relation = parse_relation(payload)
|
86
|
-
|
87
|
-
if parsed_relation.present?
|
88
|
-
hash["#{relationship_name}_attributes".to_sym] = parsed_relation
|
89
|
-
end
|
90
|
-
end
|
91
|
-
end
|
92
|
-
end
|
93
|
-
|
94
|
-
def parse_relation(payload)
|
95
|
-
if payload[:data].is_a?(Array)
|
96
|
-
parse_has_many(payload[:data])
|
97
|
-
else
|
98
|
-
parse_belongs_to(payload)
|
99
|
-
end
|
100
|
-
end
|
101
|
-
|
102
|
-
def parse_belongs_to(payload)
|
103
|
-
self.class.new(payload, namespace: false).deserialize
|
104
|
-
end
|
105
|
-
|
106
|
-
def parse_has_many(payloads)
|
107
|
-
payloads.map do |payload|
|
108
|
-
payload = { data: payload }
|
109
|
-
self.class.new(payload, namespace: false).deserialize
|
110
|
-
end.compact
|
111
|
-
end
|
112
|
-
end
|
113
|
-
|
114
|
-
def deserialize_jsonapi!
|
115
|
-
self.raw_params = Util::Hash.deep_dup(self.params)
|
116
|
-
|
117
|
-
if defined?(::Rails) && (is_a?(ActionController::Base) || (defined?(ActionController::API) && is_a?(ActionController::API)))
|
118
|
-
hash = params.to_unsafe_h
|
119
|
-
hash = hash.with_indifferent_access if ::Rails::VERSION::MAJOR == 4
|
120
|
-
deserialized = Deserialization.new(hash).deserialize
|
121
|
-
self.params = ActionController::Parameters.new(deserialized)
|
122
|
-
else
|
123
|
-
self.params = Deserialization.new(params).deserialize
|
124
|
-
end
|
125
|
-
end
|
126
|
-
end
|
127
|
-
end
|