jsonapi-consumer 0.1.1 → 1.0.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 +5 -5
- data/.circleci/config.yml +27 -0
- data/.gitignore +1 -0
- data/Gemfile +6 -4
- data/README.md +9 -38
- data/Rakefile +17 -6
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/jsonapi-consumer.gemspec +10 -11
- data/lib/jsonapi/consumer/associations/base_association.rb +26 -0
- data/lib/jsonapi/consumer/associations/belongs_to.rb +30 -0
- data/lib/jsonapi/consumer/associations/has_many.rb +26 -0
- data/lib/jsonapi/consumer/associations/has_one.rb +19 -0
- data/lib/jsonapi/consumer/connection.rb +36 -0
- data/lib/jsonapi/consumer/error_collector.rb +91 -0
- data/lib/jsonapi/consumer/errors.rb +34 -76
- data/lib/jsonapi/consumer/formatter.rb +145 -0
- data/lib/jsonapi/consumer/helpers/callbacks.rb +27 -0
- data/lib/jsonapi/consumer/helpers/dirty.rb +71 -0
- data/lib/jsonapi/consumer/helpers/dynamic_attributes.rb +83 -0
- data/lib/jsonapi/consumer/helpers/uri.rb +9 -0
- data/lib/jsonapi/consumer/implementation.rb +12 -0
- data/lib/jsonapi/consumer/included_data.rb +49 -0
- data/lib/jsonapi/consumer/linking/links.rb +22 -0
- data/lib/jsonapi/consumer/linking/top_level_links.rb +39 -0
- data/lib/jsonapi/consumer/meta_data.rb +19 -0
- data/lib/jsonapi/consumer/middleware/json_request.rb +26 -0
- data/lib/jsonapi/consumer/middleware/parse_json.rb +22 -23
- data/lib/jsonapi/consumer/middleware/status.rb +41 -0
- data/lib/jsonapi/consumer/paginating/paginator.rb +89 -0
- data/lib/jsonapi/consumer/parsers/parser.rb +113 -0
- data/lib/jsonapi/consumer/query/builder.rb +212 -0
- data/lib/jsonapi/consumer/query/requestor.rb +67 -0
- data/lib/jsonapi/consumer/relationships/relations.rb +56 -0
- data/lib/jsonapi/consumer/relationships/top_level_relations.rb +30 -0
- data/lib/jsonapi/consumer/resource.rb +514 -54
- data/lib/jsonapi/consumer/result_set.rb +25 -0
- data/lib/jsonapi/consumer/schema.rb +153 -0
- data/lib/jsonapi/consumer/utils.rb +28 -0
- data/lib/jsonapi/consumer/version.rb +1 -1
- data/lib/jsonapi/consumer.rb +59 -34
- metadata +51 -111
- data/.rspec +0 -2
- data/CHANGELOG.md +0 -36
- data/lib/jsonapi/consumer/middleware/raise_error.rb +0 -21
- data/lib/jsonapi/consumer/middleware/request_headers.rb +0 -20
- data/lib/jsonapi/consumer/middleware/request_timeout.rb +0 -9
- data/lib/jsonapi/consumer/middleware.rb +0 -5
- data/lib/jsonapi/consumer/parser.rb +0 -75
- data/lib/jsonapi/consumer/query/base.rb +0 -34
- data/lib/jsonapi/consumer/query/create.rb +0 -9
- data/lib/jsonapi/consumer/query/delete.rb +0 -10
- data/lib/jsonapi/consumer/query/find.rb +0 -16
- data/lib/jsonapi/consumer/query/new.rb +0 -15
- data/lib/jsonapi/consumer/query/update.rb +0 -11
- data/lib/jsonapi/consumer/query.rb +0 -5
- data/lib/jsonapi/consumer/resource/association_concern.rb +0 -203
- data/lib/jsonapi/consumer/resource/attributes_concern.rb +0 -70
- data/lib/jsonapi/consumer/resource/connection_concern.rb +0 -99
- data/lib/jsonapi/consumer/resource/finders_concern.rb +0 -28
- data/lib/jsonapi/consumer/resource/object_build_concern.rb +0 -28
- data/lib/jsonapi/consumer/resource/serializer_concern.rb +0 -63
- data/spec/fixtures/.gitkeep +0 -0
- data/spec/fixtures/resources.rb +0 -45
- data/spec/fixtures/responses.rb +0 -64
- data/spec/jsonapi/consumer/associations_spec.rb +0 -166
- data/spec/jsonapi/consumer/attributes_spec.rb +0 -27
- data/spec/jsonapi/consumer/connection_spec.rb +0 -147
- data/spec/jsonapi/consumer/error_handling_spec.rb +0 -37
- data/spec/jsonapi/consumer/object_build_spec.rb +0 -20
- data/spec/jsonapi/consumer/parser_spec.rb +0 -39
- data/spec/jsonapi/consumer/resource_spec.rb +0 -62
- data/spec/jsonapi/consumer/serializer_spec.rb +0 -41
- data/spec/spec_helper.rb +0 -97
- data/spec/support/.gitkeep +0 -0
- data/spec/support/load_fixtures.rb +0 -4
@@ -1,92 +1,552 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
require 'active_support/all'
|
3
|
+
require 'active_model'
|
4
|
+
|
1
5
|
module JSONAPI::Consumer
|
2
|
-
|
3
|
-
extend
|
6
|
+
class Resource
|
7
|
+
extend ActiveModel::Naming
|
8
|
+
extend ActiveModel::Translation
|
9
|
+
include ActiveModel::Validations
|
10
|
+
include ActiveModel::Conversion
|
11
|
+
include ActiveModel::Serialization
|
4
12
|
|
5
|
-
|
6
|
-
|
13
|
+
include Helpers::DynamicAttributes
|
14
|
+
include Helpers::Dirty
|
7
15
|
|
8
|
-
|
9
|
-
|
10
|
-
|
16
|
+
attr_accessor :last_result_set,
|
17
|
+
:links,
|
18
|
+
:relationships
|
19
|
+
class_attribute :site,
|
20
|
+
:primary_key,
|
21
|
+
:parser,
|
22
|
+
:paginator,
|
23
|
+
:connection_class,
|
24
|
+
:connection_object,
|
25
|
+
:connection_options,
|
26
|
+
:query_builder,
|
27
|
+
:linker,
|
28
|
+
:relationship_linker,
|
29
|
+
:read_only_attributes,
|
30
|
+
:requestor_class,
|
31
|
+
:associations,
|
32
|
+
:json_key_format,
|
33
|
+
:route_format,
|
34
|
+
instance_accessor: false
|
35
|
+
self.primary_key = :id
|
36
|
+
self.parser = Parsers::Parser
|
37
|
+
self.paginator = Paginating::Paginator
|
38
|
+
self.connection_class = Connection
|
39
|
+
self.connection_options = {}
|
40
|
+
self.query_builder = Query::Builder
|
41
|
+
self.linker = Linking::Links
|
42
|
+
self.relationship_linker = Relationships::Relations
|
43
|
+
self.read_only_attributes = [:id, :type, :links, :meta, :relationships]
|
44
|
+
self.requestor_class = Query::Requestor
|
45
|
+
self.associations = []
|
46
|
+
|
47
|
+
#:underscored_key, :camelized_key, :dasherized_key, or custom
|
48
|
+
self.json_key_format = :underscored_key
|
49
|
+
|
50
|
+
#:underscored_route, :camelized_route, :dasherized_route, or custom
|
51
|
+
self.route_format = :underscored_route
|
52
|
+
|
53
|
+
include Associations::BelongsTo
|
54
|
+
include Associations::HasMany
|
55
|
+
include Associations::HasOne
|
56
|
+
|
57
|
+
class << self
|
58
|
+
extend Forwardable
|
59
|
+
def_delegators :_new_scope, :where, :order, :includes, :select, :all, :paginate, :page, :with_params, :first, :find, :last
|
60
|
+
|
61
|
+
# The table name for this resource. i.e. Article -> articles, Person -> people
|
62
|
+
#
|
63
|
+
# @return [String] The table name for this resource
|
64
|
+
def table_name
|
65
|
+
route_formatter.format(resource_name.pluralize)
|
66
|
+
end
|
67
|
+
|
68
|
+
# The name of a single resource. i.e. Article -> article, Person -> person
|
69
|
+
#
|
70
|
+
# @return [String]
|
71
|
+
def resource_name
|
72
|
+
name.demodulize.underscore
|
73
|
+
end
|
74
|
+
|
75
|
+
# Specifies the JSON API resource type. By default this is inferred
|
76
|
+
# from the resource class name.
|
77
|
+
#
|
78
|
+
# @return [String] Resource path
|
79
|
+
def type
|
80
|
+
table_name
|
81
|
+
end
|
82
|
+
|
83
|
+
# Specifies the relative path that should be used for this resource;
|
84
|
+
# by default, this is inferred from the resource class name.
|
85
|
+
#
|
86
|
+
# @return [String] Resource path
|
87
|
+
def resource_path
|
88
|
+
table_name
|
89
|
+
end
|
90
|
+
|
91
|
+
# Load a resource object from attributes and consider it persisted
|
92
|
+
#
|
93
|
+
# @return [Resource] Persisted resource object
|
94
|
+
def load(params)
|
95
|
+
new(params).tap do |resource|
|
96
|
+
resource.mark_as_persisted!
|
97
|
+
resource.clear_changes_information
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# Return/build a connection object
|
102
|
+
#
|
103
|
+
# @return [Connection] The connection to the json api server
|
104
|
+
def connection(rebuild = false, &block)
|
105
|
+
_build_connection(rebuild, &block)
|
106
|
+
connection_object
|
107
|
+
end
|
108
|
+
|
109
|
+
# Param names that will be considered path params. They will be used
|
110
|
+
# to build the resource path rather than treated as attributes
|
111
|
+
#
|
112
|
+
# @return [Array] Param name symbols of parameters that will be treated as path parameters
|
113
|
+
def prefix_params
|
114
|
+
_belongs_to_associations.map(&:param)
|
115
|
+
end
|
116
|
+
|
117
|
+
# Return the path or path pattern for this resource
|
118
|
+
def path(params = nil)
|
119
|
+
parts = [resource_path]
|
120
|
+
if params && _prefix_path.present?
|
121
|
+
path_params = params.delete(:path) || params
|
122
|
+
parts.unshift(_set_prefix_path(path_params.symbolize_keys))
|
123
|
+
else
|
124
|
+
parts.unshift(_prefix_path)
|
125
|
+
end
|
126
|
+
parts.reject!(&:blank?)
|
127
|
+
File.join(*parts)
|
128
|
+
rescue KeyError
|
129
|
+
raise ArgumentError, "Not all prefix parameters specified"
|
130
|
+
end
|
131
|
+
|
132
|
+
# Create a new instance of this resource class
|
133
|
+
#
|
134
|
+
# @param attributes [Hash] The attributes to create this resource with
|
135
|
+
# @return [Resource] The instance you tried to create. You will have to check the persisted state or errors on this object to see success/failure.
|
136
|
+
def create(attributes = {})
|
137
|
+
new(attributes).tap do |resource|
|
138
|
+
resource.save
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
# Within the given block, add these headers to all requests made by
|
143
|
+
# the resource class
|
144
|
+
#
|
145
|
+
# @param headers [Hash] The headers to send along
|
146
|
+
# @param block [Block] The block where headers will be set for
|
147
|
+
def with_headers(headers)
|
148
|
+
self._custom_headers = headers
|
149
|
+
yield
|
150
|
+
ensure
|
151
|
+
self._custom_headers = {}
|
152
|
+
end
|
153
|
+
|
154
|
+
# The current custom headers to send with any request made by this
|
155
|
+
# resource class. This supports inheritance so it only needs to be
|
156
|
+
# set on the base class.
|
157
|
+
#
|
158
|
+
# @return [Hash] Headers
|
159
|
+
def custom_headers
|
160
|
+
return _header_store.to_h if superclass == Object
|
161
|
+
|
162
|
+
superclass.custom_headers.merge(_header_store.to_h)
|
163
|
+
end
|
164
|
+
|
165
|
+
# Run a command wrapped in an Authorization header
|
166
|
+
#
|
167
|
+
def authorize_with(jwt, &block)
|
168
|
+
with_headers(authorization: %(Bearer #{jwt}), &block)
|
169
|
+
end
|
170
|
+
|
171
|
+
# Set the Authorization header to a JWT value
|
172
|
+
#
|
173
|
+
def authorize_with=(jwt)
|
174
|
+
if jwt.nil?
|
175
|
+
self._custom_headers = {authorization: nil}
|
176
|
+
else
|
177
|
+
self._custom_headers = {authorization: %(Bearer #{jwt})}
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
# Clears the Authorization header
|
182
|
+
#
|
183
|
+
def clear_authorization!
|
184
|
+
self.authorize_with = nil
|
185
|
+
end
|
186
|
+
|
187
|
+
# @return [String] The Authorization header
|
188
|
+
def authorized_as
|
189
|
+
custom_headers[:authorization]
|
190
|
+
end
|
191
|
+
|
192
|
+
# Returns based on the presence of an Authorization header
|
193
|
+
#
|
194
|
+
# @return [Boolean]
|
195
|
+
def authorized?
|
196
|
+
!custom_headers[:authorization].nil?
|
197
|
+
end
|
198
|
+
|
199
|
+
# Returns the requestor for this resource class
|
200
|
+
#
|
201
|
+
# @return [Requestor] The requestor for this resource class
|
202
|
+
def requestor
|
203
|
+
@requestor ||= requestor_class.new(self)
|
204
|
+
end
|
205
|
+
|
206
|
+
# Default attributes that every instance of this resource should be
|
207
|
+
# initialized with. Optionally, override this method in a subclass.
|
208
|
+
#
|
209
|
+
# @return [Hash] Default attributes
|
210
|
+
def default_attributes
|
211
|
+
{type: type}
|
212
|
+
end
|
213
|
+
|
214
|
+
# Returns the schema for this resource class
|
215
|
+
#
|
216
|
+
# @return [Schema] The schema for this resource class
|
217
|
+
def schema
|
218
|
+
@schema ||= Schema.new
|
219
|
+
end
|
220
|
+
|
221
|
+
def key_formatter
|
222
|
+
JSONAPI::Consumer::Formatter.formatter_for(json_key_format)
|
223
|
+
end
|
224
|
+
|
225
|
+
def route_formatter
|
226
|
+
JSONAPI::Consumer::Formatter.formatter_for(route_format)
|
227
|
+
end
|
228
|
+
|
229
|
+
protected
|
230
|
+
|
231
|
+
# Declares a new class/instance method that acts on the collection/member
|
232
|
+
#
|
233
|
+
# @param name [Symbol] the name of the endpoint
|
234
|
+
# @param options [Hash] endpoint options
|
235
|
+
# @option [Symbol] :on One of [:collection or :member] to decide whether it's a collect or member method
|
236
|
+
# @option [Symbol] :request_method The request method (:get, :post, etc)
|
237
|
+
def custom_endpoint(name, options = {})
|
238
|
+
if :collection == options.delete(:on)
|
239
|
+
collection_endpoint(name, options)
|
240
|
+
else
|
241
|
+
member_endpoint(name, options)
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
# Declares a new class method that acts on the collection
|
246
|
+
#
|
247
|
+
# @param name [Symbol] the name of the endpoint and the method name
|
248
|
+
# @param options [Hash] endpoint options
|
249
|
+
# @option options [Symbol] :request_method The request method (:get, :post, etc)
|
250
|
+
def collection_endpoint(name, options = {})
|
251
|
+
metaclass = class << self
|
252
|
+
self
|
253
|
+
end
|
254
|
+
metaclass.instance_eval do
|
255
|
+
define_method(name) do |*params|
|
256
|
+
request_params = params.first || {}
|
257
|
+
requestor.custom(name, options, request_params)
|
258
|
+
end
|
259
|
+
end
|
260
|
+
end
|
11
261
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
262
|
+
# Declares a new instance method that acts on the member object
|
263
|
+
#
|
264
|
+
# @param name [Symbol] the name of the endpoint and the method name
|
265
|
+
# @param options [Hash] endpoint options
|
266
|
+
# @option options [Symbol] :request_method The request method (:get, :post, etc)
|
267
|
+
def member_endpoint(name, options = {})
|
268
|
+
define_method name do |*params|
|
269
|
+
request_params = params.first || {}
|
270
|
+
request_params[self.class.primary_key] = attributes.fetch(self.class.primary_key)
|
271
|
+
self.class.requestor.custom(name, options, request_params)
|
272
|
+
end
|
273
|
+
end
|
18
274
|
|
19
|
-
|
20
|
-
|
21
|
-
|
275
|
+
# Declares a new property by name
|
276
|
+
#
|
277
|
+
# @param name [Symbol] the name of the property
|
278
|
+
# @param options [Hash] property options
|
279
|
+
# @option options [Symbol] :type The property type
|
280
|
+
# @option options [Symbol] :default The default value for the property
|
281
|
+
def property(name, options = {})
|
282
|
+
schema.add(name, options)
|
22
283
|
end
|
23
284
|
|
24
|
-
|
25
|
-
|
285
|
+
# Declare multiple properties with the same optional options
|
286
|
+
#
|
287
|
+
# @param [Array<Symbol>] names
|
288
|
+
# @param options [Hash] property options
|
289
|
+
# @option options [Symbol] :type The property type
|
290
|
+
# @option options [Symbol] :default The default value for the property
|
291
|
+
def properties(*names)
|
292
|
+
options = names.last.is_a?(Hash) ? names.pop : {}
|
293
|
+
names.each do |name|
|
294
|
+
property name, options
|
295
|
+
end
|
26
296
|
end
|
27
297
|
|
28
|
-
def
|
29
|
-
|
298
|
+
def _belongs_to_associations
|
299
|
+
associations.select{|association| association.is_a?(Associations::BelongsTo::Association) }
|
30
300
|
end
|
31
301
|
|
32
|
-
def
|
33
|
-
|
302
|
+
def _prefix_path
|
303
|
+
paths = _belongs_to_associations.map do |a|
|
304
|
+
a.to_prefix_path(route_formatter)
|
305
|
+
end
|
306
|
+
|
307
|
+
paths.join("/")
|
308
|
+
end
|
309
|
+
|
310
|
+
def _set_prefix_path(attrs)
|
311
|
+
paths = _belongs_to_associations.map do |a|
|
312
|
+
a.set_prefix_path(attrs, route_formatter)
|
313
|
+
end
|
314
|
+
|
315
|
+
paths.join("/")
|
34
316
|
end
|
35
317
|
|
36
|
-
def
|
37
|
-
|
318
|
+
def _new_scope
|
319
|
+
query_builder.new(self)
|
38
320
|
end
|
39
321
|
|
40
|
-
|
322
|
+
def _custom_headers=(headers)
|
323
|
+
_header_store.replace(headers)
|
324
|
+
end
|
41
325
|
|
42
|
-
def
|
43
|
-
|
326
|
+
def _header_store
|
327
|
+
Thread.current["json_api_client-#{resource_name}"] ||= {}
|
44
328
|
end
|
45
329
|
|
46
|
-
def
|
47
|
-
|
330
|
+
def _build_connection(rebuild = false)
|
331
|
+
return connection_object unless connection_object.nil? || rebuild
|
332
|
+
self.connection_object = connection_class.new(connection_options.merge(site: site)).tap do |conn|
|
333
|
+
yield(conn) if block_given?
|
334
|
+
end
|
48
335
|
end
|
49
336
|
end
|
50
337
|
|
51
|
-
|
52
|
-
|
53
|
-
|
338
|
+
# Instantiate a new resource object
|
339
|
+
#
|
340
|
+
# @param params [Hash] Attributes, links, and relationships
|
341
|
+
def initialize(params = {})
|
342
|
+
@persisted = nil
|
343
|
+
self.links = self.class.linker.new(params.delete("links") || {})
|
344
|
+
self.relationships = self.class.relationship_linker.new(self.class, params.delete("relationships") || {})
|
345
|
+
self.attributes = self.class.default_attributes.merge(params)
|
346
|
+
|
347
|
+
self.class.schema.each_property do |property|
|
348
|
+
attributes[property.name] = property.default unless attributes.has_key?(property.name) || property.default.nil?
|
54
349
|
end
|
55
350
|
|
56
|
-
self.
|
57
|
-
|
58
|
-
|
351
|
+
self.class.associations.each do |association|
|
352
|
+
if params.has_key?(association.attr_name.to_s)
|
353
|
+
set_attribute(association.attr_name, params[association.attr_name.to_s])
|
354
|
+
end
|
355
|
+
end
|
356
|
+
end
|
357
|
+
|
358
|
+
# Set the current attributes and try to save them
|
359
|
+
#
|
360
|
+
# @param attrs [Hash] Attributes to update
|
361
|
+
# @return [Boolean] Whether the update succeeded or not
|
362
|
+
def update_attributes(attrs = {})
|
363
|
+
self.attributes = attrs
|
364
|
+
save
|
365
|
+
end
|
366
|
+
|
367
|
+
# Alias to update_attributes
|
368
|
+
#
|
369
|
+
# @param attrs [Hash] Attributes to update
|
370
|
+
# @return [Boolean] Whether the update succeeded or not
|
371
|
+
def update(attrs = {})
|
372
|
+
update_attributes(attrs)
|
373
|
+
end
|
374
|
+
|
375
|
+
# Mark the record as persisted
|
376
|
+
def mark_as_persisted!
|
377
|
+
@persisted = true
|
378
|
+
end
|
379
|
+
|
380
|
+
# Whether or not this record has been persisted to the database previously
|
381
|
+
#
|
382
|
+
# @return [Boolean]
|
383
|
+
def persisted?
|
384
|
+
!!@persisted && has_attribute?(self.class.primary_key)
|
385
|
+
end
|
386
|
+
|
387
|
+
# Returns true if this is a new record (never persisted to the database)
|
388
|
+
#
|
389
|
+
# @return [Boolean]
|
390
|
+
def new_record?
|
391
|
+
!persisted?
|
59
392
|
end
|
60
393
|
|
61
|
-
#
|
62
|
-
# if the object is persisted or not.
|
63
|
-
# Returns nil if there are no key attributes.
|
394
|
+
# When we represent this resource as a relationship, we do so with id & type
|
64
395
|
#
|
65
|
-
#
|
66
|
-
def
|
67
|
-
|
396
|
+
# @return [Hash] Representation of this object as a relation
|
397
|
+
def as_relation
|
398
|
+
attributes.slice(:type, self.class.primary_key)
|
399
|
+
end
|
400
|
+
|
401
|
+
# When we represent this resource for serialization (create/update), we do so
|
402
|
+
# with this implementation
|
403
|
+
#
|
404
|
+
# @return [Hash] Representation of this object as JSONAPI object
|
405
|
+
def as_json_api(*)
|
406
|
+
attributes.slice(:id, :type).tap do |h|
|
407
|
+
relationships_for_serialization.tap do |r|
|
408
|
+
h[:relationships] = self.class.key_formatter.format_keys(r) unless r.empty?
|
409
|
+
end
|
410
|
+
h[:attributes] = self.class.key_formatter.format_keys(attributes_for_serialization)
|
411
|
+
end
|
412
|
+
end
|
413
|
+
|
414
|
+
def as_json(*)
|
415
|
+
attributes.slice(:id, :type).tap do |h|
|
416
|
+
relationships.as_json.tap do |r|
|
417
|
+
h[:relationships] = r unless r.empty?
|
418
|
+
end
|
419
|
+
h[:attributes] = attributes.except(:id, :type).as_json
|
420
|
+
end
|
421
|
+
end
|
422
|
+
|
423
|
+
# Mark all attributes for this record as dirty
|
424
|
+
def set_all_dirty!
|
425
|
+
set_all_attributes_dirty
|
426
|
+
relationships.set_all_attributes_dirty if relationships
|
68
427
|
end
|
69
428
|
|
70
|
-
|
429
|
+
def valid?(context = nil)
|
430
|
+
context ||= (new_record? ? :create : :update)
|
431
|
+
super(context)
|
432
|
+
end
|
433
|
+
|
434
|
+
# Commit the current changes to the resource to the remote server.
|
435
|
+
# If the resource was previously loaded from the server, we will
|
436
|
+
# try to update the record. Otherwise if it's a new record, then
|
437
|
+
# we will try to create it
|
438
|
+
#
|
439
|
+
# @return [Boolean] Whether or not the save succeeded
|
440
|
+
def save
|
441
|
+
return false unless valid?
|
442
|
+
|
443
|
+
self.last_result_set = if persisted?
|
444
|
+
self.class.requestor.update(self)
|
445
|
+
else
|
446
|
+
self.class.requestor.create(self)
|
447
|
+
end
|
71
448
|
|
72
|
-
|
73
|
-
|
449
|
+
if last_result_set.has_errors?
|
450
|
+
fill_errors
|
451
|
+
false
|
452
|
+
else
|
453
|
+
self.errors.clear if self.errors
|
454
|
+
mark_as_persisted!
|
455
|
+
if updated = last_result_set.first
|
456
|
+
self.attributes = updated.attributes
|
457
|
+
self.links.attributes = updated.links.attributes
|
458
|
+
self.relationships.attributes = updated.relationships.attributes
|
459
|
+
clear_changes_information
|
460
|
+
end
|
461
|
+
true
|
462
|
+
end
|
74
463
|
end
|
75
464
|
|
76
|
-
|
77
|
-
|
78
|
-
|
465
|
+
# Try to destroy this resource
|
466
|
+
#
|
467
|
+
# @return [Boolean] Whether or not the destroy succeeded
|
468
|
+
def destroy
|
469
|
+
self.last_result_set = self.class.requestor.destroy(self)
|
470
|
+
if last_result_set.has_errors?
|
471
|
+
fill_errors
|
472
|
+
false
|
79
473
|
else
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
474
|
+
self.attributes.clear
|
475
|
+
true
|
476
|
+
end
|
477
|
+
end
|
478
|
+
|
479
|
+
def inspect
|
480
|
+
"#<#{self.class.name}:@attributes=#{attributes.inspect}>"
|
481
|
+
end
|
482
|
+
|
483
|
+
protected
|
484
|
+
|
485
|
+
def method_missing(method, *args)
|
486
|
+
association = association_for(method)
|
487
|
+
|
488
|
+
return super unless association || (relationships && relationships.has_attribute?(method))
|
489
|
+
|
490
|
+
return nil unless relationship_definitions = relationships[method]
|
491
|
+
|
492
|
+
# look in included data
|
493
|
+
if relationship_definitions.key?("data")
|
494
|
+
# included.data_for returns an array, if the association is a has_one, then pick the first, otherise return the whole array
|
495
|
+
if association.is_a?(JSONAPI::Consumer::Associations::HasOne::Association)
|
496
|
+
return last_result_set.included.data_for(method, relationship_definitions).try(:first)
|
86
497
|
else
|
87
|
-
|
498
|
+
return last_result_set.included.data_for(method, relationship_definitions)
|
499
|
+
end
|
500
|
+
end
|
501
|
+
|
502
|
+
if association = association_for(method)
|
503
|
+
# look for a defined relationship url
|
504
|
+
if relationship_definitions["links"] && url = relationship_definitions["links"]["related"]
|
505
|
+
return association.data(url)
|
88
506
|
end
|
89
507
|
end
|
508
|
+
nil
|
509
|
+
end
|
510
|
+
|
511
|
+
def respond_to_missing?(symbol, include_all = false)
|
512
|
+
return true if relationships && relationships.has_attribute?(symbol)
|
513
|
+
return true if association_for(symbol)
|
514
|
+
super
|
515
|
+
end
|
516
|
+
|
517
|
+
def set_attribute(name, value)
|
518
|
+
property = property_for(name)
|
519
|
+
value = property.cast(value) if property
|
520
|
+
super(name, value)
|
521
|
+
end
|
522
|
+
|
523
|
+
def has_attribute?(attr_name)
|
524
|
+
!!property_for(attr_name) || super
|
525
|
+
end
|
526
|
+
|
527
|
+
def property_for(name)
|
528
|
+
self.class.schema.find(name)
|
529
|
+
end
|
530
|
+
|
531
|
+
def association_for(name)
|
532
|
+
self.class.associations.detect do |association|
|
533
|
+
association.attr_name.to_s == self.class.key_formatter.unformat(name)
|
534
|
+
end
|
535
|
+
end
|
536
|
+
|
537
|
+
def attributes_for_serialization
|
538
|
+
attributes.except(*self.class.read_only_attributes).slice(*changed)
|
539
|
+
end
|
540
|
+
|
541
|
+
def relationships_for_serialization
|
542
|
+
relationships.as_json_api
|
543
|
+
end
|
544
|
+
|
545
|
+
def fill_errors
|
546
|
+
last_result_set.errors.each do |error|
|
547
|
+
key = self.class.key_formatter.unformat(error.error_key)
|
548
|
+
errors.add(key, error.error_msg)
|
549
|
+
end
|
90
550
|
end
|
91
551
|
end
|
92
552
|
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
module JSONAPI::Consumer
|
4
|
+
class ResultSet < Array
|
5
|
+
extend Forwardable
|
6
|
+
|
7
|
+
attr_accessor :errors,
|
8
|
+
:record_class,
|
9
|
+
:meta,
|
10
|
+
:pages,
|
11
|
+
:uri,
|
12
|
+
:links,
|
13
|
+
:implementation,
|
14
|
+
:relationships,
|
15
|
+
:included
|
16
|
+
|
17
|
+
# pagination methods are handled by the paginator
|
18
|
+
def_delegators :pages, :total_pages, :total_entries, :total_count, :offset, :per_page, :current_page, :limit_value, :next_page, :previous_page, :out_of_bounds?
|
19
|
+
|
20
|
+
def has_errors?
|
21
|
+
errors.present?
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|