activeresource 3.2.22.5 → 5.1.1

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.
Files changed (33) hide show
  1. checksums.yaml +5 -5
  2. data/MIT-LICENSE +2 -2
  3. data/README.rdoc +147 -49
  4. data/lib/active_resource.rb +12 -12
  5. data/lib/active_resource/active_job_serializer.rb +26 -0
  6. data/lib/active_resource/associations.rb +175 -0
  7. data/lib/active_resource/associations/builder/association.rb +33 -0
  8. data/lib/active_resource/associations/builder/belongs_to.rb +16 -0
  9. data/lib/active_resource/associations/builder/has_many.rb +14 -0
  10. data/lib/active_resource/associations/builder/has_one.rb +14 -0
  11. data/lib/active_resource/base.rb +444 -231
  12. data/lib/active_resource/callbacks.rb +22 -0
  13. data/lib/active_resource/collection.rb +94 -0
  14. data/lib/active_resource/connection.rb +112 -105
  15. data/lib/active_resource/custom_methods.rb +24 -14
  16. data/lib/active_resource/exceptions.rb +5 -3
  17. data/lib/active_resource/formats.rb +5 -3
  18. data/lib/active_resource/formats/json_format.rb +4 -1
  19. data/lib/active_resource/formats/xml_format.rb +4 -2
  20. data/lib/active_resource/http_mock.rb +69 -31
  21. data/lib/active_resource/log_subscriber.rb +14 -3
  22. data/lib/active_resource/observing.rb +0 -29
  23. data/lib/active_resource/railtie.rb +14 -3
  24. data/lib/active_resource/reflection.rb +78 -0
  25. data/lib/active_resource/schema.rb +4 -4
  26. data/lib/active_resource/singleton.rb +113 -0
  27. data/lib/active_resource/threadsafe_attributes.rb +66 -0
  28. data/lib/active_resource/validations.rb +56 -14
  29. data/lib/active_resource/version.rb +7 -5
  30. data/lib/activeresource.rb +3 -0
  31. metadata +78 -16
  32. data/CHANGELOG.md +0 -437
  33. data/examples/performance.rb +0 -70
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveResource::Associations::Builder
4
+ class Association #:nodoc:
5
+ # providing a Class-Variable, which will have a different store of subclasses
6
+ class_attribute :valid_options
7
+ self.valid_options = [:class_name]
8
+
9
+ # would identify subclasses of association
10
+ class_attribute :macro
11
+
12
+ attr_reader :model, :name, :options, :klass
13
+
14
+ def self.build(model, name, options)
15
+ new(model, name, options).build
16
+ end
17
+
18
+ def initialize(model, name, options)
19
+ @model, @name, @options = model, name, options
20
+ end
21
+
22
+ def build
23
+ validate_options
24
+ model.create_reflection(self.class.macro, name, options)
25
+ end
26
+
27
+ private
28
+
29
+ def validate_options
30
+ options.assert_valid_keys(self.class.valid_options)
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveResource::Associations::Builder
4
+ class BelongsTo < Association
5
+ self.valid_options += [:foreign_key]
6
+
7
+ self.macro = :belongs_to
8
+
9
+ def build
10
+ validate_options
11
+ reflection = model.create_reflection(self.class.macro, name, options)
12
+ model.defines_belongs_to_finder_method(reflection)
13
+ reflection
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveResource::Associations::Builder
4
+ class HasMany < Association
5
+ self.macro = :has_many
6
+
7
+ def build
8
+ validate_options
9
+ model.create_reflection(self.class.macro, name, options).tap do |reflection|
10
+ model.defines_has_many_finder_method(reflection)
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveResource::Associations::Builder
4
+ class HasOne < Association
5
+ self.macro = :has_one
6
+
7
+ def build
8
+ validate_options
9
+ model.create_reflection(self.class.macro, name, options).tap do |reflection|
10
+ model.defines_has_one_finder_method(reflection)
11
+ end
12
+ end
13
+ end
14
+ end
@@ -1,21 +1,28 @@
1
- require 'active_support'
2
- require 'active_support/core_ext/class/attribute_accessors'
3
- require 'active_support/core_ext/class/attribute'
4
- require 'active_support/core_ext/hash/indifferent_access'
5
- require 'active_support/core_ext/kernel/reporting'
6
- require 'active_support/core_ext/module/delegation'
7
- require 'active_support/core_ext/module/aliasing'
8
- require 'active_support/core_ext/object/blank'
9
- require 'active_support/core_ext/object/to_query'
10
- require 'active_support/core_ext/object/duplicable'
11
- require 'set'
12
- require 'uri'
13
-
14
- require 'active_support/core_ext/uri'
15
- require 'active_resource/connection'
16
- require 'active_resource/formats'
17
- require 'active_resource/schema'
18
- require 'active_resource/log_subscriber'
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support"
4
+ require "active_support/core_ext/class/attribute_accessors"
5
+ require "active_support/core_ext/class/attribute"
6
+ require "active_support/core_ext/hash/indifferent_access"
7
+ require "active_support/core_ext/kernel/reporting"
8
+ require "active_support/core_ext/module/delegation"
9
+ require "active_support/core_ext/module/aliasing"
10
+ require "active_support/core_ext/object/blank"
11
+ require "active_support/core_ext/object/to_query"
12
+ require "active_support/core_ext/object/duplicable"
13
+ require "set"
14
+ require "uri"
15
+
16
+ require "active_support/core_ext/uri"
17
+ require "active_resource/connection"
18
+ require "active_resource/formats"
19
+ require "active_resource/schema"
20
+ require "active_resource/log_subscriber"
21
+ require "active_resource/associations"
22
+ require "active_resource/reflection"
23
+ require "active_resource/threadsafe_attributes"
24
+
25
+ require "active_model/serializers/xml"
19
26
 
20
27
  module ActiveResource
21
28
  # ActiveResource::Base is the main class for mapping RESTful resources as models in a Rails application.
@@ -24,29 +31,29 @@ module ActiveResource
24
31
  #
25
32
  # == Automated mapping
26
33
  #
27
- # Active Resource objects represent your RESTful resources as manipulatable Ruby objects. To map resources
34
+ # Active Resource objects represent your RESTful resources as manipulatable Ruby objects. To map resources
28
35
  # to Ruby objects, Active Resource only needs a class name that corresponds to the resource name (e.g., the class
29
36
  # Person maps to the resources people, very similarly to Active Record) and a +site+ value, which holds the
30
37
  # URI of the resources.
31
38
  #
32
39
  # class Person < ActiveResource::Base
33
- # self.site = "http://api.people.com:3000/"
40
+ # self.site = "https://api.people.com"
34
41
  # end
35
42
  #
36
- # Now the Person class is mapped to RESTful resources located at <tt>http://api.people.com:3000/people/</tt>, and
43
+ # Now the Person class is mapped to RESTful resources located at <tt>https://api.people.com/people/</tt>, and
37
44
  # you can now use Active Resource's life cycle methods to manipulate resources. In the case where you already have
38
45
  # an existing model with the same name as the desired RESTful resource you can set the +element_name+ value.
39
46
  #
40
47
  # class PersonResource < ActiveResource::Base
41
- # self.site = "http://api.people.com:3000/"
48
+ # self.site = "https://api.people.com"
42
49
  # self.element_name = "person"
43
50
  # end
44
51
  #
45
52
  # If your Active Resource object is required to use an HTTP proxy you can set the +proxy+ value which holds a URI.
46
53
  #
47
54
  # class PersonResource < ActiveResource::Base
48
- # self.site = "http://api.people.com:3000/"
49
- # self.proxy = "http://user:password@proxy.people.com:8080"
55
+ # self.site = "https://api.people.com"
56
+ # self.proxy = "https://user:password@proxy.people.com:8080"
50
57
  # end
51
58
  #
52
59
  #
@@ -76,7 +83,7 @@ module ActiveResource
76
83
  #
77
84
  # Since simple CRUD/life cycle methods can't accomplish every task, Active Resource also supports
78
85
  # defining your own custom REST methods. To invoke them, Active Resource provides the <tt>get</tt>,
79
- # <tt>post</tt>, <tt>put</tt> and <tt>\delete</tt> methods where you can specify a custom REST method
86
+ # <tt>post</tt>, <tt>put</tt> and <tt>delete</tt> methods where you can specify a custom REST method
80
87
  # name to invoke.
81
88
  #
82
89
  # # POST to the custom 'register' REST method, i.e. POST /people/new/register.json.
@@ -102,7 +109,7 @@ module ActiveResource
102
109
  # You can validate resources client side by overriding validation methods in the base class.
103
110
  #
104
111
  # class Person < ActiveResource::Base
105
- # self.site = "http://api.people.com:3000/"
112
+ # self.site = "https://api.people.com"
106
113
  # protected
107
114
  # def validate
108
115
  # errors.add("last", "has invalid characters") unless last =~ /[a-zA-Z]*/
@@ -113,47 +120,64 @@ module ActiveResource
113
120
  #
114
121
  # == Authentication
115
122
  #
116
- # Many REST APIs will require authentication, usually in the form of basic
117
- # HTTP authentication. Authentication can be specified by:
123
+ # Many REST APIs require authentication. The HTTP spec describes two ways to
124
+ # make requests with a username and password (see RFC 2617).
118
125
  #
119
- # === HTTP Basic Authentication
120
- # * putting the credentials in the URL for the +site+ variable.
126
+ # Basic authentication simply sends a username and password along with HTTP
127
+ # requests. These sensitive credentials are sent unencrypted, visible to
128
+ # any onlooker, so this scheme should only be used with SSL.
129
+ #
130
+ # Digest authentication sends a crytographic hash of the username, password,
131
+ # HTTP method, URI, and a single-use secret key provided by the server.
132
+ # Sensitive credentials aren't visible to onlookers, so digest authentication
133
+ # doesn't require SSL. However, this doesn't mean the connection is secure!
134
+ # Just the username and password.
135
+ #
136
+ # (You really, really want to use SSL. There's little reason not to.)
137
+ #
138
+ # === Picking an authentication scheme
139
+ #
140
+ # Basic authentication is the default. To switch to digest authentication,
141
+ # set +auth_type+ to +:digest+:
121
142
  #
122
143
  # class Person < ActiveResource::Base
123
- # self.site = "http://ryan:password@api.people.com:3000/"
144
+ # self.auth_type = :digest
124
145
  # end
125
146
  #
126
- # * defining +user+ and/or +password+ variables
147
+ # === Setting the username and password
148
+ #
149
+ # Set +user+ and +password+ on the class, or include them in the +site+ URL.
127
150
  #
128
151
  # class Person < ActiveResource::Base
129
- # self.site = "http://api.people.com:3000/"
152
+ # # Set user and password directly:
130
153
  # self.user = "ryan"
131
154
  # self.password = "password"
132
- # end
133
155
  #
134
- # For obvious security reasons, it is probably best if such services are available
135
- # over HTTPS.
136
- #
137
- # Note: Some values cannot be provided in the URL passed to site. e.g. email addresses
138
- # as usernames. In those situations you should use the separate user and password option.
156
+ # # Or include them in the site:
157
+ # self.site = "https://ryan:password@api.people.com"
158
+ # end
139
159
  #
140
160
  # === Certificate Authentication
141
161
  #
142
- # * End point uses an X509 certificate for authentication. <tt>See ssl_options=</tt> for all options.
162
+ # You can also authenticate using an X509 certificate. <tt>See ssl_options=</tt> for all options.
143
163
  #
144
164
  # class Person < ActiveResource::Base
145
165
  # self.site = "https://secure.api.people.com/"
146
- # self.ssl_options = {:cert => OpenSSL::X509::Certificate.new(File.open(pem_file))
147
- # :key => OpenSSL::PKey::RSA.new(File.open(pem_file)),
148
- # :ca_path => "/path/to/OpenSSL/formatted/CA_Certs",
149
- # :verify_mode => OpenSSL::SSL::VERIFY_PEER}
166
+ #
167
+ # File.open(pem_file_path, 'rb') do |pem_file|
168
+ # self.ssl_options = {
169
+ # cert: OpenSSL::X509::Certificate.new(pem_file),
170
+ # key: OpenSSL::PKey::RSA.new(pem_file),
171
+ # ca_path: "/path/to/OpenSSL/formatted/CA_Certs",
172
+ # verify_mode: OpenSSL::SSL::VERIFY_PEER }
173
+ # end
150
174
  # end
151
175
  #
152
176
  #
153
177
  # == Errors & Validation
154
178
  #
155
179
  # Error handling and validation is handled in much the same manner as you're used to seeing in
156
- # Active Record. Both the response code in the HTTP response and the body of the response are used to
180
+ # Active Record. Both the response code in the HTTP response and the body of the response are used to
157
181
  # indicate that an error occurred.
158
182
  #
159
183
  # === Resource errors
@@ -162,7 +186,7 @@ module ActiveResource
162
186
  # response code will be returned from the server which will raise an ActiveResource::ResourceNotFound
163
187
  # exception.
164
188
  #
165
- # # GET http://api.people.com:3000/people/999.json
189
+ # # GET https://api.people.com/people/999.json
166
190
  # ryan = Person.find(999) # 404, raises ActiveResource::ResourceNotFound
167
191
  #
168
192
  #
@@ -184,7 +208,7 @@ module ActiveResource
184
208
  # * Other - ActiveResource::ConnectionError
185
209
  #
186
210
  # These custom exceptions allow you to deal with resource errors more naturally and with more precision
187
- # rather than returning a general HTTP error. For example:
211
+ # rather than returning a general HTTP error. For example:
188
212
  #
189
213
  # begin
190
214
  # ryan = Person.find(my_id)
@@ -198,7 +222,7 @@ module ActiveResource
198
222
  # an ActiveResource::MissingPrefixParam will be raised.
199
223
  #
200
224
  # class Comment < ActiveResource::Base
201
- # self.site = "http://someip.com/posts/:post_id/"
225
+ # self.site = "https://someip.com/posts/:post_id"
202
226
  # end
203
227
  #
204
228
  # Comment.find(1)
@@ -207,8 +231,8 @@ module ActiveResource
207
231
  # === Validation errors
208
232
  #
209
233
  # Active Resource supports validations on resources and will return errors if any of these validations fail
210
- # (e.g., "First name can not be blank" and so on). These types of errors are denoted in the response by
211
- # a response code of <tt>422</tt> and an XML or JSON representation of the validation errors. The save operation will
234
+ # (e.g., "First name can not be blank" and so on). These types of errors are denoted in the response by
235
+ # a response code of <tt>422</tt> and an JSON or XML representation of the validation errors. The save operation will
212
236
  # then fail (with a <tt>false</tt> return value) and the validation errors can be accessed on the resource in question.
213
237
  #
214
238
  # ryan = Person.find(1)
@@ -216,20 +240,29 @@ module ActiveResource
216
240
  # ryan.save # => false
217
241
  #
218
242
  # # When
219
- # # PUT http://api.people.com:3000/people/1.json
243
+ # # PUT https://api.people.com/people/1.xml
220
244
  # # or
221
- # # PUT http://api.people.com:3000/people/1.json
245
+ # # PUT https://api.people.com/people/1.json
222
246
  # # is requested with invalid values, the response is:
223
247
  # #
224
248
  # # Response (422):
225
249
  # # <errors><error>First cannot be empty</error></errors>
226
250
  # # or
227
- # # {"errors":["First cannot be empty"]}
251
+ # # {"errors":{"first":["cannot be empty"]}}
228
252
  # #
229
253
  #
230
254
  # ryan.errors.invalid?(:first) # => true
231
255
  # ryan.errors.full_messages # => ['First cannot be empty']
232
256
  #
257
+ # For backwards-compatibility with older endpoints, the following formats are also supported in JSON responses:
258
+ #
259
+ # # {"errors":['First cannot be empty']}
260
+ # # This was the required format for previous versions of ActiveResource
261
+ # # {"first":["cannot be empty"]}
262
+ # # This was the default format produced by respond_with in ActionController <3.2.1
263
+ #
264
+ # Parsing either of these formats will result in a deprecation warning.
265
+ #
233
266
  # Learn more about Active Resource's validation features in the ActiveResource::Validations documentation.
234
267
  #
235
268
  # === Timeouts
@@ -239,7 +272,7 @@ module ActiveResource
239
272
  # amount of time before Active Resource times out with the +timeout+ variable.
240
273
  #
241
274
  # class Person < ActiveResource::Base
242
- # self.site = "http://api.people.com:3000/"
275
+ # self.site = "https://api.people.com"
243
276
  # self.timeout = 5
244
277
  # end
245
278
  #
@@ -255,15 +288,39 @@ module ActiveResource
255
288
  # Internally, Active Resource relies on Ruby's Net::HTTP library to make HTTP requests. Setting +timeout+
256
289
  # sets the <tt>read_timeout</tt> of the internal Net::HTTP instance to the same value. The default
257
290
  # <tt>read_timeout</tt> is 60 seconds on most Ruby implementations.
291
+ #
292
+ # Active Resource also supports distinct +open_timeout+ (time to connect) and +read_timeout+ (how long to
293
+ # wait for an upstream response). This is inline with supported +Net::HTTP+ timeout configuration and allows
294
+ # for finer control of client timeouts depending on context.
295
+ #
296
+ # class Person < ActiveResource::Base
297
+ # self.site = "https://api.people.com"
298
+ # self.open_timeout = 2
299
+ # self.read_timeout = 10
300
+ # end
258
301
  class Base
259
302
  ##
260
303
  # :singleton-method:
261
304
  # The logger for diagnosing and tracing Active Resource calls.
262
- cattr_accessor :logger
305
+ cattr_reader :logger
306
+
307
+ def self.logger=(logger)
308
+ self._connection = nil
309
+ @@logger = logger
310
+ end
263
311
 
264
312
  class_attribute :_format
313
+ class_attribute :_collection_parser
314
+ class_attribute :include_format_in_path
315
+ self.include_format_in_path = true
316
+
317
+ class_attribute :connection_class
318
+ self.connection_class = Connection
265
319
 
266
320
  class << self
321
+ include ThreadsafeAttributes
322
+ threadsafe_attribute :_headers, :_connection, :_user, :_password, :_site, :_proxy
323
+
267
324
  # Creates a schema for this resource - setting the attributes that are
268
325
  # known prior to fetching an instance from the remote system.
269
326
  #
@@ -276,39 +333,39 @@ module ActiveResource
276
333
  # remote system.
277
334
  #
278
335
  # example:
279
- # class Person < ActiveResource::Base
280
- # schema do
281
- # # define each attribute separately
282
- # attribute 'name', :string
283
- #
284
- # # or use the convenience methods and pass >=1 attribute names
285
- # string 'eye_color', 'hair_color'
286
- # integer 'age'
287
- # float 'height', 'weight'
288
- #
289
- # # unsupported types should be left as strings
290
- # # overload the accessor methods if you need to convert them
291
- # attribute 'created_at', 'string'
336
+ # class Person < ActiveResource::Base
337
+ # schema do
338
+ # # define each attribute separately
339
+ # attribute 'name', :string
340
+ #
341
+ # # or use the convenience methods and pass >=1 attribute names
342
+ # string 'eye_color', 'hair_color'
343
+ # integer 'age'
344
+ # float 'height', 'weight'
345
+ #
346
+ # # unsupported types should be left as strings
347
+ # # overload the accessor methods if you need to convert them
348
+ # attribute 'created_at', 'string'
349
+ # end
292
350
  # end
293
- # end
294
351
  #
295
- # p = Person.new
296
- # p.respond_to? :name # => true
297
- # p.respond_to? :age # => true
298
- # p.name # => nil
299
- # p.age # => nil
352
+ # p = Person.new
353
+ # p.respond_to? :name # => true
354
+ # p.respond_to? :age # => true
355
+ # p.name # => nil
356
+ # p.age # => nil
300
357
  #
301
- # j = Person.find_by_name('John') # <person><name>John</name><age>34</age><num_children>3</num_children></person>
302
- # j.respond_to? :name # => true
303
- # j.respond_to? :age # => true
304
- # j.name # => 'John'
305
- # j.age # => '34' # note this is a string!
306
- # j.num_children # => '3' # note this is a string!
358
+ # j = Person.find_by_name('John')
359
+ # <person><name>John</name><age>34</age><num_children>3</num_children></person>
360
+ # j.respond_to? :name # => true
361
+ # j.respond_to? :age # => true
362
+ # j.name # => 'John'
363
+ # j.age # => '34' # note this is a string!
364
+ # j.num_children # => '3' # note this is a string!
307
365
  #
308
- # p.num_children # => NoMethodError
366
+ # p.num_children # => NoMethodError
309
367
  #
310
- # Attribute-types must be one of:
311
- # string, integer, float
368
+ # Attribute-types must be one of: <tt>string, text, integer, float, decimal, datetime, timestamp, time, date, binary, boolean</tt>
312
369
  #
313
370
  # Note: at present the attribute-type doesn't do anything, but stay
314
371
  # tuned...
@@ -328,12 +385,12 @@ module ActiveResource
328
385
  @schema ||= {}.with_indifferent_access
329
386
  @known_attributes ||= []
330
387
 
331
- schema_definition.attrs.each do |k,v|
388
+ schema_definition.attrs.each do |k, v|
332
389
  @schema[k] = v
333
390
  @known_attributes << k
334
391
  end
335
392
 
336
- schema
393
+ @schema
337
394
  else
338
395
  @schema ||= nil
339
396
  end
@@ -349,9 +406,9 @@ module ActiveResource
349
406
  #
350
407
  # example:
351
408
  #
352
- # class Person < ActiveResource::Base
353
- # schema = {'name' => :string, 'age' => :integer }
354
- # end
409
+ # class Person < ActiveResource::Base
410
+ # schema = {'name' => :string, 'age' => :integer }
411
+ # end
355
412
  #
356
413
  # The keys/values can be strings or symbols. They will be converted to
357
414
  # strings.
@@ -367,7 +424,7 @@ module ActiveResource
367
424
  raise ArgumentError, "Expected a hash" unless the_schema.kind_of? Hash
368
425
 
369
426
  schema do
370
- the_schema.each {|k,v| attribute(k,v) }
427
+ the_schema.each { |k, v| attribute(k, v) }
371
428
  end
372
429
  end
373
430
 
@@ -375,33 +432,33 @@ module ActiveResource
375
432
  # from the provided <tt>schema</tt>
376
433
  # Attributes that are known will cause your resource to return 'true'
377
434
  # when <tt>respond_to?</tt> is called on them. A known attribute will
378
- # return nil if not set (rather than <t>MethodNotFound</tt>); thus
435
+ # return nil if not set (rather than <tt>MethodNotFound</tt>); thus
379
436
  # known attributes can be used with <tt>validates_presence_of</tt>
380
437
  # without a getter-method.
381
438
  def known_attributes
382
439
  @known_attributes ||= []
383
440
  end
384
441
 
385
- # Gets the URI of the REST resources to map for this class. The site variable is required for
442
+ # Gets the URI of the REST resources to map for this class. The site variable is required for
386
443
  # Active Resource's mapping to work.
387
444
  def site
388
445
  # Not using superclass_delegating_reader because don't want subclasses to modify superclass instance
389
446
  #
390
447
  # With superclass_delegating_reader
391
448
  #
392
- # Parent.site = 'http://anonymous@test.com'
393
- # Subclass.site # => 'http://anonymous@test.com'
449
+ # Parent.site = 'https://anonymous@test.com'
450
+ # Subclass.site # => 'https://anonymous@test.com'
394
451
  # Subclass.site.user = 'david'
395
- # Parent.site # => 'http://david@test.com'
452
+ # Parent.site # => 'https://david@test.com'
396
453
  #
397
454
  # Without superclass_delegating_reader (expected behavior)
398
455
  #
399
- # Parent.site = 'http://anonymous@test.com'
400
- # Subclass.site # => 'http://anonymous@test.com'
456
+ # Parent.site = 'https://anonymous@test.com'
457
+ # Subclass.site # => 'https://anonymous@test.com'
401
458
  # Subclass.site.user = 'david' # => TypeError: can't modify frozen object
402
459
  #
403
- if defined?(@site)
404
- @site
460
+ if _site_defined?
461
+ _site
405
462
  elsif superclass != Object && superclass.site
406
463
  superclass.site.dup.freeze
407
464
  end
@@ -410,21 +467,21 @@ module ActiveResource
410
467
  # Sets the URI of the REST resources to map for this class to the value in the +site+ argument.
411
468
  # The site variable is required for Active Resource's mapping to work.
412
469
  def site=(site)
413
- @connection = nil
470
+ self._connection = nil
414
471
  if site.nil?
415
- @site = nil
472
+ self._site = nil
416
473
  else
417
- @site = create_site_uri_from(site)
418
- @user = URI.parser.unescape(@site.user) if @site.user
419
- @password = URI.parser.unescape(@site.password) if @site.password
474
+ self._site = create_site_uri_from(site)
475
+ self._user = URI.parser.unescape(_site.user) if _site.user
476
+ self._password = URI.parser.unescape(_site.password) if _site.password
420
477
  end
421
478
  end
422
479
 
423
480
  # Gets the \proxy variable if a proxy is required
424
481
  def proxy
425
482
  # Not using superclass_delegating_reader. See +site+ for explanation
426
- if defined?(@proxy)
427
- @proxy
483
+ if _proxy_defined?
484
+ _proxy
428
485
  elsif superclass != Object && superclass.proxy
429
486
  superclass.proxy.dup.freeze
430
487
  end
@@ -432,15 +489,15 @@ module ActiveResource
432
489
 
433
490
  # Sets the URI of the http proxy to the value in the +proxy+ argument.
434
491
  def proxy=(proxy)
435
- @connection = nil
436
- @proxy = proxy.nil? ? nil : create_proxy_uri_from(proxy)
492
+ self._connection = nil
493
+ self._proxy = proxy.nil? ? nil : create_proxy_uri_from(proxy)
437
494
  end
438
495
 
439
496
  # Gets the \user for REST HTTP authentication.
440
497
  def user
441
498
  # Not using superclass_delegating_reader. See +site+ for explanation
442
- if defined?(@user)
443
- @user
499
+ if _user_defined?
500
+ _user
444
501
  elsif superclass != Object && superclass.user
445
502
  superclass.user.dup.freeze
446
503
  end
@@ -448,15 +505,15 @@ module ActiveResource
448
505
 
449
506
  # Sets the \user for REST HTTP authentication.
450
507
  def user=(user)
451
- @connection = nil
452
- @user = user
508
+ self._connection = nil
509
+ self._user = user
453
510
  end
454
511
 
455
512
  # Gets the \password for REST HTTP authentication.
456
513
  def password
457
514
  # Not using superclass_delegating_reader. See +site+ for explanation
458
- if defined?(@password)
459
- @password
515
+ if _password_defined?
516
+ _password
460
517
  elsif superclass != Object && superclass.password
461
518
  superclass.password.dup.freeze
462
519
  end
@@ -464,8 +521,8 @@ module ActiveResource
464
521
 
465
522
  # Sets the \password for REST HTTP authentication.
466
523
  def password=(password)
467
- @connection = nil
468
- @password = password
524
+ self._connection = nil
525
+ self._password = password
469
526
  end
470
527
 
471
528
  def auth_type
@@ -475,7 +532,7 @@ module ActiveResource
475
532
  end
476
533
 
477
534
  def auth_type=(auth_type)
478
- @connection = nil
535
+ self._connection = nil
479
536
  @auth_type = auth_type
480
537
  end
481
538
 
@@ -501,12 +558,34 @@ module ActiveResource
501
558
  self._format || ActiveResource::Formats::JsonFormat
502
559
  end
503
560
 
561
+ # Sets the parser to use when a collection is returned. The parser must be Enumerable.
562
+ def collection_parser=(parser_instance)
563
+ parser_instance = parser_instance.constantize if parser_instance.is_a?(String)
564
+ self._collection_parser = parser_instance
565
+ end
566
+
567
+ def collection_parser
568
+ self._collection_parser || ActiveResource::Collection
569
+ end
570
+
504
571
  # Sets the number of seconds after which requests to the REST API should time out.
505
572
  def timeout=(timeout)
506
- @connection = nil
573
+ self._connection = nil
507
574
  @timeout = timeout
508
575
  end
509
576
 
577
+ # Sets the number of seconds after which connection attempts to the REST API should time out.
578
+ def open_timeout=(timeout)
579
+ self._connection = nil
580
+ @open_timeout = timeout
581
+ end
582
+
583
+ # Sets the number of seconds after which reads to the REST API should time out.
584
+ def read_timeout=(timeout)
585
+ self._connection = nil
586
+ @read_timeout = timeout
587
+ end
588
+
510
589
  # Gets the number of seconds after which requests to the REST API should time out.
511
590
  def timeout
512
591
  if defined?(@timeout)
@@ -516,6 +595,24 @@ module ActiveResource
516
595
  end
517
596
  end
518
597
 
598
+ # Gets the number of seconds after which connection attempts to the REST API should time out.
599
+ def open_timeout
600
+ if defined?(@open_timeout)
601
+ @open_timeout
602
+ elsif superclass != Object && superclass.open_timeout
603
+ superclass.open_timeout
604
+ end
605
+ end
606
+
607
+ # Gets the number of seconds after which reads to the REST API should time out.
608
+ def read_timeout
609
+ if defined?(@read_timeout)
610
+ @read_timeout
611
+ elsif superclass != Object && superclass.read_timeout
612
+ superclass.read_timeout
613
+ end
614
+ end
615
+
519
616
  # Options that will get applied to an SSL connection.
520
617
  #
521
618
  # * <tt>:key</tt> - An OpenSSL::PKey::RSA or OpenSSL::PKey::DSA object.
@@ -527,9 +624,9 @@ module ActiveResource
527
624
  # * <tt>:verify_depth</tt> - The maximum depth for the certificate chain verification.
528
625
  # * <tt>:cert_store</tt> - OpenSSL::X509::Store to verify peer certificate.
529
626
  # * <tt>:ssl_timeout</tt> -The SSL timeout in seconds.
530
- def ssl_options=(opts={})
531
- @connection = nil
532
- @ssl_options = opts
627
+ def ssl_options=(options)
628
+ self._connection = nil
629
+ @ssl_options = options
533
630
  end
534
631
 
535
632
  # Returns the SSL options hash.
@@ -545,22 +642,31 @@ module ActiveResource
545
642
  # The +refresh+ parameter toggles whether or not the \connection is refreshed at every request
546
643
  # or not (defaults to <tt>false</tt>).
547
644
  def connection(refresh = false)
548
- if defined?(@connection) || superclass == Object
549
- @connection = Connection.new(site, format) if refresh || @connection.nil?
550
- @connection.proxy = proxy if proxy
551
- @connection.user = user if user
552
- @connection.password = password if password
553
- @connection.auth_type = auth_type if auth_type
554
- @connection.timeout = timeout if timeout
555
- @connection.ssl_options = ssl_options if ssl_options
556
- @connection
645
+ if _connection_defined? || superclass == Object
646
+ self._connection = connection_class.new(
647
+ site, format, logger: logger
648
+ ) if refresh || _connection.nil?
649
+ _connection.proxy = proxy if proxy
650
+ _connection.user = user if user
651
+ _connection.password = password if password
652
+ _connection.auth_type = auth_type if auth_type
653
+ _connection.timeout = timeout if timeout
654
+ _connection.open_timeout = open_timeout if open_timeout
655
+ _connection.read_timeout = read_timeout if read_timeout
656
+ _connection.ssl_options = ssl_options if ssl_options
657
+ _connection
557
658
  else
558
659
  superclass.connection
559
660
  end
560
661
  end
561
662
 
562
663
  def headers
563
- @headers ||= {}
664
+ headers_state = self._headers || {}
665
+ if superclass != Object
666
+ self._headers = superclass.headers.merge(headers_state)
667
+ else
668
+ headers_state
669
+ end
564
670
  end
565
671
 
566
672
  attr_writer :element_name
@@ -578,20 +684,28 @@ module ActiveResource
578
684
  attr_writer :primary_key
579
685
 
580
686
  def primary_key
581
- @primary_key ||= 'id'
687
+ if defined?(@primary_key)
688
+ @primary_key
689
+ elsif superclass != Object && superclass.primary_key
690
+ primary_key = superclass.primary_key
691
+ return primary_key if primary_key.is_a?(Symbol)
692
+ primary_key.dup.freeze
693
+ else
694
+ "id"
695
+ end
582
696
  end
583
697
 
584
698
  # Gets the \prefix for a resource's nested URL (e.g., <tt>prefix/collectionname/1.json</tt>)
585
699
  # This method is regenerated at runtime based on what the \prefix is set to.
586
- def prefix(options={})
700
+ def prefix(options = {})
587
701
  default = site.path
588
- default << '/' unless default[-1..-1] == '/'
702
+ default << "/" unless default[-1..-1] == "/"
589
703
  # generate the actual method based on the current site path
590
704
  self.prefix = default
591
705
  prefix(options)
592
706
  end
593
707
 
594
- # An attribute reader for the source string for the resource path \prefix. This
708
+ # An attribute reader for the source string for the resource path \prefix. This
595
709
  # method is regenerated at runtime based on what the \prefix is set to.
596
710
  def prefix_source
597
711
  prefix # generate #prefix and #prefix_source methods first
@@ -600,7 +714,7 @@ module ActiveResource
600
714
 
601
715
  # Sets the \prefix for a resource's nested URL (e.g., <tt>prefix/collectionname/1.json</tt>).
602
716
  # Default value is <tt>site.path</tt>.
603
- def prefix=(value = '/')
717
+ def prefix=(value = "/")
604
718
  # Replace :placeholders with '#{embedded options[:lookups]}'
605
719
  prefix_call = value.gsub(/:\w+/) { |key| "\#{URI.parser.escape options[#{key}].to_s}" }
606
720
 
@@ -624,12 +738,17 @@ module ActiveResource
624
738
  alias_method :set_element_name, :element_name= #:nodoc:
625
739
  alias_method :set_collection_name, :collection_name= #:nodoc:
626
740
 
627
- # Gets the element path for the given ID in +id+. If the +query_options+ parameter is omitted, Rails
741
+ def format_extension
742
+ include_format_in_path ? ".#{format.extension}" : ""
743
+ end
744
+
745
+ # Gets the element path for the given ID in +id+. If the +query_options+ parameter is omitted, Rails
628
746
  # will split from the \prefix options.
629
747
  #
630
748
  # ==== Options
631
749
  # +prefix_options+ - A \hash to add a \prefix to the request for nested URLs (e.g., <tt>:account_id => 19</tt>
632
- # would yield a URL like <tt>/accounts/19/purchases.json</tt>).
750
+ # would yield a URL like <tt>/accounts/19/purchases.json</tt>).
751
+ #
633
752
  # +query_options+ - A \hash to add items to the query string for the request.
634
753
  #
635
754
  # ==== Examples
@@ -637,7 +756,7 @@ module ActiveResource
637
756
  # # => /posts/1.json
638
757
  #
639
758
  # class Comment < ActiveResource::Base
640
- # self.site = "http://37s.sunrise.i/posts/:post_id/"
759
+ # self.site = "https://37s.sunrise.com/posts/:post_id"
641
760
  # end
642
761
  #
643
762
  # Comment.element_path(1, :post_id => 5)
@@ -653,30 +772,60 @@ module ActiveResource
653
772
  check_prefix_options(prefix_options)
654
773
 
655
774
  prefix_options, query_options = split_options(prefix_options) if query_options.nil?
656
- "#{prefix(prefix_options)}#{collection_name}/#{URI.parser.escape id.to_s}.#{format.extension}#{query_string(query_options)}"
775
+ "#{prefix(prefix_options)}#{collection_name}/#{URI.encode_www_form_component(id.to_s)}#{format_extension}#{query_string(query_options)}"
776
+ end
777
+
778
+ # Gets the element url for the given ID in +id+. If the +query_options+ parameter is omitted, Rails
779
+ # will split from the \prefix options.
780
+ #
781
+ # ==== Options
782
+ # +prefix_options+ - A \hash to add a \prefix to the request for nested URLs (e.g., <tt>:account_id => 19</tt>
783
+ # would yield a URL like <tt>https://37s.sunrise.com/accounts/19/purchases.json</tt>).
784
+ #
785
+ # +query_options+ - A \hash to add items to the query string for the request.
786
+ #
787
+ # ==== Examples
788
+ # Post.element_url(1)
789
+ # # => https://37s.sunrise.com/posts/1.json
790
+ #
791
+ # class Comment < ActiveResource::Base
792
+ # self.site = "https://37s.sunrise.com/posts/:post_id"
793
+ # end
794
+ #
795
+ # Comment.element_url(1, :post_id => 5)
796
+ # # => https://37s.sunrise.com/posts/5/comments/1.json
797
+ #
798
+ # Comment.element_url(1, :post_id => 5, :active => 1)
799
+ # # => https://37s.sunrise.com/posts/5/comments/1.json?active=1
800
+ #
801
+ # Comment.element_url(1, {:post_id => 5}, {:active => 1})
802
+ # # => https://37s.sunrise.com/posts/5/comments/1.json?active=1
803
+ #
804
+ def element_url(id, prefix_options = {}, query_options = nil)
805
+ URI.join(site, element_path(id, prefix_options, query_options)).to_s
657
806
  end
658
807
 
659
808
  # Gets the new element path for REST resources.
660
809
  #
661
810
  # ==== Options
662
811
  # * +prefix_options+ - A hash to add a prefix to the request for nested URLs (e.g., <tt>:account_id => 19</tt>
663
- # would yield a URL like <tt>/accounts/19/purchases/new.json</tt>).
812
+ # would yield a URL like <tt>/accounts/19/purchases/new.json</tt>).
664
813
  #
665
814
  # ==== Examples
666
815
  # Post.new_element_path
667
816
  # # => /posts/new.json
668
817
  #
669
818
  # class Comment < ActiveResource::Base
670
- # self.site = "http://37s.sunrise.i/posts/:post_id/"
819
+ # self.site = "https://37s.sunrise.com/posts/:post_id"
671
820
  # end
672
821
  #
673
822
  # Comment.collection_path(:post_id => 5)
674
823
  # # => /posts/5/comments/new.json
675
824
  def new_element_path(prefix_options = {})
676
- "#{prefix(prefix_options)}#{collection_name}/new.#{format.extension}"
825
+ "#{prefix(prefix_options)}#{collection_name}/new#{format_extension}"
677
826
  end
678
827
 
679
- # Gets the collection path for the REST resources. If the +query_options+ parameter is omitted, Rails
828
+ # Gets the collection path for the REST resources. If the +query_options+ parameter is omitted, Rails
680
829
  # will split from the +prefix_options+.
681
830
  #
682
831
  # ==== Options
@@ -700,7 +849,7 @@ module ActiveResource
700
849
  def collection_path(prefix_options = {}, query_options = nil)
701
850
  check_prefix_options(prefix_options)
702
851
  prefix_options, query_options = split_options(prefix_options) if query_options.nil?
703
- "#{prefix(prefix_options)}#{collection_name}.#{format.extension}#{query_string(query_options)}"
852
+ "#{prefix(prefix_options)}#{collection_name}#{format_extension}#{query_string(query_options)}"
704
853
  end
705
854
 
706
855
  alias_method :set_primary_key, :primary_key= #:nodoc:
@@ -714,7 +863,7 @@ module ActiveResource
714
863
  # Returns the new resource instance.
715
864
  #
716
865
  def build(attributes = {})
717
- attrs = self.format.decode(connection.get("#{new_element_path}").body).merge(attributes)
866
+ attrs = self.format.decode(connection.get("#{new_element_path(attributes)}", headers).body).merge(attributes)
718
867
  self.new(attrs)
719
868
  end
720
869
 
@@ -724,8 +873,8 @@ module ActiveResource
724
873
  # ryan = Person.new(:first => 'ryan')
725
874
  # ryan.save
726
875
  #
727
- # Returns the newly created resource. If a failure has occurred an
728
- # exception will be raised (see <tt>save</tt>). If the resource is invalid and
876
+ # Returns the newly created resource. If a failure has occurred an
877
+ # exception will be raised (see <tt>save</tt>). If the resource is invalid and
729
878
  # has not been saved then <tt>valid?</tt> will return <tt>false</tt>,
730
879
  # while <tt>new?</tt> will still return <tt>true</tt>.
731
880
  #
@@ -746,11 +895,22 @@ module ActiveResource
746
895
  self.new(attributes).tap { |resource| resource.save }
747
896
  end
748
897
 
749
- # Core method for finding resources. Used similarly to Active Record's +find+ method.
898
+ # Creates a new resource (just like <tt>create</tt>) and makes a request to the
899
+ # remote service that it be saved, but runs validations and raises
900
+ # <tt>ActiveResource::ResourceInvalid</tt>, making it equivalent to the following
901
+ # simultaneous calls:
902
+ #
903
+ # ryan = Person.new(:first => 'ryan')
904
+ # ryan.save!
905
+ def create!(attributes = {})
906
+ self.new(attributes).tap { |resource| resource.save! }
907
+ end
908
+
909
+ # Core method for finding resources. Used similarly to Active Record's +find+ method.
750
910
  #
751
911
  # ==== Arguments
752
- # The first argument is considered to be the scope of the query. That is, how many
753
- # resources are returned from the request. It can be one of the following.
912
+ # The first argument is considered to be the scope of the query. That is, how many
913
+ # resources are returned from the request. It can be one of the following.
754
914
  #
755
915
  # * <tt>:one</tt> - Returns a single resource.
756
916
  # * <tt>:first</tt> - Returns the first resource found.
@@ -794,9 +954,9 @@ module ActiveResource
794
954
  # # => GET /people/1/street_addresses/1.json
795
955
  #
796
956
  # == Failure or missing data
797
- # A failure to find the requested object raises a ResourceNotFound
798
- # exception if the find was called with an id.
799
- # With any other scope, find returns nil when no data is returned.
957
+ # A failure to find the requested object raises a ResourceNotFound
958
+ # exception if the find was called with an id.
959
+ # With any other scope, find returns nil when no data is returned.
800
960
  #
801
961
  # Person.find(1)
802
962
  # # => raises ResourceNotFound
@@ -810,11 +970,18 @@ module ActiveResource
810
970
  options = arguments.slice!(0) || {}
811
971
 
812
972
  case scope
813
- when :all then find_every(options)
814
- when :first then find_every(options).first
815
- when :last then find_every(options).last
816
- when :one then find_one(options)
817
- else find_single(scope, options)
973
+ when :all
974
+ find_every(options)
975
+ when :first
976
+ collection = find_every(options)
977
+ collection && collection.first
978
+ when :last
979
+ collection = find_every(options)
980
+ collection && collection.last
981
+ when :one
982
+ find_one(options)
983
+ else
984
+ find_single(scope, options)
818
985
  end
819
986
  end
820
987
 
@@ -833,12 +1000,17 @@ module ActiveResource
833
1000
  find(:last, *args)
834
1001
  end
835
1002
 
836
- # This is an alias for find(:all). You can pass in all the same
1003
+ # This is an alias for find(:all). You can pass in all the same
837
1004
  # arguments to this method as you can to <tt>find(:all)</tt>
838
1005
  def all(*args)
839
1006
  find(:all, *args)
840
1007
  end
841
1008
 
1009
+ def where(clauses = {})
1010
+ raise ArgumentError, "expected a clauses Hash, got #{clauses.inspect}" unless clauses.is_a? Hash
1011
+ find(:all, params: clauses)
1012
+ end
1013
+
842
1014
 
843
1015
  # Deletes the resources with the ID in the +id+ parameter.
844
1016
  #
@@ -855,7 +1027,7 @@ module ActiveResource
855
1027
  # # Let's assume a request to events/5/cancel.json
856
1028
  # Event.delete(params[:id]) # sends DELETE /events/5
857
1029
  def delete(id, options = {})
858
- connection.delete(element_path(id, options))
1030
+ connection.delete(element_path(id, options), headers)
859
1031
  end
860
1032
 
861
1033
  # Asserts the existence of a resource, returning <tt>true</tt> if the resource is found.
@@ -870,7 +1042,7 @@ module ActiveResource
870
1042
  prefix_options, query_options = split_options(options[:params])
871
1043
  path = element_path(id, prefix_options, query_options)
872
1044
  response = connection.head(path, headers)
873
- response.code.to_i == 200
1045
+ (200..206).include? response.code.to_i
874
1046
  end
875
1047
  # id && !find_single(id, options).nil?
876
1048
  rescue ActiveResource::ResourceNotFound, ActiveResource::ResourceGone
@@ -891,14 +1063,14 @@ module ActiveResource
891
1063
  begin
892
1064
  case from = options[:from]
893
1065
  when Symbol
894
- instantiate_collection(get(from, options[:params]))
1066
+ instantiate_collection(get(from, options[:params]), options[:params])
895
1067
  when String
896
1068
  path = "#{from}#{query_string(options[:params])}"
897
- instantiate_collection(format.decode(connection.get(path, headers).body) || [])
1069
+ instantiate_collection(format.decode(connection.get(path, headers).body) || [], options[:params])
898
1070
  else
899
1071
  prefix_options, query_options = split_options(options[:params])
900
1072
  path = collection_path(prefix_options, query_options)
901
- instantiate_collection( (format.decode(connection.get(path, headers).body) || []), prefix_options )
1073
+ instantiate_collection((format.decode(connection.get(path, headers).body) || []), query_options, prefix_options)
902
1074
  end
903
1075
  rescue ActiveResource::ResourceNotFound
904
1076
  # Swallowing ResourceNotFound exceptions and return nil - as per
@@ -925,8 +1097,11 @@ module ActiveResource
925
1097
  instantiate_record(format.decode(connection.get(path, headers).body), prefix_options)
926
1098
  end
927
1099
 
928
- def instantiate_collection(collection, prefix_options = {})
929
- collection.collect! { |record| instantiate_record(record, prefix_options) }
1100
+ def instantiate_collection(collection, original_params = {}, prefix_options = {})
1101
+ collection_parser.new(collection).tap do |parser|
1102
+ parser.resource_class = self
1103
+ parser.original_params = original_params
1104
+ end.collect! { |record| instantiate_record(record, prefix_options) }
930
1105
  end
931
1106
 
932
1107
  def instantiate_record(record, prefix_options = {})
@@ -938,12 +1113,12 @@ module ActiveResource
938
1113
 
939
1114
  # Accepts a URI and creates the site URI from that.
940
1115
  def create_site_uri_from(site)
941
- site.is_a?(URI) ? site.dup : URI.parser.parse(site)
1116
+ site.is_a?(URI) ? site.dup : URI.parse(site)
942
1117
  end
943
1118
 
944
1119
  # Accepts a URI and creates the proxy URI from that.
945
1120
  def create_proxy_uri_from(proxy)
946
- proxy.is_a?(URI) ? proxy.dup : URI.parser.parse(proxy)
1121
+ proxy.is_a?(URI) ? proxy.dup : URI.parse(proxy)
947
1122
  end
948
1123
 
949
1124
  # contains a set of the current prefix parameters.
@@ -962,8 +1137,8 @@ module ActiveResource
962
1137
  prefix_options, query_options = {}, {}
963
1138
 
964
1139
  (options || {}).each do |key, value|
965
- next if key.blank? || !key.respond_to?(:to_sym)
966
- (prefix_parameters.include?(key.to_sym) ? prefix_options : query_options)[key.to_sym] = value
1140
+ next if key.blank?
1141
+ (prefix_parameters.include?(key.to_s.to_sym) ? prefix_options : query_options)[key.to_s.to_sym] = value
967
1142
  end
968
1143
 
969
1144
  [ prefix_options, query_options ]
@@ -984,7 +1159,7 @@ module ActiveResource
984
1159
  # gathered from the provided <tt>schema</tt>, or from the attributes
985
1160
  # set on this instance after it has been fetched from the remote system.
986
1161
  def known_attributes
987
- self.class.known_attributes + self.attributes.keys.map(&:to_s)
1162
+ (self.class.known_attributes + self.attributes.keys.map(&:to_s)).uniq
988
1163
  end
989
1164
 
990
1165
 
@@ -1003,7 +1178,7 @@ module ActiveResource
1003
1178
  @attributes = {}.with_indifferent_access
1004
1179
  @prefix_options = {}
1005
1180
  @persisted = persisted
1006
- load(attributes)
1181
+ load(attributes, false, persisted)
1007
1182
  end
1008
1183
 
1009
1184
  # Returns a \clone of the resource that hasn't been assigned an +id+ yet and
@@ -1014,7 +1189,7 @@ module ActiveResource
1014
1189
  # not_ryan.new? # => true
1015
1190
  #
1016
1191
  # Any active resource member attributes will NOT be cloned, though all other
1017
- # attributes are. This is to prevent the conflict between any +prefix_options+
1192
+ # attributes are. This is to prevent the conflict between any +prefix_options+
1018
1193
  # that refer to the original parent resource and the newly cloned parent
1019
1194
  # resource that does not exist.
1020
1195
  #
@@ -1028,13 +1203,13 @@ module ActiveResource
1028
1203
  # not_ryan.hash # => {:not => "an ARes instance"}
1029
1204
  def clone
1030
1205
  # Clone all attributes except the pk and any nested ARes
1031
- cloned = Hash[attributes.reject {|k,v| k == self.class.primary_key || v.is_a?(ActiveResource::Base)}.map { |k, v| [k, v.clone] }]
1206
+ cloned = Hash[attributes.reject { |k, v| k == self.class.primary_key || v.is_a?(ActiveResource::Base) }.map { |k, v| [k, v.clone] }]
1032
1207
  # Form the new resource - bypass initialize of resource with 'new' as that will call 'load' which
1033
- # attempts to convert hashes into member objects and arrays into collections of objects. We want
1208
+ # attempts to convert hashes into member objects and arrays into collections of objects. We want
1034
1209
  # the raw objects to be cloned so we bypass load by directly setting the attributes hash.
1035
1210
  resource = self.class.new({})
1036
1211
  resource.prefix_options = self.prefix_options
1037
- resource.send :instance_variable_set, '@attributes', cloned
1212
+ resource.send :instance_variable_set, "@attributes", cloned
1038
1213
  resource
1039
1214
  end
1040
1215
 
@@ -1082,7 +1257,7 @@ module ActiveResource
1082
1257
  attributes[self.class.primary_key] = id
1083
1258
  end
1084
1259
 
1085
- # Test for equality. Resource are equal if and only if +other+ is the same object or
1260
+ # Test for equality. Resource are equal if and only if +other+ is the same object or
1086
1261
  # is an instance of the same class, is not <tt>new?</tt>, and has the same +id+.
1087
1262
  #
1088
1263
  # ==== Examples
@@ -1138,7 +1313,7 @@ module ActiveResource
1138
1313
  end
1139
1314
  end
1140
1315
 
1141
- # Saves (+POST+) or \updates (+PUT+) a resource. Delegates to +create+ if the object is \new,
1316
+ # Saves (+POST+) or \updates (+PUT+) a resource. Delegates to +create+ if the object is \new,
1142
1317
  # +update+ if it exists. If the response to the \save includes a body, it will be assumed that this body
1143
1318
  # is Json for the final object as it looked after the \save (which would include attributes like +created_at+
1144
1319
  # that weren't part of the original submit).
@@ -1152,7 +1327,9 @@ module ActiveResource
1152
1327
  # my_company.size = 10
1153
1328
  # my_company.save # sends PUT /companies/1 (update)
1154
1329
  def save
1155
- new? ? create : update
1330
+ run_callbacks :save do
1331
+ new? ? create : update
1332
+ end
1156
1333
  end
1157
1334
 
1158
1335
  # Saves the resource.
@@ -1185,11 +1362,13 @@ module ActiveResource
1185
1362
  # new_person.destroy
1186
1363
  # Person.find(new_id) # 404 (Resource Not Found)
1187
1364
  def destroy
1188
- connection.delete(element_path, self.class.headers)
1365
+ run_callbacks :destroy do
1366
+ connection.delete(element_path, self.class.headers)
1367
+ end
1189
1368
  end
1190
1369
 
1191
1370
  # Evaluates to <tt>true</tt> if this resource is not <tt>new?</tt> and is
1192
- # found on the remote service. Using this method, you can check for
1371
+ # found on the remote service. Using this method, you can check for
1193
1372
  # resources that may have been deleted between the object's instantiation
1194
1373
  # and actions on it.
1195
1374
  #
@@ -1205,13 +1384,13 @@ module ActiveResource
1205
1384
  # Person.delete(guys_id)
1206
1385
  # that_guy.exists? # => false
1207
1386
  def exists?
1208
- !new? && self.class.exists?(to_param, :params => prefix_options)
1387
+ !new? && self.class.exists?(to_param, params: prefix_options)
1209
1388
  end
1210
1389
 
1211
1390
  # Returns the serialized string representation of the resource in the configured
1212
1391
  # serialization format specified in ActiveResource::Base.format. The options
1213
1392
  # applicable depend on the configured encoding format.
1214
- def encode(options={})
1393
+ def encode(options = {})
1215
1394
  send("to_#{self.class.format.extension}", options)
1216
1395
  end
1217
1396
 
@@ -1227,11 +1406,11 @@ module ActiveResource
1227
1406
  # my_branch.reload
1228
1407
  # my_branch.name # => "Wilson Road"
1229
1408
  def reload
1230
- self.load(self.class.find(to_param, :params => @prefix_options).attributes)
1409
+ self.load(self.class.find(to_param, params: @prefix_options).attributes, false, true)
1231
1410
  end
1232
1411
 
1233
1412
  # A method to manually load attributes from a \hash. Recursively loads collections of
1234
- # resources. This method is called in +initialize+ and +create+ when a \hash of attributes
1413
+ # resources. This method is called in +initialize+ and +create+ when a \hash of attributes
1235
1414
  # is provided.
1236
1415
  #
1237
1416
  # ==== Examples
@@ -1251,8 +1430,12 @@ module ActiveResource
1251
1430
  # your_supplier = Supplier.new
1252
1431
  # your_supplier.load(my_attrs)
1253
1432
  # your_supplier.save
1254
- def load(attributes, remove_root = false)
1255
- raise ArgumentError, "expected an attributes Hash, got #{attributes.inspect}" unless attributes.is_a?(Hash)
1433
+ def load(attributes, remove_root = false, persisted = false)
1434
+ unless attributes.respond_to?(:to_hash)
1435
+ raise ArgumentError, "expected attributes to be able to convert to Hash, got #{attributes.inspect}"
1436
+ end
1437
+
1438
+ attributes = attributes.to_hash
1256
1439
  @prefix_options, attributes = split_options(attributes)
1257
1440
 
1258
1441
  if attributes.keys.size == 1
@@ -1264,21 +1447,21 @@ module ActiveResource
1264
1447
  attributes.each do |key, value|
1265
1448
  @attributes[key.to_s] =
1266
1449
  case value
1267
- when Array
1268
- resource = nil
1269
- value.map do |attrs|
1270
- if attrs.is_a?(Hash)
1271
- resource ||= find_or_create_resource_for_collection(key)
1272
- resource.new(attrs)
1273
- else
1274
- attrs.duplicable? ? attrs.dup : attrs
1275
- end
1450
+ when Array
1451
+ resource = nil
1452
+ value.map do |attrs|
1453
+ if attrs.is_a?(Hash)
1454
+ resource ||= find_or_create_resource_for_collection(key)
1455
+ resource.new(attrs, persisted)
1456
+ else
1457
+ attrs.duplicable? ? attrs.dup : attrs
1276
1458
  end
1277
- when Hash
1278
- resource = find_or_create_resource_for(key)
1279
- resource.new(value)
1280
- else
1281
- value.duplicable? ? value.dup : value
1459
+ end
1460
+ when Hash
1461
+ resource = find_or_create_resource_for(key)
1462
+ resource.new(value, persisted)
1463
+ else
1464
+ value.duplicable? ? value.dup : value
1282
1465
  end
1283
1466
  end
1284
1467
  self
@@ -1286,14 +1469,14 @@ module ActiveResource
1286
1469
 
1287
1470
  # Updates a single attribute and then saves the object.
1288
1471
  #
1289
- # Note: Unlike ActiveRecord::Base.update_attribute, this method <b>is</b>
1472
+ # Note: <tt>Unlike ActiveRecord::Base.update_attribute</tt>, this method <b>is</b>
1290
1473
  # subject to normal validation routines as an update sends the whole body
1291
- # of the resource in the request. (See Validations).
1474
+ # of the resource in the request. (See Validations).
1292
1475
  #
1293
1476
  # As such, this method is equivalent to calling update_attributes with a single attribute/value pair.
1294
1477
  #
1295
1478
  # If the saving fails because of a connection or remote service error, an
1296
- # exception will be raised. If saving fails because the resource is
1479
+ # exception will be raised. If saving fails because the resource is
1297
1480
  # invalid then <tt>false</tt> will be returned.
1298
1481
  def update_attribute(name, value)
1299
1482
  self.send("#{name}=".to_sym, value)
@@ -1304,7 +1487,7 @@ module ActiveResource
1304
1487
  # and requests that the record be saved.
1305
1488
  #
1306
1489
  # If the saving fails because of a connection or remote service error, an
1307
- # exception will be raised. If saving fails because the resource is
1490
+ # exception will be raised. If saving fails because the resource is
1308
1491
  # invalid then <tt>false</tt> will be returned.
1309
1492
  #
1310
1493
  # Note: Though this request can be made with a partial set of the
@@ -1320,7 +1503,7 @@ module ActiveResource
1320
1503
  # A method to determine if an object responds to a message (e.g., a method call). In Active Resource, a Person object with a
1321
1504
  # +name+ attribute can answer <tt>true</tt> to <tt>my_person.respond_to?(:name)</tt>, <tt>my_person.respond_to?(:name=)</tt>, and
1322
1505
  # <tt>my_person.respond_to?(:name?)</tt>.
1323
- def respond_to?(method, include_priv = false)
1506
+ def respond_to_missing?(method, include_priv = false)
1324
1507
  method_name = method.to_s
1325
1508
  if attributes.nil?
1326
1509
  super
@@ -1335,12 +1518,20 @@ module ActiveResource
1335
1518
  end
1336
1519
  end
1337
1520
 
1338
- def to_json(options={})
1339
- super(include_root_in_json ? { :root => self.class.element_name }.merge(options) : options)
1521
+ def to_json(options = {})
1522
+ super(include_root_in_json ? { root: self.class.element_name }.merge(options) : options)
1340
1523
  end
1341
1524
 
1342
- def to_xml(options={})
1343
- super({ :root => self.class.element_name }.merge(options))
1525
+ def to_xml(options = {})
1526
+ super({ root: self.class.element_name }.merge(options))
1527
+ end
1528
+
1529
+ def read_attribute_for_serialization(n)
1530
+ if !attributes[n].nil?
1531
+ attributes[n]
1532
+ elsif respond_to?(n)
1533
+ send(n)
1534
+ end
1344
1535
  end
1345
1536
 
1346
1537
  protected
@@ -1350,37 +1541,45 @@ module ActiveResource
1350
1541
 
1351
1542
  # Update the resource on the remote service.
1352
1543
  def update
1353
- connection.put(element_path(prefix_options), encode, self.class.headers).tap do |response|
1354
- load_attributes_from_response(response)
1544
+ run_callbacks :update do
1545
+ connection.put(element_path(prefix_options), encode, self.class.headers).tap do |response|
1546
+ load_attributes_from_response(response)
1547
+ end
1355
1548
  end
1356
1549
  end
1357
1550
 
1358
1551
  # Create (i.e., \save to the remote service) the \new resource.
1359
1552
  def create
1360
- connection.post(collection_path, encode, self.class.headers).tap do |response|
1361
- self.id = id_from_response(response)
1362
- load_attributes_from_response(response)
1553
+ run_callbacks :create do
1554
+ connection.post(collection_path, encode, self.class.headers).tap do |response|
1555
+ self.id = id_from_response(response)
1556
+ load_attributes_from_response(response)
1557
+ end
1363
1558
  end
1364
1559
  end
1365
1560
 
1366
1561
  def load_attributes_from_response(response)
1367
- if (response_code_allows_body?(response.code) &&
1368
- (response['Content-Length'].nil? || response['Content-Length'] != "0") &&
1369
- !response.body.nil? && response.body.strip.size > 0)
1370
- load(self.class.format.decode(response.body), true)
1562
+ if response_code_allows_body?(response.code.to_i) &&
1563
+ (response["Content-Length"].nil? || response["Content-Length"] != "0") &&
1564
+ !response.body.nil? && response.body.strip.size > 0
1565
+ load(self.class.format.decode(response.body), true, true)
1371
1566
  @persisted = true
1372
1567
  end
1373
1568
  end
1374
1569
 
1375
1570
  # Takes a response from a typical create post and pulls the ID out
1376
1571
  def id_from_response(response)
1377
- response['Location'][/\/([^\/]*?)(\.\w+)?$/, 1] if response['Location']
1572
+ response["Location"][/\/([^\/]*?)(\.\w+)?$/, 1] if response["Location"]
1378
1573
  end
1379
1574
 
1380
1575
  def element_path(options = nil)
1381
1576
  self.class.element_path(to_param, options || prefix_options)
1382
1577
  end
1383
1578
 
1579
+ def element_url(options = nil)
1580
+ self.class.element_url(to_param, options || prefix_options)
1581
+ end
1582
+
1384
1583
  def new_element_path
1385
1584
  self.class.new_element_path(prefix_options)
1386
1585
  end
@@ -1391,17 +1590,14 @@ module ActiveResource
1391
1590
 
1392
1591
  private
1393
1592
 
1394
- def read_attribute_for_serialization(n)
1395
- attributes[n]
1396
- end
1397
-
1398
1593
  # Determine whether the response is allowed to have a body per HTTP 1.1 spec section 4.4.1
1399
1594
  def response_code_allows_body?(c)
1400
- !((100..199).include?(c) || [204,304].include?(c))
1595
+ !((100..199).include?(c) || [204, 304].include?(c))
1401
1596
  end
1402
1597
 
1403
1598
  # Tries to find a resource for a given collection name; if it fails, then the resource is created
1404
1599
  def find_or_create_resource_for_collection(name)
1600
+ return reflections[name.to_sym].klass if reflections.key?(name.to_sym)
1405
1601
  find_or_create_resource_for(ActiveSupport::Inflector.singularize(name.to_s))
1406
1602
  end
1407
1603
 
@@ -1409,10 +1605,10 @@ module ActiveResource
1409
1605
  # if it fails, then the resource is created
1410
1606
  def find_or_create_resource_in_modules(resource_name, module_names)
1411
1607
  receiver = Object
1412
- namespaces = module_names[0, module_names.size-1].map do |module_name|
1608
+ namespaces = module_names[0, module_names.size - 1].map do |module_name|
1413
1609
  receiver = receiver.const_get(module_name)
1414
1610
  end
1415
- const_args = RUBY_VERSION < "1.9" ? [resource_name] : [resource_name, false]
1611
+ const_args = [resource_name, false]
1416
1612
  if namespace = namespaces.reverse.detect { |ns| ns.const_defined?(*const_args) }
1417
1613
  namespace.const_get(*const_args)
1418
1614
  else
@@ -1422,13 +1618,18 @@ module ActiveResource
1422
1618
 
1423
1619
  # Tries to find a resource for a given name; if it fails, then the resource is created
1424
1620
  def find_or_create_resource_for(name)
1621
+ return reflections[name.to_sym].klass if reflections.key?(name.to_sym)
1425
1622
  resource_name = name.to_s.camelize
1426
1623
 
1427
- const_args = RUBY_VERSION < "1.9" ? [resource_name] : [resource_name, false]
1428
- if self.class.const_defined?(*const_args)
1624
+ const_args = [resource_name, false]
1625
+
1626
+ if !const_valid?(*const_args)
1627
+ # resource_name is not a valid ruby module name and cannot be created normally
1628
+ find_or_create_resource_for(:UnnamedResource)
1629
+ elsif self.class.const_defined?(*const_args)
1429
1630
  self.class.const_get(*const_args)
1430
1631
  else
1431
- ancestors = self.class.name.split("::")
1632
+ ancestors = self.class.name.to_s.split("::")
1432
1633
  if ancestors.size > 1
1433
1634
  find_or_create_resource_in_modules(resource_name, ancestors)
1434
1635
  else
@@ -1441,6 +1642,13 @@ module ActiveResource
1441
1642
  end
1442
1643
  end
1443
1644
 
1645
+ def const_valid?(*const_args)
1646
+ self.class.const_defined?(*const_args)
1647
+ true
1648
+ rescue NameError
1649
+ false
1650
+ end
1651
+
1444
1652
  # Create and return a class definition for a resource inside the current resource
1445
1653
  def create_resource_for(resource_name)
1446
1654
  resource = self.class.const_set(resource_name, Class.new(ActiveResource::Base))
@@ -1474,9 +1682,14 @@ module ActiveResource
1474
1682
 
1475
1683
  class Base
1476
1684
  extend ActiveModel::Naming
1477
- include CustomMethods, Observing, Validations
1685
+ extend ActiveResource::Associations
1686
+
1687
+ include Callbacks, CustomMethods, Validations
1478
1688
  include ActiveModel::Conversion
1479
1689
  include ActiveModel::Serializers::JSON
1480
1690
  include ActiveModel::Serializers::Xml
1691
+ include ActiveResource::Reflection
1481
1692
  end
1693
+
1694
+ ActiveSupport.run_load_hooks(:active_resource, Base)
1482
1695
  end