jsonapi_compliable 0.5.7 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|