activeresource 3.2.22.5 → 5.1.1

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