jsonapi_compliable 0.6.4 → 0.6.5
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/.gitignore +1 -1
- data/.travis.yml +11 -3
- data/.yardopts +1 -0
- data/README.md +10 -1
- data/Rakefile +1 -0
- data/docs/JsonapiCompliable.html +202 -0
- data/docs/JsonapiCompliable/Adapters.html +119 -0
- data/docs/JsonapiCompliable/Adapters/Abstract.html +2285 -0
- data/docs/JsonapiCompliable/Adapters/ActiveRecord.html +2151 -0
- data/docs/JsonapiCompliable/Adapters/ActiveRecordSideloading.html +582 -0
- data/docs/JsonapiCompliable/Adapters/Null.html +1682 -0
- data/docs/JsonapiCompliable/Base.html +1395 -0
- data/docs/JsonapiCompliable/Deserializer.html +835 -0
- data/docs/JsonapiCompliable/Errors.html +115 -0
- data/docs/JsonapiCompliable/Errors/BadFilter.html +124 -0
- data/docs/JsonapiCompliable/Errors/StatNotFound.html +266 -0
- data/docs/JsonapiCompliable/Errors/UnsupportedPageSize.html +264 -0
- data/docs/JsonapiCompliable/Errors/ValidationError.html +124 -0
- data/docs/JsonapiCompliable/Extensions.html +117 -0
- data/docs/JsonapiCompliable/Extensions/BooleanAttribute.html +212 -0
- data/docs/JsonapiCompliable/Extensions/BooleanAttribute/ClassMethods.html +229 -0
- data/docs/JsonapiCompliable/Extensions/ExtraAttribute.html +242 -0
- data/docs/JsonapiCompliable/Extensions/ExtraAttribute/ClassMethods.html +237 -0
- data/docs/JsonapiCompliable/Query.html +1099 -0
- data/docs/JsonapiCompliable/Rails.html +211 -0
- data/docs/JsonapiCompliable/Resource.html +5241 -0
- data/docs/JsonapiCompliable/Scope.html +703 -0
- data/docs/JsonapiCompliable/Scoping.html +117 -0
- data/docs/JsonapiCompliable/Scoping/Base.html +843 -0
- data/docs/JsonapiCompliable/Scoping/DefaultFilter.html +318 -0
- data/docs/JsonapiCompliable/Scoping/ExtraFields.html +301 -0
- data/docs/JsonapiCompliable/Scoping/Filter.html +313 -0
- data/docs/JsonapiCompliable/Scoping/Filterable.html +364 -0
- data/docs/JsonapiCompliable/Scoping/Paginate.html +613 -0
- data/docs/JsonapiCompliable/Scoping/Sort.html +454 -0
- data/docs/JsonapiCompliable/SerializableTempId.html +216 -0
- data/docs/JsonapiCompliable/Sideload.html +2484 -0
- data/docs/JsonapiCompliable/Stats.html +117 -0
- data/docs/JsonapiCompliable/Stats/DSL.html +999 -0
- data/docs/JsonapiCompliable/Stats/Payload.html +391 -0
- data/docs/JsonapiCompliable/Util.html +117 -0
- data/docs/JsonapiCompliable/Util/FieldParams.html +228 -0
- data/docs/JsonapiCompliable/Util/Hash.html +471 -0
- data/docs/JsonapiCompliable/Util/IncludeParams.html +299 -0
- data/docs/JsonapiCompliable/Util/Persistence.html +435 -0
- data/docs/JsonapiCompliable/Util/RelationshipPayload.html +563 -0
- data/docs/JsonapiCompliable/Util/RenderOptions.html +250 -0
- data/docs/JsonapiCompliable/Util/ValidationResponse.html +532 -0
- data/docs/_index.html +527 -0
- data/docs/class_list.html +51 -0
- data/docs/css/common.css +1 -0
- data/docs/css/full_list.css +58 -0
- data/docs/css/style.css +492 -0
- data/docs/file.README.html +99 -0
- data/docs/file_list.html +56 -0
- data/docs/frames.html +17 -0
- data/docs/index.html +99 -0
- data/docs/js/app.js +248 -0
- data/docs/js/full_list.js +216 -0
- data/docs/js/jquery.js +4 -0
- data/docs/method_list.html +1731 -0
- data/docs/top-level-namespace.html +110 -0
- data/gemfiles/rails_5.gemfile +1 -1
- data/lib/jsonapi_compliable/adapters/abstract.rb +267 -4
- data/lib/jsonapi_compliable/adapters/active_record.rb +23 -1
- data/lib/jsonapi_compliable/adapters/null.rb +43 -3
- data/lib/jsonapi_compliable/base.rb +182 -33
- data/lib/jsonapi_compliable/deserializer.rb +90 -21
- data/lib/jsonapi_compliable/extensions/boolean_attribute.rb +12 -0
- data/lib/jsonapi_compliable/extensions/extra_attribute.rb +32 -0
- data/lib/jsonapi_compliable/extensions/temp_id.rb +11 -0
- data/lib/jsonapi_compliable/query.rb +94 -2
- data/lib/jsonapi_compliable/rails.rb +8 -0
- data/lib/jsonapi_compliable/resource.rb +548 -11
- data/lib/jsonapi_compliable/scope.rb +43 -1
- data/lib/jsonapi_compliable/scoping/base.rb +59 -8
- data/lib/jsonapi_compliable/scoping/default_filter.rb +33 -0
- data/lib/jsonapi_compliable/scoping/extra_fields.rb +33 -0
- data/lib/jsonapi_compliable/scoping/filter.rb +29 -2
- data/lib/jsonapi_compliable/scoping/filterable.rb +4 -0
- data/lib/jsonapi_compliable/scoping/paginate.rb +33 -0
- data/lib/jsonapi_compliable/scoping/sort.rb +18 -0
- data/lib/jsonapi_compliable/sideload.rb +229 -1
- data/lib/jsonapi_compliable/stats/dsl.rb +44 -0
- data/lib/jsonapi_compliable/stats/payload.rb +20 -0
- data/lib/jsonapi_compliable/util/field_params.rb +1 -0
- data/lib/jsonapi_compliable/util/hash.rb +18 -0
- data/lib/jsonapi_compliable/util/include_params.rb +22 -0
- data/lib/jsonapi_compliable/util/persistence.rb +13 -3
- data/lib/jsonapi_compliable/util/relationship_payload.rb +2 -0
- data/lib/jsonapi_compliable/util/render_options.rb +2 -0
- data/lib/jsonapi_compliable/util/validation_response.rb +16 -0
- data/lib/jsonapi_compliable/version.rb +1 -1
- metadata +60 -5
- data/gemfiles/rails_4.gemfile.lock +0 -208
- data/gemfiles/rails_5.gemfile.lock +0 -212
- data/lib/jsonapi_compliable/write.rb +0 -93
|
@@ -1,25 +1,65 @@
|
|
|
1
1
|
module JsonapiCompliable
|
|
2
2
|
module Adapters
|
|
3
|
+
# The Null adapter is a 'pass-through' adapter. It won't modify the scope.
|
|
4
|
+
# Useful when your customization does not support all possible
|
|
5
|
+
# configuration (e.g. the service you hit does not support sorting)
|
|
3
6
|
class Null < Abstract
|
|
7
|
+
# (see Adapters::Abstract#filter)
|
|
4
8
|
def filter(scope, attribute, value)
|
|
5
9
|
scope
|
|
6
10
|
end
|
|
7
11
|
|
|
12
|
+
# (see Adapters::Abstract#order)
|
|
8
13
|
def order(scope, attribute, direction)
|
|
9
14
|
scope
|
|
10
15
|
end
|
|
11
16
|
|
|
12
|
-
|
|
17
|
+
# (see Adapters::Abstract#paginate)
|
|
18
|
+
def paginate(scope, current_page, per_page)
|
|
13
19
|
scope
|
|
14
20
|
end
|
|
15
21
|
|
|
16
|
-
|
|
22
|
+
# (see Adapters::Abstract#count)
|
|
23
|
+
def count(scope, attr)
|
|
17
24
|
scope
|
|
18
25
|
end
|
|
19
26
|
|
|
20
|
-
|
|
27
|
+
# (see Adapters::Abstract#average)
|
|
28
|
+
def average(scope, attr)
|
|
29
|
+
scope
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# (see Adapters::Abstract#sum)
|
|
33
|
+
def sum(scope, attr)
|
|
34
|
+
scope
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# (see Adapters::Abstract#sum)
|
|
38
|
+
def maximum(scope, attr)
|
|
39
|
+
scope
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# (see Adapters::Abstract#minimum)
|
|
43
|
+
def minimum(scope, attr)
|
|
44
|
+
scope
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Since this is a null adapter, just yield
|
|
48
|
+
# @see Adapters::ActiveRecord#transaction
|
|
49
|
+
# @return Result of yield
|
|
50
|
+
# @param [Class] model_class The class we're operating on
|
|
51
|
+
def transaction(model_class)
|
|
21
52
|
yield
|
|
22
53
|
end
|
|
54
|
+
|
|
55
|
+
# (see Adapters::Abstract#resolve)
|
|
56
|
+
def resolve(scope)
|
|
57
|
+
scope
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# (see Adapters::Abstract#associate)
|
|
61
|
+
def associate(parent, child, association_name, association_type)
|
|
62
|
+
end
|
|
23
63
|
end
|
|
24
64
|
end
|
|
25
65
|
end
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
module JsonapiCompliable
|
|
2
|
+
# Provides main interface to jsonapi_compliable
|
|
3
|
+
#
|
|
4
|
+
# This gets mixed in to a "context" class, such as a Rails controller.
|
|
2
5
|
module Base
|
|
3
6
|
extend ActiveSupport::Concern
|
|
4
7
|
|
|
5
|
-
MAX_PAGE_SIZE = 1_000
|
|
6
|
-
|
|
7
8
|
included do
|
|
8
9
|
class << self
|
|
9
10
|
attr_accessor :_jsonapi_compliable
|
|
@@ -15,22 +16,77 @@ module JsonapiCompliable
|
|
|
15
16
|
end
|
|
16
17
|
end
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
# @!classmethods
|
|
20
|
+
module ClassMethods
|
|
21
|
+
# Define your JSONAPI configuration
|
|
22
|
+
#
|
|
23
|
+
# @example Inline Resource
|
|
24
|
+
# # 'Quick and Dirty' solution that does not require a separate
|
|
25
|
+
# # Resource object
|
|
26
|
+
# class PostsController < ApplicationController
|
|
27
|
+
# jsonapi do
|
|
28
|
+
# type :posts
|
|
29
|
+
# use_adapter JsonapiCompliable::Adapters::ActiveRecord
|
|
30
|
+
#
|
|
31
|
+
# allow_filter :title
|
|
32
|
+
# end
|
|
33
|
+
# end
|
|
34
|
+
#
|
|
35
|
+
# @example Resource Class (preferred)
|
|
36
|
+
# # Make code reusable by encapsulating it in a Resource class
|
|
37
|
+
# class PostsController < ApplicationController
|
|
38
|
+
# jsonapi resource: PostResource
|
|
39
|
+
# end
|
|
40
|
+
#
|
|
41
|
+
# @see Resource
|
|
42
|
+
# @param resource [Resource] the Resource class associated to this endpoint
|
|
43
|
+
# @return [void]
|
|
44
|
+
def jsonapi(foo = 'bar', resource: nil, &blk)
|
|
45
|
+
if resource
|
|
46
|
+
self._jsonapi_compliable = resource
|
|
47
|
+
else
|
|
48
|
+
if !self._jsonapi_compliable
|
|
49
|
+
self._jsonapi_compliable = Class.new(JsonapiCompliable::Resource)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
self._jsonapi_compliable.class_eval(&blk) if blk
|
|
54
|
+
end
|
|
20
55
|
end
|
|
21
56
|
|
|
22
|
-
|
|
23
|
-
|
|
57
|
+
# Returns an instance of the associated Resource
|
|
58
|
+
#
|
|
59
|
+
# In other words, if you configured your controller as:
|
|
60
|
+
#
|
|
61
|
+
# jsonapi resource: MyResource
|
|
62
|
+
#
|
|
63
|
+
# This returns MyResource.new
|
|
64
|
+
#
|
|
65
|
+
# @return [Resource] the configured Resource for this controller
|
|
66
|
+
def resource
|
|
67
|
+
@resource ||= self.class._jsonapi_compliable.new
|
|
24
68
|
end
|
|
25
69
|
|
|
70
|
+
# Instantiates the relevant Query object
|
|
71
|
+
#
|
|
72
|
+
# @see Query
|
|
73
|
+
# @return [Query] the Query object for this resource/params
|
|
26
74
|
def query
|
|
27
75
|
@query ||= Query.new(resource, params)
|
|
28
76
|
end
|
|
29
77
|
|
|
78
|
+
# @see Query#to_hash
|
|
79
|
+
# @return [Hash] the normalized query hash for only the *current* resource
|
|
30
80
|
def query_hash
|
|
31
81
|
@query_hash ||= query.to_hash[resource.type]
|
|
32
82
|
end
|
|
33
83
|
|
|
84
|
+
# Tracks the current context so we can refer to it within any
|
|
85
|
+
# random object. Helpful for easy-access to things like the current
|
|
86
|
+
# user.
|
|
87
|
+
#
|
|
88
|
+
# @api private
|
|
89
|
+
# @yieldreturn Code to run within the current context
|
|
34
90
|
def wrap_context
|
|
35
91
|
if self.class._jsonapi_compliable
|
|
36
92
|
resource.with_context(self, action_name.to_sym) do
|
|
@@ -39,14 +95,57 @@ module JsonapiCompliable
|
|
|
39
95
|
end
|
|
40
96
|
end
|
|
41
97
|
|
|
98
|
+
# Use when direct, low-level access to the scope is required.
|
|
99
|
+
#
|
|
100
|
+
# @example Show Action
|
|
101
|
+
# # Scope#resolve returns an array, but we only want to render
|
|
102
|
+
# # one object, not an array
|
|
103
|
+
# scope = jsonapi_scope(Employee.where(id: params[:id]))
|
|
104
|
+
# render_jsonapi(scope.resolve.first, scope: false)
|
|
105
|
+
#
|
|
106
|
+
# @example Scope Chaining
|
|
107
|
+
# # Chain onto scope after running through typical DSL
|
|
108
|
+
# # Here, we'll add active: true to our hash if the user
|
|
109
|
+
# # is filtering on something
|
|
110
|
+
# scope = jsonapi_scope({})
|
|
111
|
+
# scope.object.merge!(active: true) if scope.object[:filter]
|
|
112
|
+
#
|
|
113
|
+
# @see Resource#build_scope
|
|
114
|
+
# @return [Scope] the configured scope
|
|
42
115
|
def jsonapi_scope(scope, opts = {})
|
|
43
116
|
resource.build_scope(scope, query, opts)
|
|
44
117
|
end
|
|
45
118
|
|
|
119
|
+
# @see Deserializer#initialize
|
|
120
|
+
# @return [Deserializer]
|
|
46
121
|
def deserialized_params
|
|
47
122
|
@deserialized_params ||= JsonapiCompliable::Deserializer.new(params, request.env)
|
|
48
123
|
end
|
|
49
124
|
|
|
125
|
+
# Create the resource model and process all nested relationships via the
|
|
126
|
+
# serialized parameters. Any error, including validation errors, will roll
|
|
127
|
+
# back the transaction.
|
|
128
|
+
#
|
|
129
|
+
# @example Basic Rails
|
|
130
|
+
# # Example Resource must have 'model'
|
|
131
|
+
# #
|
|
132
|
+
# # class PostResource < ApplicationResource
|
|
133
|
+
# # model Post
|
|
134
|
+
# # end
|
|
135
|
+
# def create
|
|
136
|
+
# post, success = jsonapi_create.to_a
|
|
137
|
+
#
|
|
138
|
+
# if success
|
|
139
|
+
# render_jsonapi(post, scope: false)
|
|
140
|
+
# else
|
|
141
|
+
# render_errors_for(post)
|
|
142
|
+
# end
|
|
143
|
+
# end
|
|
144
|
+
#
|
|
145
|
+
# @see Resource.model
|
|
146
|
+
# @see #resource
|
|
147
|
+
# @see #deserialized_params
|
|
148
|
+
# @return [Util::ValidationResponse]
|
|
50
149
|
def jsonapi_create
|
|
51
150
|
_persist do
|
|
52
151
|
resource.persist_with_relationships \
|
|
@@ -56,6 +155,28 @@ module JsonapiCompliable
|
|
|
56
155
|
end
|
|
57
156
|
end
|
|
58
157
|
|
|
158
|
+
# Update the resource model and process all nested relationships via the
|
|
159
|
+
# serialized parameters. Any error, including validation errors, will roll
|
|
160
|
+
# back the transaction.
|
|
161
|
+
#
|
|
162
|
+
# @example Basic Rails
|
|
163
|
+
# # Example Resource must have 'model'
|
|
164
|
+
# #
|
|
165
|
+
# # class PostResource < ApplicationResource
|
|
166
|
+
# # model Post
|
|
167
|
+
# # end
|
|
168
|
+
# def update
|
|
169
|
+
# post, success = jsonapi_update.to_a
|
|
170
|
+
#
|
|
171
|
+
# if success
|
|
172
|
+
# render_jsonapi(post, scope: false)
|
|
173
|
+
# else
|
|
174
|
+
# render_errors_for(post)
|
|
175
|
+
# end
|
|
176
|
+
# end
|
|
177
|
+
#
|
|
178
|
+
# @see #jsonapi_create
|
|
179
|
+
# @return [Util::ValidationResponse]
|
|
59
180
|
def jsonapi_update
|
|
60
181
|
_persist do
|
|
61
182
|
resource.persist_with_relationships \
|
|
@@ -65,21 +186,37 @@ module JsonapiCompliable
|
|
|
65
186
|
end
|
|
66
187
|
end
|
|
67
188
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
189
|
+
# Similar to +render :json+ or +render :jsonapi+
|
|
190
|
+
#
|
|
191
|
+
# By default, this will "build" the scope via +#jsonapi_scope+. To avoid
|
|
192
|
+
# this, pass +scope: false+
|
|
193
|
+
#
|
|
194
|
+
# This builds relevant options and sends them to
|
|
195
|
+
# +JSONAPI::Serializable::Renderer.render+from
|
|
196
|
+
# {http://jsonapi-rb.org jsonapi-rb}
|
|
197
|
+
#
|
|
198
|
+
# @example Build Scope by Default
|
|
199
|
+
# # Employee.all returns an ActiveRecord::Relation. No SQL is fired at this point.
|
|
200
|
+
# # We further 'chain' onto this scope, applying pagination, sorting,
|
|
201
|
+
# # filters, etc that the user has requested.
|
|
202
|
+
# def index
|
|
203
|
+
# employees = Employee.all
|
|
204
|
+
# render_jsonapi(employees)
|
|
205
|
+
# end
|
|
206
|
+
#
|
|
207
|
+
# @example Avoid Building Scope by Default
|
|
208
|
+
# # Maybe we already manually scoped, and don't want to fire the logic twice
|
|
209
|
+
# # This code is equivalent to the above example
|
|
210
|
+
# def index
|
|
211
|
+
# scope = jsonapi_scope(Employee.all)
|
|
212
|
+
# # ... do other things with the scope ...
|
|
213
|
+
# render_jsonapi(scope.resolve, scope: false)
|
|
214
|
+
# end
|
|
215
|
+
#
|
|
216
|
+
# @param scope [Scope, Object] the scope to build or render.
|
|
217
|
+
# @param [Hash] opts the render options passed to {http://jsonapi-rb.org jsonapi-rb}
|
|
218
|
+
# @option opts [Boolean] :scope Default: true. Should we call #jsonapi_scope on this object?
|
|
219
|
+
# @see #jsonapi_scope
|
|
83
220
|
def render_jsonapi(scope, opts = {})
|
|
84
221
|
scope = jsonapi_scope(scope) unless opts[:scope] == false || scope.is_a?(JsonapiCompliable::Scope)
|
|
85
222
|
opts = default_jsonapi_render_options.merge(opts)
|
|
@@ -89,29 +226,41 @@ module JsonapiCompliable
|
|
|
89
226
|
perform_render_jsonapi(opts)
|
|
90
227
|
end
|
|
91
228
|
|
|
92
|
-
#
|
|
93
|
-
#
|
|
229
|
+
# Define a hash that will be automatically merged into your
|
|
230
|
+
# render_jsonapi call
|
|
231
|
+
#
|
|
232
|
+
# @example
|
|
233
|
+
# # this
|
|
234
|
+
# render_jsonapi(foo)
|
|
235
|
+
# # is equivalent to this
|
|
236
|
+
# render jsonapi: foo, default_jsonapi_render_options
|
|
237
|
+
#
|
|
238
|
+
# @see #render_jsonapi
|
|
239
|
+
# @return [Hash] the options hash you define
|
|
94
240
|
def default_jsonapi_render_options
|
|
95
241
|
{}.tap do |options|
|
|
96
242
|
end
|
|
97
243
|
end
|
|
98
244
|
|
|
245
|
+
private
|
|
246
|
+
|
|
99
247
|
def force_includes?
|
|
100
248
|
not params[:data].nil?
|
|
101
249
|
end
|
|
102
250
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
self._jsonapi_compliable = resource
|
|
107
|
-
else
|
|
108
|
-
if !self._jsonapi_compliable
|
|
109
|
-
self._jsonapi_compliable = Class.new(JsonapiCompliable::Resource)
|
|
110
|
-
end
|
|
111
|
-
end
|
|
251
|
+
def perform_render_jsonapi(opts)
|
|
252
|
+
JSONAPI::Serializable::Renderer.render(opts.delete(:jsonapi), opts)
|
|
253
|
+
end
|
|
112
254
|
|
|
113
|
-
|
|
255
|
+
def _persist
|
|
256
|
+
validation_response = nil
|
|
257
|
+
resource.transaction do
|
|
258
|
+
object = yield
|
|
259
|
+
validation_response = Util::ValidationResponse.new \
|
|
260
|
+
object, deserialized_params
|
|
261
|
+
raise Errors::ValidationError unless validation_response.to_a[1]
|
|
114
262
|
end
|
|
263
|
+
validation_response
|
|
115
264
|
end
|
|
116
265
|
end
|
|
117
266
|
end
|
|
@@ -1,35 +1,81 @@
|
|
|
1
|
+
# Responsible for parsing incoming write payloads
|
|
2
|
+
#
|
|
3
|
+
# Given a PUT payload like:
|
|
4
|
+
#
|
|
5
|
+
# {
|
|
6
|
+
# data: {
|
|
7
|
+
# id: '1',
|
|
8
|
+
# type: 'posts',
|
|
9
|
+
# attributes: { title: 'My Title' },
|
|
10
|
+
# relationships: {
|
|
11
|
+
# author: {
|
|
12
|
+
# data: {
|
|
13
|
+
# id: '1',
|
|
14
|
+
# type: 'authors'
|
|
15
|
+
# }
|
|
16
|
+
# }
|
|
17
|
+
# }
|
|
18
|
+
# },
|
|
19
|
+
# included: [
|
|
20
|
+
# {
|
|
21
|
+
# id: '1'
|
|
22
|
+
# type: 'authors',
|
|
23
|
+
# attributes: { name: 'Joe Author' }
|
|
24
|
+
# }
|
|
25
|
+
# ]
|
|
26
|
+
# }
|
|
27
|
+
#
|
|
28
|
+
# You can now easily deal with this payload:
|
|
29
|
+
#
|
|
30
|
+
# deserializer.attributes
|
|
31
|
+
# # => { id: '1', title: 'My Title' }
|
|
32
|
+
# deserializer.meta
|
|
33
|
+
# # => { type: 'posts', method: :update }
|
|
34
|
+
# deserializer.relationships
|
|
35
|
+
# # {
|
|
36
|
+
# # author: {
|
|
37
|
+
# # meta: { ... },
|
|
38
|
+
# # attributes: { ... },
|
|
39
|
+
# # relationships: { ... }
|
|
40
|
+
# # }
|
|
41
|
+
# # }
|
|
42
|
+
#
|
|
43
|
+
# When creating objects, we accept a +temp-id+ so that the client can track
|
|
44
|
+
# the object it just created. Expect this in +meta+:
|
|
45
|
+
#
|
|
46
|
+
# { type: 'authors', method: :create, temp_id: 'abc123' }
|
|
1
47
|
class JsonapiCompliable::Deserializer
|
|
48
|
+
# @param payload [Hash] The incoming payload with symbolized keys
|
|
49
|
+
# @param env [Hash] the Rack env (e.g. +request.env+).
|
|
2
50
|
def initialize(payload, env)
|
|
3
51
|
@payload = payload
|
|
4
52
|
@env = env
|
|
5
53
|
end
|
|
6
54
|
|
|
55
|
+
# @return [Hash] the raw :data value of the payload
|
|
7
56
|
def data
|
|
8
57
|
@payload[:data]
|
|
9
58
|
end
|
|
10
59
|
|
|
60
|
+
# @return [String] the raw :id value of the payload
|
|
11
61
|
def id
|
|
12
62
|
data[:id]
|
|
13
63
|
end
|
|
14
64
|
|
|
65
|
+
# @return [Hash] the raw :attributes hash + +id+
|
|
15
66
|
def attributes
|
|
16
67
|
@attributes ||= raw_attributes.tap do |hash|
|
|
17
68
|
hash.merge!(id: id) if id
|
|
18
69
|
end
|
|
19
70
|
end
|
|
20
71
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
when 'PUT', 'PATCH' then :update
|
|
29
|
-
when 'DELETE' then :destroy
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
|
|
72
|
+
# 'meta' information about this resource. Includes:
|
|
73
|
+
#
|
|
74
|
+
# +type+: the jsonapi type
|
|
75
|
+
# +method+: create/update/destroy/disassociate. Based on the request env or the +method+ within the +relationships+ hash
|
|
76
|
+
# +temp_id+: the +temp-id+, if specified
|
|
77
|
+
#
|
|
78
|
+
# @return [Hash]
|
|
33
79
|
def meta
|
|
34
80
|
{
|
|
35
81
|
type: data[:type],
|
|
@@ -38,23 +84,25 @@ class JsonapiCompliable::Deserializer
|
|
|
38
84
|
}
|
|
39
85
|
end
|
|
40
86
|
|
|
87
|
+
# @return [Hash] the relationships hash
|
|
41
88
|
def relationships
|
|
42
89
|
@relationships ||= process_relationships(raw_relationships)
|
|
43
90
|
end
|
|
44
91
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
92
|
+
# Parses the +relationships+ recursively and builds an all-hash
|
|
93
|
+
# include directive like
|
|
94
|
+
#
|
|
95
|
+
# { posts: { comments: {} } }
|
|
96
|
+
#
|
|
97
|
+
# Relationships that have been marked for destruction will NOT
|
|
98
|
+
# be part of the include directive.
|
|
99
|
+
#
|
|
100
|
+
# @return [Hash] the include directive
|
|
49
101
|
def include_directive(memo = {}, relationship_node = nil)
|
|
50
102
|
relationship_node ||= relationships
|
|
51
103
|
|
|
52
104
|
relationship_node.each_pair do |name, relationship_payload|
|
|
53
|
-
|
|
54
|
-
next if arrayified.all? { |rp| removed?(rp) }
|
|
55
|
-
|
|
56
|
-
memo[name] ||= {}
|
|
57
|
-
deep_merge!(memo[name], sub_directives(memo[name], arrayified))
|
|
105
|
+
merge_include_directive(memo, name, relationship_payload)
|
|
58
106
|
end
|
|
59
107
|
|
|
60
108
|
memo
|
|
@@ -62,6 +110,27 @@ class JsonapiCompliable::Deserializer
|
|
|
62
110
|
|
|
63
111
|
private
|
|
64
112
|
|
|
113
|
+
def merge_include_directive(memo, name, relationship_payload)
|
|
114
|
+
arrayified = [relationship_payload].flatten
|
|
115
|
+
return if arrayified.all? { |rp| removed?(rp) }
|
|
116
|
+
|
|
117
|
+
memo[name] ||= {}
|
|
118
|
+
deep_merge!(memo[name], sub_directives(memo[name], arrayified))
|
|
119
|
+
memo
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def included
|
|
123
|
+
@payload[:included] || []
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def method
|
|
127
|
+
case @env['REQUEST_METHOD']
|
|
128
|
+
when 'POST' then :create
|
|
129
|
+
when 'PUT', 'PATCH' then :update
|
|
130
|
+
when 'DELETE' then :destroy
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
65
134
|
def removed?(relationship_payload)
|
|
66
135
|
method = relationship_payload[:meta][:method]
|
|
67
136
|
[:disassociate, :destroy].include?(method)
|