api_resource 0.6.18 → 0.6.19
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/Gemfile.lock +4 -8
- data/Guardfile +5 -17
- data/api_resource.gemspec +4 -7
- data/lib/api_resource.rb +20 -19
- data/lib/api_resource/associations.rb +39 -23
- data/lib/api_resource/associations/association_proxy.rb +14 -13
- data/lib/api_resource/attributes.rb +555 -156
- data/lib/api_resource/base.rb +376 -305
- data/lib/api_resource/connection.rb +22 -12
- data/lib/api_resource/finders.rb +17 -18
- data/lib/api_resource/finders/single_finder.rb +1 -1
- data/lib/api_resource/mocks.rb +37 -31
- data/lib/api_resource/scopes.rb +70 -12
- data/lib/api_resource/serializer.rb +264 -0
- data/lib/api_resource/typecast.rb +13 -2
- data/lib/api_resource/typecasters/unknown_typecaster.rb +33 -0
- data/lib/api_resource/version.rb +1 -1
- data/spec/lib/associations/has_many_remote_object_proxy_spec.rb +3 -3
- data/spec/lib/associations_spec.rb +49 -94
- data/spec/lib/attributes_spec.rb +40 -56
- data/spec/lib/base_spec.rb +290 -382
- data/spec/lib/callbacks_spec.rb +6 -6
- data/spec/lib/connection_spec.rb +20 -20
- data/spec/lib/finders_spec.rb +14 -0
- data/spec/lib/mocks_spec.rb +9 -9
- data/spec/lib/prefixes_spec.rb +4 -5
- data/spec/lib/scopes_spec.rb +98 -0
- data/spec/lib/serializer_spec.rb +156 -0
- data/spec/spec_helper.rb +1 -4
- data/spec/support/test_resource.rb +1 -1
- metadata +14 -38
- data/spec/tmp/DIR +0 -0
data/lib/api_resource/base.rb
CHANGED
@@ -4,7 +4,7 @@ require 'active_support/core_ext'
|
|
4
4
|
require 'active_support/string_inquirer'
|
5
5
|
|
6
6
|
module ApiResource
|
7
|
-
|
7
|
+
|
8
8
|
class Base
|
9
9
|
|
10
10
|
# TODO: There's way too much in this class as it stands, some glaring problems:
|
@@ -30,35 +30,84 @@ module ApiResource
|
|
30
30
|
# => 6) Implement an IdentityMap
|
31
31
|
# => 7) Write documentation
|
32
32
|
# => 8) Write Examples
|
33
|
-
|
34
|
-
class_attribute :site, :proxy, :user, :password, :auth_type, :format,
|
33
|
+
|
34
|
+
class_attribute :site, :proxy, :user, :password, :auth_type, :format,
|
35
35
|
:timeout, :open_timeout, :ssl_options, :token, :ttl
|
36
|
-
|
37
36
|
|
38
37
|
class_attribute :include_root_in_json
|
39
38
|
self.include_root_in_json = true
|
40
|
-
|
39
|
+
|
41
40
|
class_attribute :include_nil_attributes_on_create
|
42
41
|
self.include_nil_attributes_on_create = false
|
43
|
-
|
42
|
+
|
44
43
|
class_attribute :include_all_attributes_on_update
|
45
44
|
self.include_nil_attributes_on_create = false
|
46
45
|
|
47
46
|
class_attribute :format
|
48
47
|
self.format = ApiResource::Formats::JsonFormat
|
49
|
-
|
50
|
-
class_attribute :primary_key
|
51
|
-
self.primary_key = "id"
|
52
48
|
|
53
|
-
|
54
|
-
|
49
|
+
class_attribute :resource_definition_mutex
|
50
|
+
self.resource_definition_mutex = Mutex.new
|
51
|
+
|
52
|
+
delegate :logger, to: ApiResource
|
53
|
+
|
55
54
|
class << self
|
56
|
-
|
57
|
-
# writers - accessors with defaults were not working
|
58
|
-
attr_writer :element_name, :collection_name
|
59
55
|
|
60
|
-
|
61
|
-
|
56
|
+
# @!attribute [w] collection_name
|
57
|
+
# @return [String]
|
58
|
+
attr_writer :collection_name
|
59
|
+
|
60
|
+
# @!attribute [w] element_name
|
61
|
+
# @return [String]
|
62
|
+
attr_writer :element_name
|
63
|
+
|
64
|
+
delegate :logger,
|
65
|
+
to: ApiResource
|
66
|
+
|
67
|
+
#
|
68
|
+
# Accessor for the connection
|
69
|
+
#
|
70
|
+
# @param refresh = false [Boolean] Whether to reconnect
|
71
|
+
#
|
72
|
+
# @return [Connection]
|
73
|
+
def connection(refresh = false)
|
74
|
+
if refresh || @connection.nil?
|
75
|
+
@connection = Connection.new(self.site, self.format, self.headers)
|
76
|
+
end
|
77
|
+
@connection.timeout = self.timeout
|
78
|
+
@connection
|
79
|
+
end
|
80
|
+
|
81
|
+
#
|
82
|
+
# Handles the setting of format to a MimeType
|
83
|
+
#
|
84
|
+
# @param mime_type_or_format [Symbol, MimeType]
|
85
|
+
#
|
86
|
+
# @return [MimeType] The new MimeType
|
87
|
+
def format_with_mimetype_or_format_set=(mime_type_or_format)
|
88
|
+
if mime_type_or_format.is_a?(Symbol)
|
89
|
+
format = ApiResource::Formats[mime_type_or_format]
|
90
|
+
else
|
91
|
+
format = mime_type_or_format
|
92
|
+
end
|
93
|
+
self.format_without_mimetype_or_format_set = format
|
94
|
+
if self.site
|
95
|
+
self.connection.format = format
|
96
|
+
end
|
97
|
+
format
|
98
|
+
end
|
99
|
+
alias_method_chain :format=, :mimetype_or_format_set
|
100
|
+
|
101
|
+
#
|
102
|
+
# Reader for headers
|
103
|
+
#
|
104
|
+
# @return [Hash] Headers for requests
|
105
|
+
def headers
|
106
|
+
{}.tap do |ret|
|
107
|
+
ret['Lifebooker-Token'] = self.token if self.token.present?
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
62
111
|
def inherited(klass)
|
63
112
|
# Call the methods of the superclass to make sure inheritable accessors and the like have been inherited
|
64
113
|
super
|
@@ -73,97 +122,71 @@ module ApiResource
|
|
73
122
|
true
|
74
123
|
end
|
75
124
|
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
#
|
81
|
-
def
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
end
|
98
|
-
# Then scopes
|
99
|
-
if resource_definition["scopes"]
|
100
|
-
resource_definition["scopes"].each_pair do |scope_name, opts|
|
101
|
-
self.scope(scope_name, opts)
|
102
|
-
end
|
103
|
-
end
|
104
|
-
# Then associations
|
105
|
-
if resource_definition["associations"]
|
106
|
-
resource_definition["associations"].each_pair do |key, hash|
|
107
|
-
hash.each_pair do |assoc_name, assoc_options|
|
108
|
-
self.send(key, assoc_name, assoc_options)
|
109
|
-
end
|
110
|
-
end
|
111
|
-
end
|
112
|
-
|
113
|
-
# This is provided by ActiveModel::AttributeMethods, it should
|
114
|
-
# define the basic methods but we need to override all the setters
|
115
|
-
# so we do dirty tracking
|
116
|
-
attrs = []
|
117
|
-
if resource_definition["attributes"] && resource_definition["attributes"]["public"]
|
118
|
-
attrs += resource_definition["attributes"]["public"].collect{|v|
|
119
|
-
v.is_a?(Array) ? v.first : v
|
120
|
-
}.flatten
|
121
|
-
end
|
122
|
-
if resource_definition["associations"]
|
123
|
-
attrs += resource_definition["associations"].values.collect(&:keys).flatten
|
124
|
-
end
|
125
|
-
|
126
|
-
# Swallow up any loading errors because the site may be incorrect
|
127
|
-
rescue Exception => e
|
128
|
-
if ApiResource.raise_missing_definition_error
|
129
|
-
raise e
|
125
|
+
#
|
126
|
+
# Explicit call to load the resource definition
|
127
|
+
#
|
128
|
+
# @return [Boolean] True if we loaded it, false if it was already
|
129
|
+
# loaded
|
130
|
+
def load_resource_definition
|
131
|
+
unless instance_variable_defined?(:@resource_definition)
|
132
|
+
# Lock the mutex to make sure only one thread does
|
133
|
+
# this at a time
|
134
|
+
self.resource_definition_mutex.synchronize do
|
135
|
+
# once we have the lock, check to make sure the resource
|
136
|
+
# definition wasn't fetched while we were sleeping
|
137
|
+
return true if instance_variable_defined?(:@resource_definition)
|
138
|
+
# the last time we checked
|
139
|
+
@resource_load_time = Time.now
|
140
|
+
|
141
|
+
# set to not nil so we don't get an infinite loop
|
142
|
+
@resource_definition = true
|
143
|
+
self.set_class_attributes_upon_load
|
144
|
+
return true
|
130
145
|
end
|
131
|
-
ApiResource.logger.warn(
|
132
|
-
"#{self} accessing #{self.new_element_path}"
|
133
|
-
)
|
134
|
-
ApiResource.logger.warn(
|
135
|
-
"#{self}: #{e.message[0..60].gsub(/[\n\r]/, '')} ...\n"
|
136
|
-
)
|
137
|
-
ApiResource.logger.debug(e.backtrace.pretty_inspect)
|
138
|
-
return e.respond_to?(:request) ? e.request : nil
|
139
146
|
end
|
147
|
+
# we didn't do anything
|
148
|
+
false
|
140
149
|
end
|
141
|
-
|
142
|
-
|
143
|
-
|
150
|
+
|
151
|
+
#
|
152
|
+
# Set the open timeout on the connection and connect
|
153
|
+
#
|
154
|
+
# @param timeout [Fixnum] Open timeout in number of seconds
|
155
|
+
#
|
156
|
+
# @return [Fixnum] The timeout
|
157
|
+
def open_timeout_with_connection_reset=(timeout)
|
158
|
+
@connection = nil
|
159
|
+
self.open_timeout_without_connection_reset = timeout
|
144
160
|
end
|
161
|
+
alias_method_chain :open_timeout=, :connection_reset
|
145
162
|
|
146
|
-
#
|
147
|
-
#
|
148
|
-
|
149
|
-
|
150
|
-
|
163
|
+
#
|
164
|
+
# Prefix for the resource path
|
165
|
+
#
|
166
|
+
# @todo Are the options used?
|
167
|
+
#
|
168
|
+
# @param options = {} [Hash] Options
|
169
|
+
#
|
170
|
+
# @return [String] Collection prefix
|
171
|
+
def prefix(options = {})
|
172
|
+
default = (self.site ? self.site.path : '/')
|
173
|
+
default << '/' unless default[-1..-1] == '/'
|
174
|
+
self.prefix = default
|
175
|
+
prefix(options)
|
151
176
|
end
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
# set to not nil so we don't get an infinite loop
|
159
|
-
@resource_definition = true
|
160
|
-
self.set_class_attributes_upon_load
|
161
|
-
return true
|
162
|
-
end
|
163
|
-
# we didn't do anything
|
164
|
-
false
|
177
|
+
|
178
|
+
#
|
179
|
+
# @todo Not sure what this does
|
180
|
+
def prefix_source
|
181
|
+
prefix
|
182
|
+
prefix_source
|
165
183
|
end
|
166
184
|
|
185
|
+
#
|
186
|
+
# Clear the old resource definition and reload it from the
|
187
|
+
# server
|
188
|
+
#
|
189
|
+
# @return [Boolean] True if it loaded
|
167
190
|
def reload_resource_definition
|
168
191
|
# clear the public_attribute_names, protected_attribute_names
|
169
192
|
if instance_variable_defined?(:@resource_definition)
|
@@ -175,22 +198,71 @@ module ApiResource
|
|
175
198
|
end
|
176
199
|
# backwards compatibility
|
177
200
|
alias_method :reload_class_attributes, :reload_resource_definition
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
201
|
+
|
202
|
+
#
|
203
|
+
# Reset our connection instance so that we will reconnect the
|
204
|
+
# next time we need it
|
205
|
+
#
|
206
|
+
# @return [Boolean] true
|
207
|
+
def reset_connection
|
208
|
+
remove_instance_variable(:@connection) if @connection.present?
|
209
|
+
true
|
210
|
+
end
|
211
|
+
|
212
|
+
#
|
213
|
+
# Reader for the resource_definition
|
214
|
+
#
|
215
|
+
# @return [Hash, nil] Our stored resource definition
|
216
|
+
def resource_definition
|
217
|
+
@resource_definition
|
218
|
+
end
|
219
|
+
|
220
|
+
#
|
221
|
+
# Load our resource definition to make sure we know what this class
|
222
|
+
# responds to
|
223
|
+
#
|
224
|
+
# @return [Boolean] Whether or not it responss
|
225
|
+
def respond_to?(*args)
|
226
|
+
self.load_resource_definition
|
227
|
+
super
|
228
|
+
end
|
229
|
+
|
230
|
+
#
|
231
|
+
# This makes a request to new_element_path and sets up the correct
|
232
|
+
# attribute, scope and association methods for this class
|
233
|
+
#
|
234
|
+
# @return [Boolean] true
|
235
|
+
def set_class_attributes_upon_load
|
236
|
+
# this only happens in subclasses
|
237
|
+
return true if self == ApiResource::Base
|
238
|
+
begin
|
239
|
+
@resource_definition = self.connection.get(
|
240
|
+
self.new_element_path, self.headers
|
241
|
+
)
|
242
|
+
# set up methods derived from our class definition
|
243
|
+
self.define_all_attributes
|
244
|
+
self.define_all_scopes
|
245
|
+
self.define_all_associations
|
246
|
+
|
247
|
+
# Swallow up any loading errors because the site may be incorrect
|
248
|
+
rescue Exception => e
|
249
|
+
self.handle_resource_definition_error(e)
|
184
250
|
end
|
251
|
+
true
|
185
252
|
end
|
186
|
-
|
187
|
-
alias_method_chain :token=, :new_token_set
|
188
253
|
|
254
|
+
#
|
255
|
+
# Handles the setting of site while reloading the resource
|
256
|
+
# definition to ensure we have the latest definition
|
257
|
+
#
|
258
|
+
# @param site [String] URL of the site
|
259
|
+
#
|
260
|
+
# @return [String] The newly set site
|
189
261
|
def site_with_connection_reset=(site)
|
190
262
|
# store so we can reload attributes if the site changed
|
191
263
|
old_site = self.site.to_s.clone
|
192
264
|
@connection = nil
|
193
|
-
|
265
|
+
|
194
266
|
if site.nil?
|
195
267
|
self.site_without_connection_reset = nil
|
196
268
|
# no site, so we'll skip the reload
|
@@ -198,73 +270,56 @@ module ApiResource
|
|
198
270
|
else
|
199
271
|
self.site_without_connection_reset = create_site_uri_from(site)
|
200
272
|
end
|
201
|
-
|
273
|
+
|
202
274
|
# reset class attributes and try to reload them if the site changed
|
203
275
|
unless self.site.to_s == old_site
|
204
276
|
self.reload_resource_definition
|
205
277
|
end
|
206
|
-
|
278
|
+
|
207
279
|
return site
|
208
280
|
end
|
209
|
-
|
210
281
|
alias_method_chain :site=, :connection_reset
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
alias_method_chain :format=, :mimetype_or_format_set
|
220
|
-
|
282
|
+
|
283
|
+
#
|
284
|
+
# Set the timeout on the connection and connect
|
285
|
+
#
|
286
|
+
# @param timeout [Fixnum] Timeout in number of seconds
|
287
|
+
#
|
288
|
+
# @return [Fixnum] The timeout
|
221
289
|
def timeout_with_connection_reset=(timeout)
|
222
290
|
@connection = nil
|
223
291
|
self.timeout_without_connection_reset = timeout
|
224
292
|
end
|
225
|
-
|
226
293
|
alias_method_chain :timeout=, :connection_reset
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
end
|
240
|
-
|
241
|
-
def headers
|
242
|
-
{}.tap do |ret|
|
243
|
-
ret['Lifebooker-Token'] = self.token if self.token.present?
|
294
|
+
|
295
|
+
#
|
296
|
+
# Handles the setting of tokens on descendants
|
297
|
+
#
|
298
|
+
# @param new_token [String] New token string
|
299
|
+
#
|
300
|
+
# @return [String] The token that was set
|
301
|
+
def token_with_new_token_set=(new_token)
|
302
|
+
self.token_without_new_token_set = new_token
|
303
|
+
self.connection(true)
|
304
|
+
self.descendants.each do |child|
|
305
|
+
child.send(:token=, new_token)
|
244
306
|
end
|
307
|
+
new_token
|
245
308
|
end
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
self.prefix = default
|
251
|
-
prefix(options)
|
252
|
-
end
|
253
|
-
|
254
|
-
def prefix_source
|
255
|
-
prefix
|
256
|
-
prefix_source
|
257
|
-
end
|
309
|
+
alias_method_chain :token=, :new_token_set
|
310
|
+
|
311
|
+
|
312
|
+
|
258
313
|
|
259
314
|
def prefix=(value = '/')
|
260
|
-
prefix_call = value.gsub(/:\w+/) { |key|
|
315
|
+
prefix_call = value.gsub(/:\w+/) { |key|
|
261
316
|
"\#{URI.escape options[#{key}].to_s}"
|
262
317
|
}
|
263
318
|
@prefix_parameters = nil
|
264
319
|
silence_warnings do
|
265
320
|
instance_eval <<-EOE, __FILE__, __LINE__ + 1
|
266
321
|
def prefix_source() "#{value}" end
|
267
|
-
def prefix(options={})
|
322
|
+
def prefix(options={})
|
268
323
|
ret = "#{prefix_call}"
|
269
324
|
ret =~ Regexp.new(Regexp.escape("//")) ? "/" : ret
|
270
325
|
end
|
@@ -274,7 +329,7 @@ module ApiResource
|
|
274
329
|
logger.error "Couldn't set prefix: #{e}\n #{code}" if logger
|
275
330
|
raise
|
276
331
|
end
|
277
|
-
|
332
|
+
|
278
333
|
# element_name with default
|
279
334
|
def element_name
|
280
335
|
@element_name ||= self.model_name.element
|
@@ -283,11 +338,11 @@ module ApiResource
|
|
283
338
|
def collection_name
|
284
339
|
@collection_name ||= ActiveSupport::Inflector.pluralize(self.element_name)
|
285
340
|
end
|
286
|
-
|
341
|
+
|
287
342
|
# alias_method :set_prefix, :prefix=
|
288
343
|
# alias_method :set_element_name, :element_name=
|
289
344
|
# alias_method :set_collection_name, :collection_name=
|
290
|
-
|
345
|
+
|
291
346
|
def element_path(id, prefix_options = {}, query_options = nil)
|
292
347
|
prefix_options, query_options = split_options(prefix_options) if query_options.nil?
|
293
348
|
|
@@ -300,27 +355,27 @@ module ApiResource
|
|
300
355
|
"#{prefix(prefix_options)}#{collection_name}/#{URI.escape id.to_s}.#{format.extension}#{query_string(query_options)}"
|
301
356
|
end
|
302
357
|
end
|
303
|
-
|
304
|
-
# path to find
|
358
|
+
|
359
|
+
# path to find
|
305
360
|
def new_element_path(prefix_options = {})
|
306
361
|
File.join(
|
307
|
-
self.prefix(prefix_options),
|
308
|
-
self.collection_name,
|
362
|
+
self.prefix(prefix_options),
|
363
|
+
self.collection_name,
|
309
364
|
"new.#{format.extension}"
|
310
365
|
)
|
311
366
|
end
|
312
|
-
|
367
|
+
|
313
368
|
def collection_path(prefix_options = {}, query_options = nil)
|
314
369
|
prefix_options, query_options = split_options(prefix_options) if query_options.nil?
|
315
370
|
|
316
371
|
# Fall back on this rather than search without the id
|
317
372
|
"#{prefix(prefix_options)}#{collection_name}.#{format.extension}#{query_string(query_options)}"
|
318
373
|
end
|
319
|
-
|
374
|
+
|
320
375
|
def build(attributes = {})
|
321
376
|
self.new(attributes)
|
322
377
|
end
|
323
|
-
|
378
|
+
|
324
379
|
def create(attributes = {})
|
325
380
|
self.new(attributes).tap{ |resource| resource.save }
|
326
381
|
end
|
@@ -334,7 +389,7 @@ module ApiResource
|
|
334
389
|
# ==== Examples
|
335
390
|
# Event.delete(2) # sends DELETE /events/2
|
336
391
|
#
|
337
|
-
# Event.create(:
|
392
|
+
# Event.create(name: 'Free Concert', location: 'Community Center')
|
338
393
|
# my_event = Event.find(:first) # let's assume this is event with ID 7
|
339
394
|
# Event.delete(my_event.id) # sends DELETE /events/7
|
340
395
|
#
|
@@ -368,6 +423,32 @@ module ApiResource
|
|
368
423
|
|
369
424
|
protected
|
370
425
|
|
426
|
+
#
|
427
|
+
# Handle any errors raised during the resource definition
|
428
|
+
# find
|
429
|
+
#
|
430
|
+
# @param e [Exception] Exception thrown
|
431
|
+
#
|
432
|
+
# @raise [Exception] Re-raised if
|
433
|
+
# ApiResource.raise_missing_definition_error is true
|
434
|
+
#
|
435
|
+
# @return [ApiResource::Request, nil] The Request associated with
|
436
|
+
# this error or nil if there is no request and the error came from
|
437
|
+
# something else
|
438
|
+
def handle_resource_definition_error(e)
|
439
|
+
if ApiResource.raise_missing_definition_error
|
440
|
+
raise e
|
441
|
+
end
|
442
|
+
ApiResource.logger.warn(
|
443
|
+
"#{self} accessing #{self.new_element_path}"
|
444
|
+
)
|
445
|
+
ApiResource.logger.warn(
|
446
|
+
"#{self}: #{e.message[0..60].gsub(/[\n\r]/, '')} ...\n"
|
447
|
+
)
|
448
|
+
ApiResource.logger.debug(e.backtrace.pretty_inspect)
|
449
|
+
return e.respond_to?(:request) ? e.request : nil
|
450
|
+
end
|
451
|
+
|
371
452
|
def method_missing(meth, *args, &block)
|
372
453
|
# make one attempt to load remote attrs
|
373
454
|
if self.resource_definition_is_invalid?
|
@@ -380,7 +461,7 @@ module ApiResource
|
|
380
461
|
super
|
381
462
|
end
|
382
463
|
end
|
383
|
-
|
464
|
+
|
384
465
|
private
|
385
466
|
|
386
467
|
# Accepts a URI and creates the site URI from that.
|
@@ -406,86 +487,86 @@ module ApiResource
|
|
406
487
|
def uri_parser
|
407
488
|
@uri_parser ||= URI.const_defined?(:Parser) ? URI::Parser.new : URI
|
408
489
|
end
|
409
|
-
|
490
|
+
|
410
491
|
end
|
411
|
-
|
492
|
+
|
412
493
|
def initialize(attributes = {})
|
413
494
|
# call super's initialize to set up any variables that we need
|
414
495
|
super(attributes)
|
415
496
|
# if we initialize this class, load the attributes
|
416
497
|
self.class.load_resource_definition
|
417
|
-
# Now we can make a call to setup the inheriting
|
498
|
+
# Now we can make a call to setup the inheriting
|
418
499
|
# klass with its attributes
|
419
500
|
self.attributes = attributes
|
420
501
|
end
|
421
|
-
|
502
|
+
|
422
503
|
def new?
|
423
504
|
id.blank?
|
424
505
|
end
|
425
506
|
alias :new_record? :new?
|
426
|
-
|
507
|
+
|
427
508
|
def persisted?
|
428
509
|
!new?
|
429
510
|
end
|
430
|
-
|
511
|
+
|
431
512
|
def id
|
432
513
|
self.read_attribute(self.class.primary_key)
|
433
514
|
end
|
434
|
-
|
515
|
+
|
435
516
|
# Bypass dirty tracking for this field
|
436
517
|
def id=(id)
|
437
518
|
@attributes[self.class.primary_key] = id
|
438
519
|
end
|
439
|
-
|
520
|
+
|
440
521
|
def ==(other)
|
441
522
|
other.equal?(self) || (other.instance_of?(self.class) && other.id == self.id)
|
442
523
|
end
|
443
|
-
|
524
|
+
|
444
525
|
def eql?(other)
|
445
526
|
self == other
|
446
527
|
end
|
447
|
-
|
528
|
+
|
448
529
|
def hash
|
449
530
|
id.hash
|
450
531
|
end
|
451
|
-
|
532
|
+
|
452
533
|
def dup
|
453
534
|
self.class.instantiate_record(self.attributes)
|
454
535
|
end
|
455
|
-
|
536
|
+
|
456
537
|
def update_attributes(attrs)
|
457
538
|
self.attributes = attrs
|
458
539
|
self.save
|
459
540
|
end
|
460
|
-
|
541
|
+
|
461
542
|
def save(*args)
|
462
543
|
new? ? create(*args) : update(*args)
|
463
544
|
end
|
464
|
-
|
545
|
+
|
465
546
|
def save!(*args)
|
466
547
|
save(*args) || raise(ApiResource::ResourceInvalid.new(self))
|
467
548
|
end
|
468
|
-
|
549
|
+
|
469
550
|
def destroy
|
470
551
|
connection.delete(element_path(self.id), self.class.headers)
|
471
552
|
end
|
472
|
-
|
553
|
+
|
473
554
|
def encode(options = {})
|
474
555
|
self.send("to_#{self.class.format.extension}", options)
|
475
556
|
end
|
476
|
-
|
557
|
+
|
477
558
|
def reload
|
478
559
|
# find the record from the remote service
|
479
560
|
reloaded = self.class.find(self.id)
|
480
|
-
|
561
|
+
|
481
562
|
# clear out the attributes cache
|
482
563
|
@attributes_cache = HashWithIndifferentAccess.new
|
483
564
|
# set up our attributes cache on our record
|
484
565
|
@attributes = reloaded.instance_variable_get(:@attributes)
|
485
|
-
|
566
|
+
|
486
567
|
reloaded
|
487
568
|
end
|
488
|
-
|
569
|
+
|
489
570
|
def to_param
|
490
571
|
# Stolen from active_record.
|
491
572
|
# We can't use alias_method here, because method 'id' optimizes itself on the fly.
|
@@ -505,92 +586,50 @@ module ApiResource
|
|
505
586
|
return [] unless self.class.prefix_source =~ /\:/
|
506
587
|
self.class.prefix_source.scan(/\:(\w+)/).collect{|match| match.first.to_sym}
|
507
588
|
end
|
508
|
-
|
589
|
+
|
509
590
|
# Override to_s and inspect so they only show attributes
|
510
591
|
# and not associations, this prevents force loading of associations
|
511
|
-
# when we call to_s or inspect on a descendent of base but allows it if we
|
592
|
+
# when we call to_s or inspect on a descendent of base but allows it if we
|
512
593
|
# try to evaluate an association directly
|
513
594
|
def to_s
|
514
595
|
return "#<#{self.class}:#{(self.object_id * 2).to_s(16)} @attributes=#{self.attributes}"
|
515
596
|
end
|
516
597
|
alias_method :inspect, :to_s
|
517
|
-
|
598
|
+
|
518
599
|
# Methods for serialization as json or xml, relying on the serializable_hash method
|
519
600
|
def to_xml(options = {})
|
520
|
-
self.serializable_hash(options).to_xml(:
|
601
|
+
self.serializable_hash(options).to_xml(root: self.class.element_name)
|
521
602
|
end
|
522
|
-
|
603
|
+
|
523
604
|
def to_json(options = {})
|
524
|
-
|
605
|
+
# handle whether or not we include root in our JSON
|
606
|
+
if self.class.include_root_in_json
|
607
|
+
ret = {
|
608
|
+
self.class.element_name => self.serializable_hash(options)
|
609
|
+
}
|
610
|
+
else
|
611
|
+
ret = self.serializable_hash(options)
|
612
|
+
end
|
613
|
+
ret.to_json
|
525
614
|
end
|
526
|
-
|
527
|
-
# TODO:
|
615
|
+
|
616
|
+
# TODO: (Updated 10/26/2013):
|
617
|
+
# Leaving this old message here though the behavior is now in Serializer.
|
618
|
+
# Any changes should be done there
|
619
|
+
#
|
620
|
+
# this method needs to change seriously to fit in with the
|
528
621
|
# new typecasting scheme, it should call self.outgoing_attributes which
|
529
622
|
# should return the converted versions after calling to_api, that should
|
530
623
|
# be implemented in the attributes module though
|
531
624
|
def serializable_hash(options = {})
|
532
|
-
|
533
|
-
action = options[:action]
|
534
|
-
|
535
|
-
include_nil_attributes = options[:include_nil_attributes]
|
536
|
-
|
537
|
-
options[:include_associations] = options[:include_associations] ? options[:include_associations].symbolize_array : self.changes.keys.symbolize_array.select{|k| self.association?(k)}
|
538
|
-
|
539
|
-
options[:include_extras] = options[:include_extras] ? options[:include_extras].symbolize_array : []
|
540
|
-
|
541
|
-
options[:except] ||= []
|
542
|
-
|
543
|
-
ret = self.attributes.inject({}) do |accum, (key,val)|
|
544
|
-
# If this is an association and it's in include_associations then include it
|
545
|
-
if options[:include_extras].include?(key.to_sym)
|
546
|
-
accum.merge(key => val)
|
547
|
-
elsif options[:except].include?(key.to_sym)
|
548
|
-
accum
|
549
|
-
# this attribute is already accounted for in the URL
|
550
|
-
elsif self.prefix_attribute_names.include?(key.to_sym)
|
551
|
-
accum
|
552
|
-
elsif(!include_nil_attributes && val.nil? && self.changes[key].blank?)
|
553
|
-
accum
|
554
|
-
else
|
555
|
-
!self.attribute?(key) || self.protected_attribute?(key) ? accum : accum.merge(key => val)
|
556
|
-
end
|
557
|
-
end
|
558
|
-
|
559
|
-
# also add in the _id fields that are changed
|
560
|
-
ret = self.association_names.inject(ret) do |accum, assoc_name|
|
561
|
-
|
562
|
-
# get the id method for the association
|
563
|
-
id_method = self.class.association_foreign_key_field(assoc_name)
|
564
|
-
|
565
|
-
# only do this if they are not prefix_attribute_names
|
566
|
-
# and we have changes
|
567
|
-
if !self.prefix_attribute_names.include?(id_method.to_sym) &&
|
568
|
-
self.changes[id_method].present?
|
569
|
-
|
570
|
-
accum[id_method] = self.changes[id_method].last
|
571
|
-
end
|
572
|
-
accum
|
573
|
-
end
|
574
|
-
|
575
|
-
options[:include_associations].each do |assoc|
|
576
|
-
if self.association?(assoc)
|
577
|
-
ret[assoc] = self.send(assoc).serializable_hash({
|
578
|
-
:include_id => true,
|
579
|
-
:include_nil_attributes => include_nil_attributes,
|
580
|
-
:action => action
|
581
|
-
})
|
582
|
-
end
|
583
|
-
end
|
584
|
-
# include id - this is for nested updates
|
585
|
-
ret[:id] = self.id if options[:include_id] && !self.new?
|
586
|
-
ret
|
625
|
+
return Serializer.new(self, options).to_hash
|
587
626
|
end
|
588
|
-
|
627
|
+
|
589
628
|
protected
|
590
629
|
def connection(refresh = false)
|
591
630
|
self.class.connection(refresh)
|
592
631
|
end
|
593
|
-
|
632
|
+
|
594
633
|
def load_attributes_from_response(response)
|
595
634
|
if response.present?
|
596
635
|
@attributes_cache = {}
|
@@ -601,27 +640,27 @@ module ApiResource
|
|
601
640
|
response
|
602
641
|
end
|
603
642
|
|
604
|
-
def method_missing(meth, *args, &block)
|
605
|
-
|
606
|
-
|
607
|
-
|
608
|
-
|
609
|
-
|
610
|
-
|
611
|
-
|
612
|
-
|
613
|
-
|
614
|
-
|
615
|
-
end
|
616
|
-
|
643
|
+
# def method_missing(meth, *args, &block)
|
644
|
+
# # make one attempt to load remote attrs
|
645
|
+
# if self.class.resource_definition_is_invalid?
|
646
|
+
# self.class.reload_resource_definition
|
647
|
+
# end
|
648
|
+
# # see if we respond to the method now
|
649
|
+
# if self.respond_to?(meth)
|
650
|
+
# return self.send(meth, *args, &block)
|
651
|
+
# else
|
652
|
+
# super
|
653
|
+
# end
|
654
|
+
# end
|
655
|
+
|
617
656
|
def element_path(id, prefix_override_options = {}, query_options = nil)
|
618
657
|
self.class.element_path(
|
619
|
-
id,
|
620
|
-
self.prefix_options.merge(prefix_override_options),
|
658
|
+
id,
|
659
|
+
self.prefix_options.merge(prefix_override_options),
|
621
660
|
query_options
|
622
661
|
)
|
623
662
|
end
|
624
|
-
|
663
|
+
|
625
664
|
# list of all attributes that are not nil
|
626
665
|
def nil_attributes
|
627
666
|
self.attributes.select{|k,v|
|
@@ -634,76 +673,108 @@ module ApiResource
|
|
634
673
|
def new_element_path(prefix_options = {})
|
635
674
|
self.class.new_element_path(prefix_options)
|
636
675
|
end
|
637
|
-
|
676
|
+
|
638
677
|
def collection_path(override_prefix_options = {},query_options = nil)
|
639
678
|
self.class.collection_path(
|
640
|
-
self.prefix_options.merge(override_prefix_options),
|
679
|
+
self.prefix_options.merge(override_prefix_options),
|
641
680
|
query_options
|
642
681
|
)
|
643
682
|
end
|
644
|
-
|
683
|
+
|
684
|
+
#
|
685
|
+
# Create a new record
|
686
|
+
# @param *args [type] [description]
|
687
|
+
#
|
688
|
+
# @return [type] [description]
|
645
689
|
def create(*args)
|
646
|
-
|
647
|
-
|
690
|
+
path = self.collection_path
|
691
|
+
body = self.setup_create_call(*args)
|
692
|
+
headers = self.class.headers
|
693
|
+
# make the post call
|
694
|
+
connection.post(path, body, headers).tap do |response|
|
648
695
|
load_attributes_from_response(response)
|
649
696
|
end
|
650
697
|
end
|
651
698
|
|
699
|
+
#
|
700
|
+
# Helper method to set up a call to create
|
701
|
+
#
|
702
|
+
# @param *args [type] [description]
|
703
|
+
#
|
704
|
+
# @return [type] [description]
|
652
705
|
def setup_create_call(*args)
|
653
706
|
opts = args.extract_options!
|
654
|
-
|
655
|
-
|
656
|
-
|
657
|
-
|
658
|
-
|
659
|
-
|
660
|
-
|
661
|
-
|
662
|
-
|
663
|
-
body = RestClient::Payload.has_file?(self.attributes) ? self.serializable_hash(opts) : encode(opts)
|
664
|
-
end
|
665
|
-
|
666
|
-
|
707
|
+
|
708
|
+
# handle nil attributes
|
709
|
+
opts[:include_nil_attributes] = self.include_nil_attributes_on_create
|
710
|
+
|
711
|
+
# more generic setup_save_call
|
712
|
+
self.setup_save_call(args, opts)
|
713
|
+
end
|
714
|
+
|
715
|
+
|
667
716
|
def update(*args)
|
668
|
-
|
717
|
+
path = self.element_path(self.id)
|
718
|
+
body = self.setup_update_call(*args)
|
719
|
+
headers = self.class.headers
|
669
720
|
# We can just ignore the response
|
670
|
-
connection.put(
|
721
|
+
connection.put(path, body, headers).tap do |response|
|
671
722
|
load_attributes_from_response(response)
|
672
723
|
end
|
673
724
|
end
|
674
725
|
|
675
726
|
def setup_update_call(*args)
|
676
|
-
|
727
|
+
options = args.extract_options!
|
728
|
+
|
677
729
|
# When we create we should not include any blank attributes
|
678
|
-
|
679
|
-
|
680
|
-
|
681
|
-
|
682
|
-
|
683
|
-
|
684
|
-
|
685
|
-
|
686
|
-
|
687
|
-
|
688
|
-
|
689
|
-
|
730
|
+
options[:include_nil_attributes] =
|
731
|
+
self.include_all_attributes_on_update
|
732
|
+
|
733
|
+
# exclude unchanged data
|
734
|
+
unless self.include_all_attributes_on_update
|
735
|
+
options[:except] ||= []
|
736
|
+
options[:except].concat(
|
737
|
+
self.attribute_names.select { |name| self.changes[name].blank? }
|
738
|
+
)
|
739
|
+
end
|
740
|
+
|
741
|
+
# more generic setup_save_call
|
742
|
+
self.setup_save_call(args, options)
|
743
|
+
end
|
744
|
+
|
745
|
+
def setup_save_call(additional_associations, options = {})
|
746
|
+
# We pass in associations as options and args for no good reason
|
747
|
+
options[:include_associations] ||= []
|
748
|
+
options[:include_associations].concat(additional_associations)
|
749
|
+
|
750
|
+
# get our data
|
751
|
+
data = self.serializable_hash(options)
|
752
|
+
|
753
|
+
# handle the root element
|
754
|
+
if self.include_root_in_json
|
755
|
+
data = { self.class.element_name.to_sym => data}
|
756
|
+
end
|
757
|
+
|
758
|
+
return data
|
759
|
+
end
|
760
|
+
|
690
761
|
private
|
691
|
-
|
762
|
+
|
692
763
|
def split_options(options = {})
|
693
764
|
self.class.__send__(:split_options, options)
|
694
765
|
end
|
695
|
-
|
766
|
+
|
696
767
|
end
|
697
|
-
|
768
|
+
|
698
769
|
class Base
|
699
770
|
extend ActiveModel::Naming
|
700
771
|
# Order is important here
|
701
772
|
# It should be Validations, Dirty Tracking, Callbacks so the include order is the opposite
|
702
773
|
include AssociationActivation
|
703
774
|
self.activate_associations
|
704
|
-
|
775
|
+
|
705
776
|
include Scopes, Callbacks, Observing, Attributes, ModelErrors, Conditions, Finders, Typecast
|
706
|
-
|
777
|
+
|
707
778
|
end
|
708
|
-
|
779
|
+
|
709
780
|
end
|