activeresource-five 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,32 @@
1
+ module ActiveResource::Associations::Builder
2
+ class Association #:nodoc:
3
+
4
+ # providing a Class-Variable, which will have a different store of subclasses
5
+ class_attribute :valid_options
6
+ self.valid_options = [:class_name]
7
+
8
+ # would identify subclasses of association
9
+ class_attribute :macro
10
+
11
+ attr_reader :model, :name, :options, :klass
12
+
13
+ def self.build(model, name, options)
14
+ new(model, name, options).build
15
+ end
16
+
17
+ def initialize(model, name, options)
18
+ @model, @name, @options = model, name, options
19
+ end
20
+
21
+ def build
22
+ validate_options
23
+ model.create_reflection(self.class.macro, name, options)
24
+ end
25
+
26
+ private
27
+
28
+ def validate_options
29
+ options.assert_valid_keys(self.class.valid_options)
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,14 @@
1
+ module ActiveResource::Associations::Builder
2
+ class BelongsTo < Association
3
+ self.valid_options += [:foreign_key]
4
+
5
+ self.macro = :belongs_to
6
+
7
+ def build
8
+ validate_options
9
+ reflection = model.create_reflection(self.class.macro, name, options)
10
+ model.defines_belongs_to_finder_method(reflection)
11
+ return reflection
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,12 @@
1
+ module ActiveResource::Associations::Builder
2
+ class HasMany < Association
3
+ self.macro = :has_many
4
+
5
+ def build
6
+ validate_options
7
+ model.create_reflection(self.class.macro, name, options).tap do |reflection|
8
+ model.defines_has_many_finder_method(reflection)
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ module ActiveResource::Associations::Builder
2
+ class HasOne < Association
3
+ self.macro = :has_one
4
+
5
+ def build
6
+ validate_options
7
+ model.create_reflection(self.class.macro, name, options).tap do |reflection|
8
+ model.defines_has_one_finder_method(reflection)
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,1626 @@
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'
19
+ require 'active_resource/associations'
20
+ require 'active_resource/reflection'
21
+ require 'active_resource/threadsafe_attributes'
22
+
23
+ require 'active_model/serializers/xml'
24
+
25
+ module ActiveResource
26
+ # ActiveResource::Base is the main class for mapping RESTful resources as models in a Rails application.
27
+ #
28
+ # For an outline of what Active Resource is capable of, see its {README}[link:files/activeresource/README_rdoc.html].
29
+ #
30
+ # == Automated mapping
31
+ #
32
+ # Active Resource objects represent your RESTful resources as manipulatable Ruby objects. To map resources
33
+ # to Ruby objects, Active Resource only needs a class name that corresponds to the resource name (e.g., the class
34
+ # Person maps to the resources people, very similarly to Active Record) and a +site+ value, which holds the
35
+ # URI of the resources.
36
+ #
37
+ # class Person < ActiveResource::Base
38
+ # self.site = "https://api.people.com"
39
+ # end
40
+ #
41
+ # Now the Person class is mapped to RESTful resources located at <tt>https://api.people.com/people/</tt>, and
42
+ # you can now use Active Resource's life cycle methods to manipulate resources. In the case where you already have
43
+ # an existing model with the same name as the desired RESTful resource you can set the +element_name+ value.
44
+ #
45
+ # class PersonResource < ActiveResource::Base
46
+ # self.site = "https://api.people.com"
47
+ # self.element_name = "person"
48
+ # end
49
+ #
50
+ # If your Active Resource object is required to use an HTTP proxy you can set the +proxy+ value which holds a URI.
51
+ #
52
+ # class PersonResource < ActiveResource::Base
53
+ # self.site = "https://api.people.com"
54
+ # self.proxy = "https://user:password@proxy.people.com:8080"
55
+ # end
56
+ #
57
+ #
58
+ # == Life cycle methods
59
+ #
60
+ # Active Resource exposes methods for creating, finding, updating, and deleting resources
61
+ # from REST web services.
62
+ #
63
+ # ryan = Person.new(:first => 'Ryan', :last => 'Daigle')
64
+ # ryan.save # => true
65
+ # ryan.id # => 2
66
+ # Person.exists?(ryan.id) # => true
67
+ # ryan.exists? # => true
68
+ #
69
+ # ryan = Person.find(1)
70
+ # # Resource holding our newly created Person object
71
+ #
72
+ # ryan.first = 'Rizzle'
73
+ # ryan.save # => true
74
+ #
75
+ # ryan.destroy # => true
76
+ #
77
+ # As you can see, these are very similar to Active Record's life cycle methods for database records.
78
+ # You can read more about each of these methods in their respective documentation.
79
+ #
80
+ # === Custom REST methods
81
+ #
82
+ # Since simple CRUD/life cycle methods can't accomplish every task, Active Resource also supports
83
+ # defining your own custom REST methods. To invoke them, Active Resource provides the <tt>get</tt>,
84
+ # <tt>post</tt>, <tt>put</tt> and <tt>delete</tt> methods where you can specify a custom REST method
85
+ # name to invoke.
86
+ #
87
+ # # POST to the custom 'register' REST method, i.e. POST /people/new/register.json.
88
+ # Person.new(:name => 'Ryan').post(:register)
89
+ # # => { :id => 1, :name => 'Ryan', :position => 'Clerk' }
90
+ #
91
+ # # PUT an update by invoking the 'promote' REST method, i.e. PUT /people/1/promote.json?position=Manager.
92
+ # Person.find(1).put(:promote, :position => 'Manager')
93
+ # # => { :id => 1, :name => 'Ryan', :position => 'Manager' }
94
+ #
95
+ # # GET all the positions available, i.e. GET /people/positions.json.
96
+ # Person.get(:positions)
97
+ # # => [{:name => 'Manager'}, {:name => 'Clerk'}]
98
+ #
99
+ # # DELETE to 'fire' a person, i.e. DELETE /people/1/fire.json.
100
+ # Person.find(1).delete(:fire)
101
+ #
102
+ # For more information on using custom REST methods, see the
103
+ # ActiveResource::CustomMethods documentation.
104
+ #
105
+ # == Validations
106
+ #
107
+ # You can validate resources client side by overriding validation methods in the base class.
108
+ #
109
+ # class Person < ActiveResource::Base
110
+ # self.site = "https://api.people.com"
111
+ # protected
112
+ # def validate
113
+ # errors.add("last", "has invalid characters") unless last =~ /[a-zA-Z]*/
114
+ # end
115
+ # end
116
+ #
117
+ # See the ActiveResource::Validations documentation for more information.
118
+ #
119
+ # == Authentication
120
+ #
121
+ # Many REST APIs require authentication. The HTTP spec describes two ways to
122
+ # make requests with a username and password (see RFC 2617).
123
+ #
124
+ # Basic authentication simply sends a username and password along with HTTP
125
+ # requests. These sensitive credentials are sent unencrypted, visible to
126
+ # any onlooker, so this scheme should only be used with SSL.
127
+ #
128
+ # Digest authentication sends a crytographic hash of the username, password,
129
+ # HTTP method, URI, and a single-use secret key provided by the server.
130
+ # Sensitive credentials aren't visible to onlookers, so digest authentication
131
+ # doesn't require SSL. However, this doesn't mean the connection is secure!
132
+ # Just the username and password.
133
+ #
134
+ # (You really, really want to use SSL. There's little reason not to.)
135
+ #
136
+ # === Picking an authentication scheme
137
+ #
138
+ # Basic authentication is the default. To switch to digest authentication,
139
+ # set +auth_type+ to +:digest+:
140
+ #
141
+ # class Person < ActiveResource::Base
142
+ # self.auth_type = :digest
143
+ # end
144
+ #
145
+ # === Setting the username and password
146
+ #
147
+ # Set +user+ and +password+ on the class, or include them in the +site+ URL.
148
+ #
149
+ # class Person < ActiveResource::Base
150
+ # # Set user and password directly:
151
+ # self.user = "ryan"
152
+ # self.password = "password"
153
+ #
154
+ # # Or include them in the site:
155
+ # self.site = "https://ryan:password@api.people.com"
156
+ # end
157
+ #
158
+ # === Certificate Authentication
159
+ #
160
+ # You can also authenticate using an X509 certificate. <tt>See ssl_options=</tt> for all options.
161
+ #
162
+ # class Person < ActiveResource::Base
163
+ # self.site = "https://secure.api.people.com/"
164
+ #
165
+ # File.open(pem_file_path, 'rb') do |pem_file|
166
+ # self.ssl_options = {
167
+ # cert: OpenSSL::X509::Certificate.new(pem_file),
168
+ # key: OpenSSL::PKey::RSA.new(pem_file),
169
+ # ca_path: "/path/to/OpenSSL/formatted/CA_Certs",
170
+ # verify_mode: OpenSSL::SSL::VERIFY_PEER }
171
+ # end
172
+ # end
173
+ #
174
+ #
175
+ # == Errors & Validation
176
+ #
177
+ # Error handling and validation is handled in much the same manner as you're used to seeing in
178
+ # Active Record. Both the response code in the HTTP response and the body of the response are used to
179
+ # indicate that an error occurred.
180
+ #
181
+ # === Resource errors
182
+ #
183
+ # When a GET is requested for a resource that does not exist, the HTTP <tt>404</tt> (Resource Not Found)
184
+ # response code will be returned from the server which will raise an ActiveResource::ResourceNotFound
185
+ # exception.
186
+ #
187
+ # # GET https://api.people.com/people/999.json
188
+ # ryan = Person.find(999) # 404, raises ActiveResource::ResourceNotFound
189
+ #
190
+ #
191
+ # <tt>404</tt> is just one of the HTTP error response codes that Active Resource will handle with its own exception. The
192
+ # following HTTP response codes will also result in these exceptions:
193
+ #
194
+ # * 200..399 - Valid response. No exceptions, other than these redirects:
195
+ # * 301, 302, 303, 307 - ActiveResource::Redirection
196
+ # * 400 - ActiveResource::BadRequest
197
+ # * 401 - ActiveResource::UnauthorizedAccess
198
+ # * 403 - ActiveResource::ForbiddenAccess
199
+ # * 404 - ActiveResource::ResourceNotFound
200
+ # * 405 - ActiveResource::MethodNotAllowed
201
+ # * 409 - ActiveResource::ResourceConflict
202
+ # * 410 - ActiveResource::ResourceGone
203
+ # * 422 - ActiveResource::ResourceInvalid (rescued by save as validation errors)
204
+ # * 401..499 - ActiveResource::ClientError
205
+ # * 500..599 - ActiveResource::ServerError
206
+ # * Other - ActiveResource::ConnectionError
207
+ #
208
+ # These custom exceptions allow you to deal with resource errors more naturally and with more precision
209
+ # rather than returning a general HTTP error. For example:
210
+ #
211
+ # begin
212
+ # ryan = Person.find(my_id)
213
+ # rescue ActiveResource::ResourceNotFound
214
+ # redirect_to :action => 'not_found'
215
+ # rescue ActiveResource::ResourceConflict, ActiveResource::ResourceInvalid
216
+ # redirect_to :action => 'new'
217
+ # end
218
+ #
219
+ # When a GET is requested for a nested resource and you don't provide the prefix_param
220
+ # an ActiveResource::MissingPrefixParam will be raised.
221
+ #
222
+ # class Comment < ActiveResource::Base
223
+ # self.site = "https://someip.com/posts/:post_id"
224
+ # end
225
+ #
226
+ # Comment.find(1)
227
+ # # => ActiveResource::MissingPrefixParam: post_id prefix_option is missing
228
+ #
229
+ # === Validation errors
230
+ #
231
+ # Active Resource supports validations on resources and will return errors if any of these validations fail
232
+ # (e.g., "First name can not be blank" and so on). These types of errors are denoted in the response by
233
+ # a response code of <tt>422</tt> and an JSON or XML representation of the validation errors. The save operation will
234
+ # then fail (with a <tt>false</tt> return value) and the validation errors can be accessed on the resource in question.
235
+ #
236
+ # ryan = Person.find(1)
237
+ # ryan.first # => ''
238
+ # ryan.save # => false
239
+ #
240
+ # # When
241
+ # # PUT https://api.people.com/people/1.xml
242
+ # # or
243
+ # # PUT https://api.people.com/people/1.json
244
+ # # is requested with invalid values, the response is:
245
+ # #
246
+ # # Response (422):
247
+ # # <errors><error>First cannot be empty</error></errors>
248
+ # # or
249
+ # # {"errors":{"first":["cannot be empty"]}}
250
+ # #
251
+ #
252
+ # ryan.errors.invalid?(:first) # => true
253
+ # ryan.errors.full_messages # => ['First cannot be empty']
254
+ #
255
+ # For backwards-compatibility with older endpoints, the following formats are also supported in JSON responses:
256
+ #
257
+ # # {"errors":['First cannot be empty']}
258
+ # # This was the required format for previous versions of ActiveResource
259
+ # # {"first":["cannot be empty"]}
260
+ # # This was the default format produced by respond_with in ActionController <3.2.1
261
+ #
262
+ # Parsing either of these formats will result in a deprecation warning.
263
+ #
264
+ # Learn more about Active Resource's validation features in the ActiveResource::Validations documentation.
265
+ #
266
+ # === Timeouts
267
+ #
268
+ # Active Resource relies on HTTP to access RESTful APIs and as such is inherently susceptible to slow or
269
+ # unresponsive servers. In such cases, your Active Resource method calls could \timeout. You can control the
270
+ # amount of time before Active Resource times out with the +timeout+ variable.
271
+ #
272
+ # class Person < ActiveResource::Base
273
+ # self.site = "https://api.people.com"
274
+ # self.timeout = 5
275
+ # end
276
+ #
277
+ # This sets the +timeout+ to 5 seconds. You can adjust the +timeout+ to a value suitable for the RESTful API
278
+ # you are accessing. It is recommended to set this to a reasonably low value to allow your Active Resource
279
+ # clients (especially if you are using Active Resource in a Rails application) to fail-fast (see
280
+ # http://en.wikipedia.org/wiki/Fail-fast) rather than cause cascading failures that could incapacitate your
281
+ # server.
282
+ #
283
+ # When a \timeout occurs, an ActiveResource::TimeoutError is raised. You should rescue from
284
+ # ActiveResource::TimeoutError in your Active Resource method calls.
285
+ #
286
+ # Internally, Active Resource relies on Ruby's Net::HTTP library to make HTTP requests. Setting +timeout+
287
+ # sets the <tt>read_timeout</tt> of the internal Net::HTTP instance to the same value. The default
288
+ # <tt>read_timeout</tt> is 60 seconds on most Ruby implementations.
289
+ #
290
+ # Active Resource also supports distinct +open_timeout+ (time to connect) and +read_timeout+ (how long to
291
+ # wait for an upstream response). This is inline with supported +Net::HTTP+ timeout configuration and allows
292
+ # for finer control of client timeouts depending on context.
293
+ #
294
+ # class Person < ActiveResource::Base
295
+ # self.site = "https://api.people.com"
296
+ # self.open_timeout = 2
297
+ # self.read_timeout = 10
298
+ # end
299
+ class Base
300
+ ##
301
+ # :singleton-method:
302
+ # The logger for diagnosing and tracing Active Resource calls.
303
+ cattr_accessor :logger
304
+
305
+ class_attribute :_format
306
+ class_attribute :_collection_parser
307
+ class_attribute :include_format_in_path
308
+ self.include_format_in_path = true
309
+
310
+ class_attribute :connection_class
311
+ self.connection_class = Connection
312
+
313
+ class << self
314
+ include ThreadsafeAttributes
315
+ threadsafe_attribute :_headers, :_connection, :_user, :_password, :_site, :_proxy
316
+
317
+ # Creates a schema for this resource - setting the attributes that are
318
+ # known prior to fetching an instance from the remote system.
319
+ #
320
+ # The schema helps define the set of <tt>known_attributes</tt> of the
321
+ # current resource.
322
+ #
323
+ # There is no need to specify a schema for your Active Resource. If
324
+ # you do not, the <tt>known_attributes</tt> will be guessed from the
325
+ # instance attributes returned when an instance is fetched from the
326
+ # remote system.
327
+ #
328
+ # example:
329
+ # class Person < ActiveResource::Base
330
+ # schema do
331
+ # # define each attribute separately
332
+ # attribute 'name', :string
333
+ #
334
+ # # or use the convenience methods and pass >=1 attribute names
335
+ # string 'eye_color', 'hair_color'
336
+ # integer 'age'
337
+ # float 'height', 'weight'
338
+ #
339
+ # # unsupported types should be left as strings
340
+ # # overload the accessor methods if you need to convert them
341
+ # attribute 'created_at', 'string'
342
+ # end
343
+ # end
344
+ #
345
+ # p = Person.new
346
+ # p.respond_to? :name # => true
347
+ # p.respond_to? :age # => true
348
+ # p.name # => nil
349
+ # p.age # => nil
350
+ #
351
+ # j = Person.find_by_name('John')
352
+ # <person><name>John</name><age>34</age><num_children>3</num_children></person>
353
+ # j.respond_to? :name # => true
354
+ # j.respond_to? :age # => true
355
+ # j.name # => 'John'
356
+ # j.age # => '34' # note this is a string!
357
+ # j.num_children # => '3' # note this is a string!
358
+ #
359
+ # p.num_children # => NoMethodError
360
+ #
361
+ # Attribute-types must be one of: <tt>string, text, integer, float, decimal, datetime, timestamp, time, date, binary, boolean</tt>
362
+ #
363
+ # Note: at present the attribute-type doesn't do anything, but stay
364
+ # tuned...
365
+ # Shortly it will also *cast* the value of the returned attribute.
366
+ # ie:
367
+ # j.age # => 34 # cast to an integer
368
+ # j.weight # => '65' # still a string!
369
+ #
370
+ def schema(&block)
371
+ if block_given?
372
+ schema_definition = Schema.new
373
+ schema_definition.instance_eval(&block)
374
+
375
+ # skip out if we didn't define anything
376
+ return unless schema_definition.attrs.present?
377
+
378
+ @schema ||= {}.with_indifferent_access
379
+ @known_attributes ||= []
380
+
381
+ schema_definition.attrs.each do |k,v|
382
+ @schema[k] = v
383
+ @known_attributes << k
384
+ end
385
+
386
+ @schema
387
+ else
388
+ @schema ||= nil
389
+ end
390
+ end
391
+
392
+ # Alternative, direct way to specify a <tt>schema</tt> for this
393
+ # Resource. <tt>schema</tt> is more flexible, but this is quick
394
+ # for a very simple schema.
395
+ #
396
+ # Pass the schema as a hash with the keys being the attribute-names
397
+ # and the value being one of the accepted attribute types (as defined
398
+ # in <tt>schema</tt>)
399
+ #
400
+ # example:
401
+ #
402
+ # class Person < ActiveResource::Base
403
+ # schema = {'name' => :string, 'age' => :integer }
404
+ # end
405
+ #
406
+ # The keys/values can be strings or symbols. They will be converted to
407
+ # strings.
408
+ #
409
+ def schema=(the_schema)
410
+ unless the_schema.present?
411
+ # purposefully nulling out the schema
412
+ @schema = nil
413
+ @known_attributes = []
414
+ return
415
+ end
416
+
417
+ raise ArgumentError, "Expected a hash" unless the_schema.kind_of? Hash
418
+
419
+ schema do
420
+ the_schema.each {|k,v| attribute(k,v) }
421
+ end
422
+ end
423
+
424
+ # Returns the list of known attributes for this resource, gathered
425
+ # from the provided <tt>schema</tt>
426
+ # Attributes that are known will cause your resource to return 'true'
427
+ # when <tt>respond_to?</tt> is called on them. A known attribute will
428
+ # return nil if not set (rather than <tt>MethodNotFound</tt>); thus
429
+ # known attributes can be used with <tt>validates_presence_of</tt>
430
+ # without a getter-method.
431
+ def known_attributes
432
+ @known_attributes ||= []
433
+ end
434
+
435
+ # Gets the URI of the REST resources to map for this class. The site variable is required for
436
+ # Active Resource's mapping to work.
437
+ def site
438
+ # Not using superclass_delegating_reader because don't want subclasses to modify superclass instance
439
+ #
440
+ # With superclass_delegating_reader
441
+ #
442
+ # Parent.site = 'https://anonymous@test.com'
443
+ # Subclass.site # => 'https://anonymous@test.com'
444
+ # Subclass.site.user = 'david'
445
+ # Parent.site # => 'https://david@test.com'
446
+ #
447
+ # Without superclass_delegating_reader (expected behavior)
448
+ #
449
+ # Parent.site = 'https://anonymous@test.com'
450
+ # Subclass.site # => 'https://anonymous@test.com'
451
+ # Subclass.site.user = 'david' # => TypeError: can't modify frozen object
452
+ #
453
+ if _site_defined?
454
+ _site
455
+ elsif superclass != Object && superclass.site
456
+ superclass.site.dup.freeze
457
+ end
458
+ end
459
+
460
+ # Sets the URI of the REST resources to map for this class to the value in the +site+ argument.
461
+ # The site variable is required for Active Resource's mapping to work.
462
+ def site=(site)
463
+ self._connection = nil
464
+ if site.nil?
465
+ self._site = nil
466
+ else
467
+ self._site = create_site_uri_from(site)
468
+ self._user = URI.parser.unescape(_site.user) if _site.user
469
+ self._password = URI.parser.unescape(_site.password) if _site.password
470
+ end
471
+ end
472
+
473
+ # Gets the \proxy variable if a proxy is required
474
+ def proxy
475
+ # Not using superclass_delegating_reader. See +site+ for explanation
476
+ if _proxy_defined?
477
+ _proxy
478
+ elsif superclass != Object && superclass.proxy
479
+ superclass.proxy.dup.freeze
480
+ end
481
+ end
482
+
483
+ # Sets the URI of the http proxy to the value in the +proxy+ argument.
484
+ def proxy=(proxy)
485
+ self._connection = nil
486
+ self._proxy = proxy.nil? ? nil : create_proxy_uri_from(proxy)
487
+ end
488
+
489
+ # Gets the \user for REST HTTP authentication.
490
+ def user
491
+ # Not using superclass_delegating_reader. See +site+ for explanation
492
+ if _user_defined?
493
+ _user
494
+ elsif superclass != Object && superclass.user
495
+ superclass.user.dup.freeze
496
+ end
497
+ end
498
+
499
+ # Sets the \user for REST HTTP authentication.
500
+ def user=(user)
501
+ self._connection = nil
502
+ self._user = user
503
+ end
504
+
505
+ # Gets the \password for REST HTTP authentication.
506
+ def password
507
+ # Not using superclass_delegating_reader. See +site+ for explanation
508
+ if _password_defined?
509
+ _password
510
+ elsif superclass != Object && superclass.password
511
+ superclass.password.dup.freeze
512
+ end
513
+ end
514
+
515
+ # Sets the \password for REST HTTP authentication.
516
+ def password=(password)
517
+ self._connection = nil
518
+ self._password = password
519
+ end
520
+
521
+ def auth_type
522
+ if defined?(@auth_type)
523
+ @auth_type
524
+ end
525
+ end
526
+
527
+ def auth_type=(auth_type)
528
+ self._connection = nil
529
+ @auth_type = auth_type
530
+ end
531
+
532
+ # Sets the format that attributes are sent and received in from a mime type reference:
533
+ #
534
+ # Person.format = :json
535
+ # Person.find(1) # => GET /people/1.json
536
+ #
537
+ # Person.format = ActiveResource::Formats::XmlFormat
538
+ # Person.find(1) # => GET /people/1.xml
539
+ #
540
+ # Default format is <tt>:json</tt>.
541
+ def format=(mime_type_reference_or_format)
542
+ format = mime_type_reference_or_format.is_a?(Symbol) ?
543
+ ActiveResource::Formats[mime_type_reference_or_format] : mime_type_reference_or_format
544
+
545
+ self._format = format
546
+ connection.format = format if site
547
+ end
548
+
549
+ # Returns the current format, default is ActiveResource::Formats::JsonFormat.
550
+ def format
551
+ self._format || ActiveResource::Formats::JsonFormat
552
+ end
553
+
554
+ # Sets the parser to use when a collection is returned. The parser must be Enumerable.
555
+ def collection_parser=(parser_instance)
556
+ parser_instance = parser_instance.constantize if parser_instance.is_a?(String)
557
+ self._collection_parser = parser_instance
558
+ end
559
+
560
+ def collection_parser
561
+ self._collection_parser || ActiveResource::Collection
562
+ end
563
+
564
+ # Sets the number of seconds after which requests to the REST API should time out.
565
+ def timeout=(timeout)
566
+ self._connection = nil
567
+ @timeout = timeout
568
+ end
569
+
570
+ # Sets the number of seconds after which connection attempts to the REST API should time out.
571
+ def open_timeout=(timeout)
572
+ self._connection = nil
573
+ @open_timeout = timeout
574
+ end
575
+
576
+ # Sets the number of seconds after which reads to the REST API should time out.
577
+ def read_timeout=(timeout)
578
+ self._connection = nil
579
+ @read_timeout = timeout
580
+ end
581
+
582
+ # Gets the number of seconds after which requests to the REST API should time out.
583
+ def timeout
584
+ if defined?(@timeout)
585
+ @timeout
586
+ elsif superclass != Object && superclass.timeout
587
+ superclass.timeout
588
+ end
589
+ end
590
+
591
+ # Gets the number of seconds after which connection attempts to the REST API should time out.
592
+ def open_timeout
593
+ if defined?(@open_timeout)
594
+ @open_timeout
595
+ elsif superclass != Object && superclass.open_timeout
596
+ superclass.open_timeout
597
+ end
598
+ end
599
+
600
+ # Gets the number of seconds after which reads to the REST API should time out.
601
+ def read_timeout
602
+ if defined?(@read_timeout)
603
+ @read_timeout
604
+ elsif superclass != Object && superclass.read_timeout
605
+ superclass.read_timeout
606
+ end
607
+ end
608
+
609
+ # Options that will get applied to an SSL connection.
610
+ #
611
+ # * <tt>:key</tt> - An OpenSSL::PKey::RSA or OpenSSL::PKey::DSA object.
612
+ # * <tt>:cert</tt> - An OpenSSL::X509::Certificate object as client certificate
613
+ # * <tt>:ca_file</tt> - Path to a CA certification file in PEM format. The file can contain several CA certificates.
614
+ # * <tt>:ca_path</tt> - Path of a CA certification directory containing certifications in PEM format.
615
+ # * <tt>:verify_mode</tt> - Flags for server the certification verification at beginning of SSL/TLS session. (OpenSSL::SSL::VERIFY_NONE or OpenSSL::SSL::VERIFY_PEER is acceptable)
616
+ # * <tt>:verify_callback</tt> - The verify callback for the server certification verification.
617
+ # * <tt>:verify_depth</tt> - The maximum depth for the certificate chain verification.
618
+ # * <tt>:cert_store</tt> - OpenSSL::X509::Store to verify peer certificate.
619
+ # * <tt>:ssl_timeout</tt> -The SSL timeout in seconds.
620
+ def ssl_options=(options)
621
+ self._connection = nil
622
+ @ssl_options = options
623
+ end
624
+
625
+ # Returns the SSL options hash.
626
+ def ssl_options
627
+ if defined?(@ssl_options)
628
+ @ssl_options
629
+ elsif superclass != Object && superclass.ssl_options
630
+ superclass.ssl_options
631
+ end
632
+ end
633
+
634
+ # An instance of ActiveResource::Connection that is the base \connection to the remote service.
635
+ # The +refresh+ parameter toggles whether or not the \connection is refreshed at every request
636
+ # or not (defaults to <tt>false</tt>).
637
+ def connection(refresh = false)
638
+ if _connection_defined? || superclass == Object
639
+ self._connection = connection_class.new(site, format) if refresh || _connection.nil?
640
+ _connection.proxy = proxy if proxy
641
+ _connection.user = user if user
642
+ _connection.password = password if password
643
+ _connection.auth_type = auth_type if auth_type
644
+ _connection.timeout = timeout if timeout
645
+ _connection.open_timeout = open_timeout if open_timeout
646
+ _connection.read_timeout = read_timeout if read_timeout
647
+ _connection.ssl_options = ssl_options if ssl_options
648
+ _connection
649
+ else
650
+ superclass.connection
651
+ end
652
+ end
653
+
654
+ def headers
655
+ headers_state = self._headers || {}
656
+ if superclass != Object
657
+ self._headers = superclass.headers.merge(headers_state)
658
+ else
659
+ headers_state
660
+ end
661
+ end
662
+
663
+ attr_writer :element_name
664
+
665
+ def element_name
666
+ @element_name ||= model_name.element
667
+ end
668
+
669
+ attr_writer :collection_name
670
+
671
+ def collection_name
672
+ @collection_name ||= ActiveSupport::Inflector.pluralize(element_name)
673
+ end
674
+
675
+ attr_writer :primary_key
676
+
677
+ def primary_key
678
+ if defined?(@primary_key)
679
+ @primary_key
680
+ elsif superclass != Object && superclass.primary_key
681
+ primary_key = superclass.primary_key
682
+ return primary_key if primary_key.is_a?(Symbol)
683
+ primary_key.dup.freeze
684
+ else
685
+ 'id'
686
+ end
687
+ end
688
+
689
+ # Gets the \prefix for a resource's nested URL (e.g., <tt>prefix/collectionname/1.json</tt>)
690
+ # This method is regenerated at runtime based on what the \prefix is set to.
691
+ def prefix(options={})
692
+ default = site.path
693
+ default << '/' unless default[-1..-1] == '/'
694
+ # generate the actual method based on the current site path
695
+ self.prefix = default
696
+ prefix(options)
697
+ end
698
+
699
+ # An attribute reader for the source string for the resource path \prefix. This
700
+ # method is regenerated at runtime based on what the \prefix is set to.
701
+ def prefix_source
702
+ prefix # generate #prefix and #prefix_source methods first
703
+ prefix_source
704
+ end
705
+
706
+ # Sets the \prefix for a resource's nested URL (e.g., <tt>prefix/collectionname/1.json</tt>).
707
+ # Default value is <tt>site.path</tt>.
708
+ def prefix=(value = '/')
709
+ # Replace :placeholders with '#{embedded options[:lookups]}'
710
+ prefix_call = value.gsub(/:\w+/) { |key| "\#{URI.parser.escape options[#{key}].to_s}" }
711
+
712
+ # Clear prefix parameters in case they have been cached
713
+ @prefix_parameters = nil
714
+
715
+ silence_warnings do
716
+ # Redefine the new methods.
717
+ instance_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
718
+ def prefix_source() "#{value}" end
719
+ def prefix(options={}) "#{prefix_call}" end
720
+ RUBY_EVAL
721
+ end
722
+ rescue Exception => e
723
+ logger.error "Couldn't set prefix: #{e}\n #{code}" if logger
724
+ raise
725
+ end
726
+
727
+ alias_method :set_prefix, :prefix= #:nodoc:
728
+
729
+ alias_method :set_element_name, :element_name= #:nodoc:
730
+ alias_method :set_collection_name, :collection_name= #:nodoc:
731
+
732
+ def format_extension
733
+ include_format_in_path ? ".#{format.extension}" : ""
734
+ end
735
+
736
+ # Gets the element path for the given ID in +id+. If the +query_options+ parameter is omitted, Rails
737
+ # will split from the \prefix options.
738
+ #
739
+ # ==== Options
740
+ # +prefix_options+ - A \hash to add a \prefix to the request for nested URLs (e.g., <tt>:account_id => 19</tt>
741
+ # would yield a URL like <tt>/accounts/19/purchases.json</tt>).
742
+ #
743
+ # +query_options+ - A \hash to add items to the query string for the request.
744
+ #
745
+ # ==== Examples
746
+ # Post.element_path(1)
747
+ # # => /posts/1.json
748
+ #
749
+ # class Comment < ActiveResource::Base
750
+ # self.site = "https://37s.sunrise.com/posts/:post_id"
751
+ # end
752
+ #
753
+ # Comment.element_path(1, :post_id => 5)
754
+ # # => /posts/5/comments/1.json
755
+ #
756
+ # Comment.element_path(1, :post_id => 5, :active => 1)
757
+ # # => /posts/5/comments/1.json?active=1
758
+ #
759
+ # Comment.element_path(1, {:post_id => 5}, {:active => 1})
760
+ # # => /posts/5/comments/1.json?active=1
761
+ #
762
+ def element_path(id, prefix_options = {}, query_options = nil)
763
+ check_prefix_options(prefix_options)
764
+
765
+ prefix_options, query_options = split_options(prefix_options) if query_options.nil?
766
+ "#{prefix(prefix_options)}#{collection_name}/#{URI.parser.escape id.to_s}#{format_extension}#{query_string(query_options)}"
767
+ end
768
+
769
+ # Gets the new element path for REST resources.
770
+ #
771
+ # ==== Options
772
+ # * +prefix_options+ - A hash to add a prefix to the request for nested URLs (e.g., <tt>:account_id => 19</tt>
773
+ # would yield a URL like <tt>/accounts/19/purchases/new.json</tt>).
774
+ #
775
+ # ==== Examples
776
+ # Post.new_element_path
777
+ # # => /posts/new.json
778
+ #
779
+ # class Comment < ActiveResource::Base
780
+ # self.site = "https://37s.sunrise.com/posts/:post_id"
781
+ # end
782
+ #
783
+ # Comment.collection_path(:post_id => 5)
784
+ # # => /posts/5/comments/new.json
785
+ def new_element_path(prefix_options = {})
786
+ "#{prefix(prefix_options)}#{collection_name}/new#{format_extension}"
787
+ end
788
+
789
+ # Gets the collection path for the REST resources. If the +query_options+ parameter is omitted, Rails
790
+ # will split from the +prefix_options+.
791
+ #
792
+ # ==== Options
793
+ # * +prefix_options+ - A hash to add a prefix to the request for nested URLs (e.g., <tt>:account_id => 19</tt>
794
+ # would yield a URL like <tt>/accounts/19/purchases.json</tt>).
795
+ # * +query_options+ - A hash to add items to the query string for the request.
796
+ #
797
+ # ==== Examples
798
+ # Post.collection_path
799
+ # # => /posts.json
800
+ #
801
+ # Comment.collection_path(:post_id => 5)
802
+ # # => /posts/5/comments.json
803
+ #
804
+ # Comment.collection_path(:post_id => 5, :active => 1)
805
+ # # => /posts/5/comments.json?active=1
806
+ #
807
+ # Comment.collection_path({:post_id => 5}, {:active => 1})
808
+ # # => /posts/5/comments.json?active=1
809
+ #
810
+ def collection_path(prefix_options = {}, query_options = nil)
811
+ check_prefix_options(prefix_options)
812
+ prefix_options, query_options = split_options(prefix_options) if query_options.nil?
813
+ "#{prefix(prefix_options)}#{collection_name}#{format_extension}#{query_string(query_options)}"
814
+ end
815
+
816
+ alias_method :set_primary_key, :primary_key= #:nodoc:
817
+
818
+ # Builds a new, unsaved record using the default values from the remote server so
819
+ # that it can be used with RESTful forms.
820
+ #
821
+ # ==== Options
822
+ # * +attributes+ - A hash that overrides the default values from the server.
823
+ #
824
+ # Returns the new resource instance.
825
+ #
826
+ def build(attributes = {})
827
+ attrs = self.format.decode(connection.get("#{new_element_path(attributes)}", headers).body).merge(attributes)
828
+ self.new(attrs)
829
+ end
830
+
831
+ # Creates a new resource instance and makes a request to the remote service
832
+ # that it be saved, making it equivalent to the following simultaneous calls:
833
+ #
834
+ # ryan = Person.new(:first => 'ryan')
835
+ # ryan.save
836
+ #
837
+ # Returns the newly created resource. If a failure has occurred an
838
+ # exception will be raised (see <tt>save</tt>). If the resource is invalid and
839
+ # has not been saved then <tt>valid?</tt> will return <tt>false</tt>,
840
+ # while <tt>new?</tt> will still return <tt>true</tt>.
841
+ #
842
+ # ==== Examples
843
+ # Person.create(:name => 'Jeremy', :email => 'myname@nospam.com', :enabled => true)
844
+ # my_person = Person.find(:first)
845
+ # my_person.email # => myname@nospam.com
846
+ #
847
+ # dhh = Person.create(:name => 'David', :email => 'dhh@nospam.com', :enabled => true)
848
+ # dhh.valid? # => true
849
+ # dhh.new? # => false
850
+ #
851
+ # # We'll assume that there's a validation that requires the name attribute
852
+ # that_guy = Person.create(:name => '', :email => 'thatguy@nospam.com', :enabled => true)
853
+ # that_guy.valid? # => false
854
+ # that_guy.new? # => true
855
+ def create(attributes = {})
856
+ self.new(attributes).tap { |resource| resource.save }
857
+ end
858
+
859
+ # Creates a new resource (just like <tt>create</tt>) and makes a request to the
860
+ # remote service that it be saved, but runs validations and raises
861
+ # <tt>ActiveResource::ResourceInvalid</tt>, making it equivalent to the following
862
+ # simultaneous calls:
863
+ #
864
+ # ryan = Person.new(:first => 'ryan')
865
+ # ryan.save!
866
+ def create!(attributes = {})
867
+ self.new(attributes).tap { |resource| resource.save! }
868
+ end
869
+
870
+ # Core method for finding resources. Used similarly to Active Record's +find+ method.
871
+ #
872
+ # ==== Arguments
873
+ # The first argument is considered to be the scope of the query. That is, how many
874
+ # resources are returned from the request. It can be one of the following.
875
+ #
876
+ # * <tt>:one</tt> - Returns a single resource.
877
+ # * <tt>:first</tt> - Returns the first resource found.
878
+ # * <tt>:last</tt> - Returns the last resource found.
879
+ # * <tt>:all</tt> - Returns every resource that matches the request.
880
+ #
881
+ # ==== Options
882
+ #
883
+ # * <tt>:from</tt> - Sets the path or custom method that resources will be fetched from.
884
+ # * <tt>:params</tt> - Sets query and \prefix (nested URL) parameters.
885
+ #
886
+ # ==== Examples
887
+ # Person.find(1)
888
+ # # => GET /people/1.json
889
+ #
890
+ # Person.find(:all)
891
+ # # => GET /people.json
892
+ #
893
+ # Person.find(:all, :params => { :title => "CEO" })
894
+ # # => GET /people.json?title=CEO
895
+ #
896
+ # Person.find(:first, :from => :managers)
897
+ # # => GET /people/managers.json
898
+ #
899
+ # Person.find(:last, :from => :managers)
900
+ # # => GET /people/managers.json
901
+ #
902
+ # Person.find(:all, :from => "/companies/1/people.json")
903
+ # # => GET /companies/1/people.json
904
+ #
905
+ # Person.find(:one, :from => :leader)
906
+ # # => GET /people/leader.json
907
+ #
908
+ # Person.find(:all, :from => :developers, :params => { :language => 'ruby' })
909
+ # # => GET /people/developers.json?language=ruby
910
+ #
911
+ # Person.find(:one, :from => "/companies/1/manager.json")
912
+ # # => GET /companies/1/manager.json
913
+ #
914
+ # StreetAddress.find(1, :params => { :person_id => 1 })
915
+ # # => GET /people/1/street_addresses/1.json
916
+ #
917
+ # == Failure or missing data
918
+ # A failure to find the requested object raises a ResourceNotFound
919
+ # exception if the find was called with an id.
920
+ # With any other scope, find returns nil when no data is returned.
921
+ #
922
+ # Person.find(1)
923
+ # # => raises ResourceNotFound
924
+ #
925
+ # Person.find(:all)
926
+ # Person.find(:first)
927
+ # Person.find(:last)
928
+ # # => nil
929
+ def find(*arguments)
930
+ scope = arguments.slice!(0)
931
+ options = arguments.slice!(0) || {}
932
+
933
+ case scope
934
+ when :all then find_every(options)
935
+ when :first then find_every(options).to_a.first
936
+ when :last then find_every(options).to_a.last
937
+ when :one then find_one(options)
938
+ else find_single(scope, options)
939
+ end
940
+ end
941
+
942
+
943
+ # A convenience wrapper for <tt>find(:first, *args)</tt>. You can pass
944
+ # in all the same arguments to this method as you can to
945
+ # <tt>find(:first)</tt>.
946
+ def first(*args)
947
+ find(:first, *args)
948
+ end
949
+
950
+ # A convenience wrapper for <tt>find(:last, *args)</tt>. You can pass
951
+ # in all the same arguments to this method as you can to
952
+ # <tt>find(:last)</tt>.
953
+ def last(*args)
954
+ find(:last, *args)
955
+ end
956
+
957
+ # This is an alias for find(:all). You can pass in all the same
958
+ # arguments to this method as you can to <tt>find(:all)</tt>
959
+ def all(*args)
960
+ find(:all, *args)
961
+ end
962
+
963
+ def where(clauses = {})
964
+ raise ArgumentError, "expected a clauses Hash, got #{clauses.inspect}" unless clauses.is_a? Hash
965
+ find(:all, :params => clauses)
966
+ end
967
+
968
+
969
+ # Deletes the resources with the ID in the +id+ parameter.
970
+ #
971
+ # ==== Options
972
+ # All options specify \prefix and query parameters.
973
+ #
974
+ # ==== Examples
975
+ # Event.delete(2) # sends DELETE /events/2
976
+ #
977
+ # Event.create(:name => 'Free Concert', :location => 'Community Center')
978
+ # my_event = Event.find(:first) # let's assume this is event with ID 7
979
+ # Event.delete(my_event.id) # sends DELETE /events/7
980
+ #
981
+ # # Let's assume a request to events/5/cancel.json
982
+ # Event.delete(params[:id]) # sends DELETE /events/5
983
+ def delete(id, options = {})
984
+ connection.delete(element_path(id, options), headers)
985
+ end
986
+
987
+ # Asserts the existence of a resource, returning <tt>true</tt> if the resource is found.
988
+ #
989
+ # ==== Examples
990
+ # Note.create(:title => 'Hello, world.', :body => 'Nothing more for now...')
991
+ # Note.exists?(1) # => true
992
+ #
993
+ # Note.exists(1349) # => false
994
+ def exists?(id, options = {})
995
+ if id
996
+ prefix_options, query_options = split_options(options[:params])
997
+ path = element_path(id, prefix_options, query_options)
998
+ response = connection.head(path, headers)
999
+ (200..206).include? response.code
1000
+ end
1001
+ # id && !find_single(id, options).nil?
1002
+ rescue ActiveResource::ResourceNotFound, ActiveResource::ResourceGone
1003
+ false
1004
+ end
1005
+
1006
+ private
1007
+
1008
+ def check_prefix_options(prefix_options)
1009
+ p_options = HashWithIndifferentAccess.new(prefix_options)
1010
+ prefix_parameters.each do |p|
1011
+ raise(MissingPrefixParam, "#{p} prefix_option is missing") if p_options[p].blank?
1012
+ end
1013
+ end
1014
+
1015
+ # Find every resource
1016
+ def find_every(options)
1017
+ begin
1018
+ case from = options[:from]
1019
+ when Symbol
1020
+ instantiate_collection(get(from, options[:params]), options[:params])
1021
+ when String
1022
+ path = "#{from}#{query_string(options[:params])}"
1023
+ instantiate_collection(format.decode(connection.get(path, headers).body) || [], options[:params])
1024
+ else
1025
+ prefix_options, query_options = split_options(options[:params])
1026
+ path = collection_path(prefix_options, query_options)
1027
+ instantiate_collection( (format.decode(connection.get(path, headers).body) || []), query_options, prefix_options )
1028
+ end
1029
+ rescue ActiveResource::ResourceNotFound
1030
+ # Swallowing ResourceNotFound exceptions and return nil - as per
1031
+ # ActiveRecord.
1032
+ nil
1033
+ end
1034
+ end
1035
+
1036
+ # Find a single resource from a one-off URL
1037
+ def find_one(options)
1038
+ case from = options[:from]
1039
+ when Symbol
1040
+ instantiate_record(get(from, options[:params]))
1041
+ when String
1042
+ path = "#{from}#{query_string(options[:params])}"
1043
+ instantiate_record(format.decode(connection.get(path, headers).body))
1044
+ end
1045
+ end
1046
+
1047
+ # Find a single resource from the default URL
1048
+ def find_single(scope, options)
1049
+ prefix_options, query_options = split_options(options[:params])
1050
+ path = element_path(scope, prefix_options, query_options)
1051
+ instantiate_record(format.decode(connection.get(path, headers).body), prefix_options)
1052
+ end
1053
+
1054
+ def instantiate_collection(collection, original_params = {}, prefix_options = {})
1055
+ collection_parser.new(collection).tap do |parser|
1056
+ parser.resource_class = self
1057
+ parser.original_params = original_params
1058
+ end.collect! { |record| instantiate_record(record, prefix_options) }
1059
+ end
1060
+
1061
+ def instantiate_record(record, prefix_options = {})
1062
+ new(record, true).tap do |resource|
1063
+ resource.prefix_options = prefix_options
1064
+ end
1065
+ end
1066
+
1067
+
1068
+ # Accepts a URI and creates the site URI from that.
1069
+ def create_site_uri_from(site)
1070
+ site.is_a?(URI) ? site.dup : URI.parse(site)
1071
+ end
1072
+
1073
+ # Accepts a URI and creates the proxy URI from that.
1074
+ def create_proxy_uri_from(proxy)
1075
+ proxy.is_a?(URI) ? proxy.dup : URI.parse(proxy)
1076
+ end
1077
+
1078
+ # contains a set of the current prefix parameters.
1079
+ def prefix_parameters
1080
+ @prefix_parameters ||= prefix_source.scan(/:\w+/).map { |key| key[1..-1].to_sym }.to_set
1081
+ end
1082
+
1083
+ # Builds the query string for the request.
1084
+ def query_string(options)
1085
+ "?#{options.to_query}" unless options.nil? || options.empty?
1086
+ end
1087
+
1088
+ # split an option hash into two hashes, one containing the prefix options,
1089
+ # and the other containing the leftovers.
1090
+ def split_options(options = {})
1091
+ prefix_options, query_options = {}, {}
1092
+
1093
+ (options || {}).each do |key, value|
1094
+ next if key.blank? || !key.respond_to?(:to_sym)
1095
+ (prefix_parameters.include?(key.to_sym) ? prefix_options : query_options)[key.to_sym] = value
1096
+ end
1097
+
1098
+ [ prefix_options, query_options ]
1099
+ end
1100
+ end
1101
+
1102
+ attr_accessor :attributes #:nodoc:
1103
+ attr_accessor :prefix_options #:nodoc:
1104
+
1105
+ # If no schema has been defined for the class (see
1106
+ # <tt>ActiveResource::schema=</tt>), the default automatic schema is
1107
+ # generated from the current instance's attributes
1108
+ def schema
1109
+ self.class.schema || self.attributes
1110
+ end
1111
+
1112
+ # This is a list of known attributes for this resource. Either
1113
+ # gathered from the provided <tt>schema</tt>, or from the attributes
1114
+ # set on this instance after it has been fetched from the remote system.
1115
+ def known_attributes
1116
+ (self.class.known_attributes + self.attributes.keys.map(&:to_s)).uniq
1117
+ end
1118
+
1119
+
1120
+ # Constructor method for \new resources; the optional +attributes+ parameter takes a \hash
1121
+ # of attributes for the \new resource.
1122
+ #
1123
+ # ==== Examples
1124
+ # my_course = Course.new
1125
+ # my_course.name = "Western Civilization"
1126
+ # my_course.lecturer = "Don Trotter"
1127
+ # my_course.save
1128
+ #
1129
+ # my_other_course = Course.new(:name => "Philosophy: Reason and Being", :lecturer => "Ralph Cling")
1130
+ # my_other_course.save
1131
+ def initialize(attributes = {}, persisted = false)
1132
+ @attributes = {}.with_indifferent_access
1133
+ @prefix_options = {}
1134
+ @persisted = persisted
1135
+ load(attributes, false, persisted)
1136
+ end
1137
+
1138
+ # Returns a \clone of the resource that hasn't been assigned an +id+ yet and
1139
+ # is treated as a \new resource.
1140
+ #
1141
+ # ryan = Person.find(1)
1142
+ # not_ryan = ryan.clone
1143
+ # not_ryan.new? # => true
1144
+ #
1145
+ # Any active resource member attributes will NOT be cloned, though all other
1146
+ # attributes are. This is to prevent the conflict between any +prefix_options+
1147
+ # that refer to the original parent resource and the newly cloned parent
1148
+ # resource that does not exist.
1149
+ #
1150
+ # ryan = Person.find(1)
1151
+ # ryan.address = StreetAddress.find(1, :person_id => ryan.id)
1152
+ # ryan.hash = {:not => "an ARes instance"}
1153
+ #
1154
+ # not_ryan = ryan.clone
1155
+ # not_ryan.new? # => true
1156
+ # not_ryan.address # => NoMethodError
1157
+ # not_ryan.hash # => {:not => "an ARes instance"}
1158
+ def clone
1159
+ # Clone all attributes except the pk and any nested ARes
1160
+ cloned = Hash[attributes.reject {|k,v| k == self.class.primary_key || v.is_a?(ActiveResource::Base)}.map { |k, v| [k, v.clone] }]
1161
+ # Form the new resource - bypass initialize of resource with 'new' as that will call 'load' which
1162
+ # attempts to convert hashes into member objects and arrays into collections of objects. We want
1163
+ # the raw objects to be cloned so we bypass load by directly setting the attributes hash.
1164
+ resource = self.class.new({})
1165
+ resource.prefix_options = self.prefix_options
1166
+ resource.send :instance_variable_set, '@attributes', cloned
1167
+ resource
1168
+ end
1169
+
1170
+
1171
+ # Returns +true+ if this object hasn't yet been saved, otherwise, returns +false+.
1172
+ #
1173
+ # ==== Examples
1174
+ # not_new = Computer.create(:brand => 'Apple', :make => 'MacBook', :vendor => 'MacMall')
1175
+ # not_new.new? # => false
1176
+ #
1177
+ # is_new = Computer.new(:brand => 'IBM', :make => 'Thinkpad', :vendor => 'IBM')
1178
+ # is_new.new? # => true
1179
+ #
1180
+ # is_new.save
1181
+ # is_new.new? # => false
1182
+ #
1183
+ def new?
1184
+ !persisted?
1185
+ end
1186
+ alias :new_record? :new?
1187
+
1188
+ # Returns +true+ if this object has been saved, otherwise returns +false+.
1189
+ #
1190
+ # ==== Examples
1191
+ # persisted = Computer.create(:brand => 'Apple', :make => 'MacBook', :vendor => 'MacMall')
1192
+ # persisted.persisted? # => true
1193
+ #
1194
+ # not_persisted = Computer.new(:brand => 'IBM', :make => 'Thinkpad', :vendor => 'IBM')
1195
+ # not_persisted.persisted? # => false
1196
+ #
1197
+ # not_persisted.save
1198
+ # not_persisted.persisted? # => true
1199
+ #
1200
+ def persisted?
1201
+ @persisted
1202
+ end
1203
+
1204
+ # Gets the <tt>\id</tt> attribute of the resource.
1205
+ def id
1206
+ attributes[self.class.primary_key]
1207
+ end
1208
+
1209
+ # Sets the <tt>\id</tt> attribute of the resource.
1210
+ def id=(id)
1211
+ attributes[self.class.primary_key] = id
1212
+ end
1213
+
1214
+ # Test for equality. Resource are equal if and only if +other+ is the same object or
1215
+ # is an instance of the same class, is not <tt>new?</tt>, and has the same +id+.
1216
+ #
1217
+ # ==== Examples
1218
+ # ryan = Person.create(:name => 'Ryan')
1219
+ # jamie = Person.create(:name => 'Jamie')
1220
+ #
1221
+ # ryan == jamie
1222
+ # # => false (Different name attribute and id)
1223
+ #
1224
+ # ryan_again = Person.new(:name => 'Ryan')
1225
+ # ryan == ryan_again
1226
+ # # => false (ryan_again is new?)
1227
+ #
1228
+ # ryans_clone = Person.create(:name => 'Ryan')
1229
+ # ryan == ryans_clone
1230
+ # # => false (Different id attributes)
1231
+ #
1232
+ # ryans_twin = Person.find(ryan.id)
1233
+ # ryan == ryans_twin
1234
+ # # => true
1235
+ #
1236
+ def ==(other)
1237
+ other.equal?(self) || (other.instance_of?(self.class) && other.id == id && other.prefix_options == prefix_options)
1238
+ end
1239
+
1240
+ # Tests for equality (delegates to ==).
1241
+ def eql?(other)
1242
+ self == other
1243
+ end
1244
+
1245
+ # Delegates to id in order to allow two resources of the same type and \id to work with something like:
1246
+ # [(a = Person.find 1), (b = Person.find 2)] & [(c = Person.find 1), (d = Person.find 4)] # => [a]
1247
+ def hash
1248
+ id.hash
1249
+ end
1250
+
1251
+ # Duplicates the current resource without saving it.
1252
+ #
1253
+ # ==== Examples
1254
+ # my_invoice = Invoice.create(:customer => 'That Company')
1255
+ # next_invoice = my_invoice.dup
1256
+ # next_invoice.new? # => true
1257
+ #
1258
+ # next_invoice.save
1259
+ # next_invoice == my_invoice # => false (different id attributes)
1260
+ #
1261
+ # my_invoice.customer # => That Company
1262
+ # next_invoice.customer # => That Company
1263
+ def dup
1264
+ self.class.new.tap do |resource|
1265
+ resource.attributes = @attributes
1266
+ resource.prefix_options = @prefix_options
1267
+ end
1268
+ end
1269
+
1270
+ # Saves (+POST+) or \updates (+PUT+) a resource. Delegates to +create+ if the object is \new,
1271
+ # +update+ if it exists. If the response to the \save includes a body, it will be assumed that this body
1272
+ # is Json for the final object as it looked after the \save (which would include attributes like +created_at+
1273
+ # that weren't part of the original submit).
1274
+ #
1275
+ # ==== Examples
1276
+ # my_company = Company.new(:name => 'RoleModel Software', :owner => 'Ken Auer', :size => 2)
1277
+ # my_company.new? # => true
1278
+ # my_company.save # sends POST /companies/ (create)
1279
+ #
1280
+ # my_company.new? # => false
1281
+ # my_company.size = 10
1282
+ # my_company.save # sends PUT /companies/1 (update)
1283
+ def save
1284
+ run_callbacks :save do
1285
+ new? ? create : update
1286
+ end
1287
+ end
1288
+
1289
+ # Saves the resource.
1290
+ #
1291
+ # If the resource is new, it is created via +POST+, otherwise the
1292
+ # existing resource is updated via +PUT+.
1293
+ #
1294
+ # With <tt>save!</tt> validations always run. If any of them fail
1295
+ # ActiveResource::ResourceInvalid gets raised, and nothing is POSTed to
1296
+ # the remote system.
1297
+ # See ActiveResource::Validations for more information.
1298
+ #
1299
+ # There's a series of callbacks associated with <tt>save!</tt>. If any
1300
+ # of the <tt>before_*</tt> callbacks return +false+ the action is
1301
+ # cancelled and <tt>save!</tt> raises ActiveResource::ResourceInvalid.
1302
+ def save!
1303
+ save || raise(ResourceInvalid.new(self))
1304
+ end
1305
+
1306
+ # Deletes the resource from the remote service.
1307
+ #
1308
+ # ==== Examples
1309
+ # my_id = 3
1310
+ # my_person = Person.find(my_id)
1311
+ # my_person.destroy
1312
+ # Person.find(my_id) # 404 (Resource Not Found)
1313
+ #
1314
+ # new_person = Person.create(:name => 'James')
1315
+ # new_id = new_person.id # => 7
1316
+ # new_person.destroy
1317
+ # Person.find(new_id) # 404 (Resource Not Found)
1318
+ def destroy
1319
+ run_callbacks :destroy do
1320
+ connection.delete(element_path, self.class.headers)
1321
+ end
1322
+ end
1323
+
1324
+ # Evaluates to <tt>true</tt> if this resource is not <tt>new?</tt> and is
1325
+ # found on the remote service. Using this method, you can check for
1326
+ # resources that may have been deleted between the object's instantiation
1327
+ # and actions on it.
1328
+ #
1329
+ # ==== Examples
1330
+ # Person.create(:name => 'Theodore Roosevelt')
1331
+ # that_guy = Person.find(:first)
1332
+ # that_guy.exists? # => true
1333
+ #
1334
+ # that_lady = Person.new(:name => 'Paul Bean')
1335
+ # that_lady.exists? # => false
1336
+ #
1337
+ # guys_id = that_guy.id
1338
+ # Person.delete(guys_id)
1339
+ # that_guy.exists? # => false
1340
+ def exists?
1341
+ !new? && self.class.exists?(to_param, :params => prefix_options)
1342
+ end
1343
+
1344
+ # Returns the serialized string representation of the resource in the configured
1345
+ # serialization format specified in ActiveResource::Base.format. The options
1346
+ # applicable depend on the configured encoding format.
1347
+ def encode(options={})
1348
+ send("to_#{self.class.format.extension}", options)
1349
+ end
1350
+
1351
+ # A method to \reload the attributes of this object from the remote web service.
1352
+ #
1353
+ # ==== Examples
1354
+ # my_branch = Branch.find(:first)
1355
+ # my_branch.name # => "Wislon Raod"
1356
+ #
1357
+ # # Another client fixes the typo...
1358
+ #
1359
+ # my_branch.name # => "Wislon Raod"
1360
+ # my_branch.reload
1361
+ # my_branch.name # => "Wilson Road"
1362
+ def reload
1363
+ self.load(self.class.find(to_param, :params => @prefix_options).attributes, false, true)
1364
+ end
1365
+
1366
+ # A method to manually load attributes from a \hash. Recursively loads collections of
1367
+ # resources. This method is called in +initialize+ and +create+ when a \hash of attributes
1368
+ # is provided.
1369
+ #
1370
+ # ==== Examples
1371
+ # my_attrs = {:name => 'J&J Textiles', :industry => 'Cloth and textiles'}
1372
+ # my_attrs = {:name => 'Marty', :colors => ["red", "green", "blue"]}
1373
+ #
1374
+ # the_supplier = Supplier.find(:first)
1375
+ # the_supplier.name # => 'J&M Textiles'
1376
+ # the_supplier.load(my_attrs)
1377
+ # the_supplier.name('J&J Textiles')
1378
+ #
1379
+ # # These two calls are the same as Supplier.new(my_attrs)
1380
+ # my_supplier = Supplier.new
1381
+ # my_supplier.load(my_attrs)
1382
+ #
1383
+ # # These three calls are the same as Supplier.create(my_attrs)
1384
+ # your_supplier = Supplier.new
1385
+ # your_supplier.load(my_attrs)
1386
+ # your_supplier.save
1387
+ def load(attributes, remove_root = false, persisted = false)
1388
+ raise ArgumentError, "expected an attributes Hash, got #{attributes.inspect}" unless attributes.is_a?(Hash)
1389
+ @prefix_options, attributes = split_options(attributes)
1390
+
1391
+ if attributes.keys.size == 1
1392
+ remove_root = self.class.element_name == attributes.keys.first.to_s
1393
+ end
1394
+
1395
+ attributes = Formats.remove_root(attributes) if remove_root
1396
+
1397
+ attributes.each do |key, value|
1398
+ @attributes[key.to_s] =
1399
+ case value
1400
+ when Array
1401
+ resource = nil
1402
+ value.map do |attrs|
1403
+ if attrs.is_a?(Hash)
1404
+ resource ||= find_or_create_resource_for_collection(key)
1405
+ resource.new(attrs, persisted)
1406
+ else
1407
+ attrs.duplicable? ? attrs.dup : attrs
1408
+ end
1409
+ end
1410
+ when Hash
1411
+ resource = find_or_create_resource_for(key)
1412
+ resource.new(value, persisted)
1413
+ else
1414
+ value.duplicable? ? value.dup : value
1415
+ end
1416
+ end
1417
+ self
1418
+ end
1419
+
1420
+ # Updates a single attribute and then saves the object.
1421
+ #
1422
+ # Note: <tt>Unlike ActiveRecord::Base.update_attribute</tt>, this method <b>is</b>
1423
+ # subject to normal validation routines as an update sends the whole body
1424
+ # of the resource in the request. (See Validations).
1425
+ #
1426
+ # As such, this method is equivalent to calling update_attributes with a single attribute/value pair.
1427
+ #
1428
+ # If the saving fails because of a connection or remote service error, an
1429
+ # exception will be raised. If saving fails because the resource is
1430
+ # invalid then <tt>false</tt> will be returned.
1431
+ def update_attribute(name, value)
1432
+ self.send("#{name}=".to_sym, value)
1433
+ self.save
1434
+ end
1435
+
1436
+ # Updates this resource with all the attributes from the passed-in Hash
1437
+ # and requests that the record be saved.
1438
+ #
1439
+ # If the saving fails because of a connection or remote service error, an
1440
+ # exception will be raised. If saving fails because the resource is
1441
+ # invalid then <tt>false</tt> will be returned.
1442
+ #
1443
+ # Note: Though this request can be made with a partial set of the
1444
+ # resource's attributes, the full body of the request will still be sent
1445
+ # in the save request to the remote service.
1446
+ def update_attributes(attributes)
1447
+ load(attributes, false) && save
1448
+ end
1449
+
1450
+ # For checking <tt>respond_to?</tt> without searching the attributes (which is faster).
1451
+ alias_method :respond_to_without_attributes?, :respond_to?
1452
+
1453
+ # A method to determine if an object responds to a message (e.g., a method call). In Active Resource, a Person object with a
1454
+ # +name+ attribute can answer <tt>true</tt> to <tt>my_person.respond_to?(:name)</tt>, <tt>my_person.respond_to?(:name=)</tt>, and
1455
+ # <tt>my_person.respond_to?(:name?)</tt>.
1456
+ def respond_to_missing?(method, include_priv = false)
1457
+ method_name = method.to_s
1458
+ if attributes.nil?
1459
+ super
1460
+ elsif known_attributes.include?(method_name)
1461
+ true
1462
+ elsif method_name =~ /(?:=|\?)$/ && attributes.include?($`)
1463
+ true
1464
+ else
1465
+ # super must be called at the end of the method, because the inherited respond_to?
1466
+ # would return true for generated readers, even if the attribute wasn't present
1467
+ super
1468
+ end
1469
+ end
1470
+
1471
+ def to_json(options={})
1472
+ super(include_root_in_json ? { :root => self.class.element_name }.merge(options) : options)
1473
+ end
1474
+
1475
+ def to_xml(options={})
1476
+ super({ :root => self.class.element_name }.merge(options))
1477
+ end
1478
+
1479
+ protected
1480
+ def connection(refresh = false)
1481
+ self.class.connection(refresh)
1482
+ end
1483
+
1484
+ # Update the resource on the remote service.
1485
+ def update
1486
+ run_callbacks :update do
1487
+ connection.put(element_path(prefix_options), encode, self.class.headers).tap do |response|
1488
+ load_attributes_from_response(response)
1489
+ end
1490
+ end
1491
+ end
1492
+
1493
+ # Create (i.e., \save to the remote service) the \new resource.
1494
+ def create
1495
+ run_callbacks :create do
1496
+ connection.post(collection_path, encode, self.class.headers).tap do |response|
1497
+ self.id = id_from_response(response)
1498
+ load_attributes_from_response(response)
1499
+ end
1500
+ end
1501
+ end
1502
+
1503
+ def load_attributes_from_response(response)
1504
+ if (response_code_allows_body?(response.code) &&
1505
+ (response['Content-Length'].nil? || response['Content-Length'] != "0") &&
1506
+ !response.body.nil? && response.body.strip.size > 0)
1507
+ load(self.class.format.decode(response.body), true, true)
1508
+ @persisted = true
1509
+ end
1510
+ end
1511
+
1512
+ # Takes a response from a typical create post and pulls the ID out
1513
+ def id_from_response(response)
1514
+ response['Location'][/\/([^\/]*?)(\.\w+)?$/, 1] if response['Location']
1515
+ end
1516
+
1517
+ def element_path(options = nil)
1518
+ self.class.element_path(to_param, options || prefix_options)
1519
+ end
1520
+
1521
+ def new_element_path
1522
+ self.class.new_element_path(prefix_options)
1523
+ end
1524
+
1525
+ def collection_path(options = nil)
1526
+ self.class.collection_path(options || prefix_options)
1527
+ end
1528
+
1529
+ private
1530
+
1531
+ def read_attribute_for_serialization(n)
1532
+ attributes[n]
1533
+ end
1534
+
1535
+ # Determine whether the response is allowed to have a body per HTTP 1.1 spec section 4.4.1
1536
+ def response_code_allows_body?(c)
1537
+ !((100..199).include?(c) || [204,304].include?(c))
1538
+ end
1539
+
1540
+ # Tries to find a resource for a given collection name; if it fails, then the resource is created
1541
+ def find_or_create_resource_for_collection(name)
1542
+ return reflections[name.to_sym].klass if reflections.key?(name.to_sym)
1543
+ find_or_create_resource_for(ActiveSupport::Inflector.singularize(name.to_s))
1544
+ end
1545
+
1546
+ # Tries to find a resource in a non empty list of nested modules
1547
+ # if it fails, then the resource is created
1548
+ def find_or_create_resource_in_modules(resource_name, module_names)
1549
+ receiver = Object
1550
+ namespaces = module_names[0, module_names.size-1].map do |module_name|
1551
+ receiver = receiver.const_get(module_name)
1552
+ end
1553
+ const_args = [resource_name, false]
1554
+ if namespace = namespaces.reverse.detect { |ns| ns.const_defined?(*const_args) }
1555
+ namespace.const_get(*const_args)
1556
+ else
1557
+ create_resource_for(resource_name)
1558
+ end
1559
+ end
1560
+
1561
+ # Tries to find a resource for a given name; if it fails, then the resource is created
1562
+ def find_or_create_resource_for(name)
1563
+ return reflections[name.to_sym].klass if reflections.key?(name.to_sym)
1564
+ resource_name = name.to_s.camelize
1565
+
1566
+ const_args = [resource_name, false]
1567
+ if self.class.const_defined?(*const_args)
1568
+ self.class.const_get(*const_args)
1569
+ else
1570
+ ancestors = self.class.name.to_s.split("::")
1571
+ if ancestors.size > 1
1572
+ find_or_create_resource_in_modules(resource_name, ancestors)
1573
+ else
1574
+ if Object.const_defined?(*const_args)
1575
+ Object.const_get(*const_args)
1576
+ else
1577
+ create_resource_for(resource_name)
1578
+ end
1579
+ end
1580
+ end
1581
+ end
1582
+
1583
+ # Create and return a class definition for a resource inside the current resource
1584
+ def create_resource_for(resource_name)
1585
+ resource = self.class.const_set(resource_name, Class.new(ActiveResource::Base))
1586
+ resource.prefix = self.class.prefix
1587
+ resource.site = self.class.site
1588
+ resource
1589
+ end
1590
+
1591
+ def split_options(options = {})
1592
+ self.class.__send__(:split_options, options)
1593
+ end
1594
+
1595
+ def method_missing(method_symbol, *arguments) #:nodoc:
1596
+ method_name = method_symbol.to_s
1597
+
1598
+ if method_name =~ /(=|\?)$/
1599
+ case $1
1600
+ when "="
1601
+ attributes[$`] = arguments.first
1602
+ when "?"
1603
+ attributes[$`]
1604
+ end
1605
+ else
1606
+ return attributes[method_name] if attributes.include?(method_name)
1607
+ # not set right now but we know about it
1608
+ return nil if known_attributes.include?(method_name)
1609
+ super
1610
+ end
1611
+ end
1612
+ end
1613
+
1614
+ class Base
1615
+ extend ActiveModel::Naming
1616
+ extend ActiveResource::Associations
1617
+
1618
+ include Callbacks, CustomMethods, Validations
1619
+ include ActiveModel::Conversion
1620
+ include ActiveModel::Serializers::JSON
1621
+ include ActiveModel::Serializers::Xml
1622
+ include ActiveResource::Reflection
1623
+ end
1624
+
1625
+ ActiveSupport.run_load_hooks(:active_resource, Base)
1626
+ end