activeresource_csi 2.3.5.p6

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,1157 @@
1
+ require 'active_resource/connection'
2
+ require 'cgi'
3
+ require 'set'
4
+
5
+ module ActiveResource
6
+ # ActiveResource::Base is the main class for mapping RESTful resources as models in a Rails application.
7
+ #
8
+ # For an outline of what Active Resource is capable of, see link:files/vendor/rails/activeresource/README.html.
9
+ #
10
+ # == Automated mapping
11
+ #
12
+ # Active Resource objects represent your RESTful resources as manipulatable Ruby objects. To map resources
13
+ # to Ruby objects, Active Resource only needs a class name that corresponds to the resource name (e.g., the class
14
+ # Person maps to the resources people, very similarly to Active Record) and a +site+ value, which holds the
15
+ # URI of the resources.
16
+ #
17
+ # class Person < ActiveResource::Base
18
+ # self.site = "http://api.people.com:3000/"
19
+ # end
20
+ #
21
+ # Now the Person class is mapped to RESTful resources located at <tt>http://api.people.com:3000/people/</tt>, and
22
+ # you can now use Active Resource's lifecycles methods to manipulate resources. In the case where you already have
23
+ # an existing model with the same name as the desired RESTful resource you can set the +element_name+ value.
24
+ #
25
+ # class PersonResource < ActiveResource::Base
26
+ # self.site = "http://api.people.com:3000/"
27
+ # self.element_name = "person"
28
+ # end
29
+ #
30
+ #
31
+ # == Lifecycle methods
32
+ #
33
+ # Active Resource exposes methods for creating, finding, updating, and deleting resources
34
+ # from REST web services.
35
+ #
36
+ # ryan = Person.new(:first => 'Ryan', :last => 'Daigle')
37
+ # ryan.save # => true
38
+ # ryan.id # => 2
39
+ # Person.exists?(ryan.id) # => true
40
+ # ryan.exists? # => true
41
+ #
42
+ # ryan = Person.find(1)
43
+ # # Resource holding our newly created Person object
44
+ #
45
+ # ryan.first = 'Rizzle'
46
+ # ryan.save # => true
47
+ #
48
+ # ryan.destroy # => true
49
+ #
50
+ # As you can see, these are very similar to Active Record's lifecycle methods for database records.
51
+ # You can read more about each of these methods in their respective documentation.
52
+ #
53
+ # === Custom REST methods
54
+ #
55
+ # Since simple CRUD/lifecycle methods can't accomplish every task, Active Resource also supports
56
+ # defining your own custom REST methods. To invoke them, Active Resource provides the <tt>get</tt>,
57
+ # <tt>post</tt>, <tt>put</tt> and <tt>\delete</tt> methods where you can specify a custom REST method
58
+ # name to invoke.
59
+ #
60
+ # # POST to the custom 'register' REST method, i.e. POST /people/new/register.xml.
61
+ # Person.new(:name => 'Ryan').post(:register)
62
+ # # => { :id => 1, :name => 'Ryan', :position => 'Clerk' }
63
+ #
64
+ # # PUT an update by invoking the 'promote' REST method, i.e. PUT /people/1/promote.xml?position=Manager.
65
+ # Person.find(1).put(:promote, :position => 'Manager')
66
+ # # => { :id => 1, :name => 'Ryan', :position => 'Manager' }
67
+ #
68
+ # # GET all the positions available, i.e. GET /people/positions.xml.
69
+ # Person.get(:positions)
70
+ # # => [{:name => 'Manager'}, {:name => 'Clerk'}]
71
+ #
72
+ # # DELETE to 'fire' a person, i.e. DELETE /people/1/fire.xml.
73
+ # Person.find(1).delete(:fire)
74
+ #
75
+ # For more information on using custom REST methods, see the
76
+ # ActiveResource::CustomMethods documentation.
77
+ #
78
+ # == Validations
79
+ #
80
+ # You can validate resources client side by overriding validation methods in the base class.
81
+ #
82
+ # class Person < ActiveResource::Base
83
+ # self.site = "http://api.people.com:3000/"
84
+ # protected
85
+ # def validate
86
+ # errors.add("last", "has invalid characters") unless last =~ /[a-zA-Z]*/
87
+ # end
88
+ # end
89
+ #
90
+ # See the ActiveResource::Validations documentation for more information.
91
+ #
92
+ # == Authentication
93
+ #
94
+ # Many REST APIs will require authentication, usually in the form of basic
95
+ # HTTP authentication. Authentication can be specified by:
96
+ #
97
+ # === HTTP Basic Authentication
98
+ # * putting the credentials in the URL for the +site+ variable.
99
+ #
100
+ # class Person < ActiveResource::Base
101
+ # self.site = "http://ryan:password@api.people.com:3000/"
102
+ # end
103
+ #
104
+ # * defining +user+ and/or +password+ variables
105
+ #
106
+ # class Person < ActiveResource::Base
107
+ # self.site = "http://api.people.com:3000/"
108
+ # self.user = "ryan"
109
+ # self.password = "password"
110
+ # end
111
+ #
112
+ # For obvious security reasons, it is probably best if such services are available
113
+ # over HTTPS.
114
+ #
115
+ # Note: Some values cannot be provided in the URL passed to site. e.g. email addresses
116
+ # as usernames. In those situations you should use the separate user and password option.
117
+ #
118
+ # === Certificate Authentication
119
+ #
120
+ # * End point uses an X509 certificate for authentication. <tt>See ssl_options=</tt> for all options.
121
+ #
122
+ # class Person < ActiveResource::Base
123
+ # self.site = "https://secure.api.people.com/"
124
+ # self.ssl_options = {:cert => OpenSSL::X509::Certificate.new(File.open(pem_file))
125
+ # :key => OpenSSL::PKey::RSA.new(File.open(pem_file)),
126
+ # :ca_path => "/path/to/OpenSSL/formatted/CA_Certs",
127
+ # :verify_mode => OpenSSL::SSL::VERIFY_PEER}
128
+ # end
129
+ #
130
+ # == Errors & Validation
131
+ #
132
+ # Error handling and validation is handled in much the same manner as you're used to seeing in
133
+ # Active Record. Both the response code in the HTTP response and the body of the response are used to
134
+ # indicate that an error occurred.
135
+ #
136
+ # === Resource errors
137
+ #
138
+ # When a GET is requested for a resource that does not exist, the HTTP <tt>404</tt> (Resource Not Found)
139
+ # response code will be returned from the server which will raise an ActiveResource::ResourceNotFound
140
+ # exception.
141
+ #
142
+ # # GET http://api.people.com:3000/people/999.xml
143
+ # ryan = Person.find(999) # 404, raises ActiveResource::ResourceNotFound
144
+ #
145
+ # <tt>404</tt> is just one of the HTTP error response codes that Active Resource will handle with its own exception. The
146
+ # following HTTP response codes will also result in these exceptions:
147
+ #
148
+ # * 200..399 - Valid response, no exception (other than 301, 302)
149
+ # * 301, 302 - ActiveResource::Redirection
150
+ # * 400 - ActiveResource::BadRequest
151
+ # * 401 - ActiveResource::UnauthorizedAccess
152
+ # * 403 - ActiveResource::ForbiddenAccess
153
+ # * 404 - ActiveResource::ResourceNotFound
154
+ # * 405 - ActiveResource::MethodNotAllowed
155
+ # * 409 - ActiveResource::ResourceConflict
156
+ # * 410 - ActiveResource::ResourceGone
157
+ # * 422 - ActiveResource::ResourceInvalid (rescued by save as validation errors)
158
+ # * 401..499 - ActiveResource::ClientError
159
+ # * 500..599 - ActiveResource::ServerError
160
+ # * Other - ActiveResource::ConnectionError
161
+ #
162
+ # These custom exceptions allow you to deal with resource errors more naturally and with more precision
163
+ # rather than returning a general HTTP error. For example:
164
+ #
165
+ # begin
166
+ # ryan = Person.find(my_id)
167
+ # rescue ActiveResource::ResourceNotFound
168
+ # redirect_to :action => 'not_found'
169
+ # rescue ActiveResource::ResourceConflict, ActiveResource::ResourceInvalid
170
+ # redirect_to :action => 'new'
171
+ # end
172
+ #
173
+ # === Validation errors
174
+ #
175
+ # Active Resource supports validations on resources and will return errors if any these validations fail
176
+ # (e.g., "First name can not be blank" and so on). These types of errors are denoted in the response by
177
+ # a response code of <tt>422</tt> and an XML or JSON representation of the validation errors. The save operation will
178
+ # then fail (with a <tt>false</tt> return value) and the validation errors can be accessed on the resource in question.
179
+ #
180
+ # ryan = Person.find(1)
181
+ # ryan.first # => ''
182
+ # ryan.save # => false
183
+ #
184
+ # # When
185
+ # # PUT http://api.people.com:3000/people/1.xml
186
+ # # or
187
+ # # PUT http://api.people.com:3000/people/1.json
188
+ # # is requested with invalid values, the response is:
189
+ # #
190
+ # # Response (422):
191
+ # # <errors type="array"><error>First cannot be empty</error></errors>
192
+ # # or
193
+ # # {"errors":["First cannot be empty"]}
194
+ # #
195
+ #
196
+ # ryan.errors.invalid?(:first) # => true
197
+ # ryan.errors.full_messages # => ['First cannot be empty']
198
+ #
199
+ # Learn more about Active Resource's validation features in the ActiveResource::Validations documentation.
200
+ #
201
+ # === Timeouts
202
+ #
203
+ # Active Resource relies on HTTP to access RESTful APIs and as such is inherently susceptible to slow or
204
+ # unresponsive servers. In such cases, your Active Resource method calls could \timeout. You can control the
205
+ # amount of time before Active Resource times out with the +timeout+ variable.
206
+ #
207
+ # class Person < ActiveResource::Base
208
+ # self.site = "http://api.people.com:3000/"
209
+ # self.timeout = 5
210
+ # end
211
+ #
212
+ # This sets the +timeout+ to 5 seconds. You can adjust the +timeout+ to a value suitable for the RESTful API
213
+ # you are accessing. It is recommended to set this to a reasonably low value to allow your Active Resource
214
+ # clients (especially if you are using Active Resource in a Rails application) to fail-fast (see
215
+ # http://en.wikipedia.org/wiki/Fail-fast) rather than cause cascading failures that could incapacitate your
216
+ # server.
217
+ #
218
+ # When a \timeout occurs, an ActiveResource::TimeoutError is raised. You should rescue from
219
+ # ActiveResource::TimeoutError in your Active Resource method calls.
220
+ #
221
+ # Internally, Active Resource relies on Ruby's Net::HTTP library to make HTTP requests. Setting +timeout+
222
+ # sets the <tt>read_timeout</tt> of the internal Net::HTTP instance to the same value. The default
223
+ # <tt>read_timeout</tt> is 60 seconds on most Ruby implementations.
224
+ class Base
225
+ ##
226
+ # :singleton-method:
227
+ # The logger for diagnosing and tracing Active Resource calls.
228
+ cattr_accessor :logger
229
+
230
+ class << self
231
+ # Gets the URI of the REST resources to map for this class. The site variable is required for
232
+ # Active Resource's mapping to work.
233
+ def site
234
+ # Not using superclass_delegating_reader because don't want subclasses to modify superclass instance
235
+ #
236
+ # With superclass_delegating_reader
237
+ #
238
+ # Parent.site = 'http://anonymous@test.com'
239
+ # Subclass.site # => 'http://anonymous@test.com'
240
+ # Subclass.site.user = 'david'
241
+ # Parent.site # => 'http://david@test.com'
242
+ #
243
+ # Without superclass_delegating_reader (expected behaviour)
244
+ #
245
+ # Parent.site = 'http://anonymous@test.com'
246
+ # Subclass.site # => 'http://anonymous@test.com'
247
+ # Subclass.site.user = 'david' # => TypeError: can't modify frozen object
248
+ #
249
+ if defined?(@site)
250
+ @site
251
+ elsif superclass != Object && superclass.site
252
+ superclass.site.dup.freeze
253
+ end
254
+ end
255
+
256
+ # Sets the URI of the REST resources to map for this class to the value in the +site+ argument.
257
+ # The site variable is required for Active Resource's mapping to work.
258
+ def site=(site)
259
+ @connection = nil
260
+ if site.nil?
261
+ @site = nil
262
+ else
263
+ @site = create_site_uri_from(site)
264
+ @user = URI.decode(@site.user) if @site.user
265
+ @password = URI.decode(@site.password) if @site.password
266
+ end
267
+ end
268
+
269
+ # Gets the \proxy variable if a proxy is required
270
+ def proxy
271
+ # Not using superclass_delegating_reader. See +site+ for explanation
272
+ if defined?(@proxy)
273
+ @proxy
274
+ elsif superclass != Object && superclass.proxy
275
+ superclass.proxy.dup.freeze
276
+ end
277
+ end
278
+
279
+ # Sets the URI of the http proxy to the value in the +proxy+ argument.
280
+ def proxy=(proxy)
281
+ @connection = nil
282
+ @proxy = proxy.nil? ? nil : create_proxy_uri_from(proxy)
283
+ end
284
+
285
+ # Gets the \user for REST HTTP authentication.
286
+ def user
287
+ # Not using superclass_delegating_reader. See +site+ for explanation
288
+ if defined?(@user)
289
+ @user
290
+ elsif superclass != Object && superclass.user
291
+ superclass.user.dup.freeze
292
+ end
293
+ end
294
+
295
+ # Sets the \user for REST HTTP authentication.
296
+ def user=(user)
297
+ @connection = nil
298
+ @user = user
299
+ end
300
+
301
+ # Gets the \password for REST HTTP authentication.
302
+ def password
303
+ # Not using superclass_delegating_reader. See +site+ for explanation
304
+ if defined?(@password)
305
+ @password
306
+ elsif superclass != Object && superclass.password
307
+ superclass.password.dup.freeze
308
+ end
309
+ end
310
+
311
+ # Sets the \password for REST HTTP authentication.
312
+ def password=(password)
313
+ @connection = nil
314
+ @password = password
315
+ end
316
+
317
+ # Sets the format that attributes are sent and received in from a mime type reference:
318
+ #
319
+ # Person.format = :json
320
+ # Person.find(1) # => GET /people/1.json
321
+ #
322
+ # Person.format = ActiveResource::Formats::XmlFormat
323
+ # Person.find(1) # => GET /people/1.xml
324
+ #
325
+ # Default format is <tt>:xml</tt>.
326
+ def format=(mime_type_reference_or_format)
327
+ format = mime_type_reference_or_format.is_a?(Symbol) ?
328
+ ActiveResource::Formats[mime_type_reference_or_format] : mime_type_reference_or_format
329
+
330
+ write_inheritable_attribute(:format, format)
331
+ connection.format = format if site
332
+ end
333
+
334
+ # Returns the current format, default is ActiveResource::Formats::XmlFormat.
335
+ def format
336
+ read_inheritable_attribute(:format) || ActiveResource::Formats[:xml]
337
+ end
338
+
339
+ # Sets the number of seconds after which requests to the REST API should time out.
340
+ def timeout=(timeout)
341
+ @connection = nil
342
+ @timeout = timeout
343
+ end
344
+
345
+ # Gets the number of seconds after which requests to the REST API should time out.
346
+ def timeout
347
+ if defined?(@timeout)
348
+ @timeout
349
+ elsif superclass != Object && superclass.timeout
350
+ superclass.timeout
351
+ end
352
+ end
353
+
354
+ # Options that will get applied to an SSL connection.
355
+ #
356
+ # * <tt>:key</tt> - An OpenSSL::PKey::RSA or OpenSSL::PKey::DSA object.
357
+ # * <tt>:cert</tt> - An OpenSSL::X509::Certificate object as client certificate
358
+ # * <tt>:ca_file</tt> - Path to a CA certification file in PEM format. The file can contrain several CA certificates.
359
+ # * <tt>:ca_path</tt> - Path of a CA certification directory containing certifications in PEM format.
360
+ # * <tt>:verify_mode</tt> - Flags for server the certification verification at begining of SSL/TLS session. (OpenSSL::SSL::VERIFY_NONE or OpenSSL::SSL::VERIFY_PEER is acceptable)
361
+ # * <tt>:verify_callback</tt> - The verify callback for the server certification verification.
362
+ # * <tt>:verify_depth</tt> - The maximum depth for the certificate chain verification.
363
+ # * <tt>:cert_store</tt> - OpenSSL::X509::Store to verify peer certificate.
364
+ # * <tt>:ssl_timeout</tt> -The SSL timeout in seconds.
365
+ def ssl_options=(opts={})
366
+ @connection = nil
367
+ @ssl_options = opts
368
+ end
369
+
370
+ # Returns the SSL options hash.
371
+ def ssl_options
372
+ if defined?(@ssl_options)
373
+ @ssl_options
374
+ elsif superclass != Object && superclass.ssl_options
375
+ superclass.ssl_options
376
+ end
377
+ end
378
+
379
+ # An instance of ActiveResource::Connection that is the base \connection to the remote service.
380
+ # The +refresh+ parameter toggles whether or not the \connection is refreshed at every request
381
+ # or not (defaults to <tt>false</tt>).
382
+ def connection(refresh = false)
383
+ if defined?(@connection) || superclass == Object
384
+ @connection = Connection.new(site, format) if refresh || @connection.nil?
385
+ @connection.proxy = proxy if proxy
386
+ @connection.user = user if user
387
+ @connection.password = password if password
388
+ @connection.timeout = timeout if timeout
389
+ @connection.ssl_options = ssl_options if ssl_options
390
+ @connection
391
+ else
392
+ superclass.connection
393
+ end
394
+ end
395
+
396
+ def headers
397
+ @headers ||= {}
398
+ end
399
+
400
+ # Do not include any modules in the default element name. This makes it easier to seclude ARes objects
401
+ # in a separate namespace without having to set element_name repeatedly.
402
+ attr_accessor_with_default(:element_name) { to_s.split("::").last.underscore } #:nodoc:
403
+
404
+ attr_accessor_with_default(:collection_name) { element_name.pluralize } #:nodoc:
405
+ attr_accessor_with_default(:primary_key, 'id') #:nodoc:
406
+
407
+ # Gets the \prefix for a resource's nested URL (e.g., <tt>prefix/collectionname/1.xml</tt>)
408
+ # This method is regenerated at runtime based on what the \prefix is set to.
409
+ def prefix(options={})
410
+ default = site.path
411
+ default << '/' unless default[-1..-1] == '/'
412
+ # generate the actual method based on the current site path
413
+ self.prefix = default
414
+ prefix(options)
415
+ end
416
+
417
+ # An attribute reader for the source string for the resource path \prefix. This
418
+ # method is regenerated at runtime based on what the \prefix is set to.
419
+ def prefix_source
420
+ prefix # generate #prefix and #prefix_source methods first
421
+ prefix_source
422
+ end
423
+
424
+ # Sets the \prefix for a resource's nested URL (e.g., <tt>prefix/collectionname/1.xml</tt>).
425
+ # Default value is <tt>site.path</tt>.
426
+ def prefix=(value = '/')
427
+ # Replace :placeholders with '#{embedded options[:lookups]}'
428
+ prefix_call = value.gsub(/:\w+/) { |key| "\#{options[#{key}]}" }
429
+
430
+ # Clear prefix parameters in case they have been cached
431
+ @prefix_parameters = nil
432
+
433
+ # Redefine the new methods.
434
+ code = <<-end_code
435
+ def prefix_source() "#{value}" end
436
+ def prefix(options={}) "#{prefix_call}" end
437
+ end_code
438
+ silence_warnings { instance_eval code, __FILE__, __LINE__ }
439
+ rescue
440
+ logger.error "Couldn't set prefix: #{$!}\n #{code}"
441
+ raise
442
+ end
443
+
444
+ alias_method :set_prefix, :prefix= #:nodoc:
445
+
446
+ alias_method :set_element_name, :element_name= #:nodoc:
447
+ alias_method :set_collection_name, :collection_name= #:nodoc:
448
+
449
+ # Gets the element path for the given ID in +id+. If the +query_options+ parameter is omitted, Rails
450
+ # will split from the \prefix options.
451
+ #
452
+ # ==== Options
453
+ # +prefix_options+ - A \hash to add a \prefix to the request for nested URLs (e.g., <tt>:account_id => 19</tt>
454
+ # would yield a URL like <tt>/accounts/19/purchases.xml</tt>).
455
+ # +query_options+ - A \hash to add items to the query string for the request.
456
+ #
457
+ # ==== Examples
458
+ # Post.element_path(1)
459
+ # # => /posts/1.xml
460
+ #
461
+ # Comment.element_path(1, :post_id => 5)
462
+ # # => /posts/5/comments/1.xml
463
+ #
464
+ # Comment.element_path(1, :post_id => 5, :active => 1)
465
+ # # => /posts/5/comments/1.xml?active=1
466
+ #
467
+ # Comment.element_path(1, {:post_id => 5}, {:active => 1})
468
+ # # => /posts/5/comments/1.xml?active=1
469
+ #
470
+ def element_path(id, prefix_options = {}, query_options = nil)
471
+ prefix_options, query_options = split_options(prefix_options) if query_options.nil?
472
+ "#{prefix(prefix_options)}#{collection_name}/#{id}.#{format.extension}#{query_string(query_options)}"
473
+ end
474
+
475
+ # Gets the collection path for the REST resources. If the +query_options+ parameter is omitted, Rails
476
+ # will split from the +prefix_options+.
477
+ #
478
+ # ==== Options
479
+ # * +prefix_options+ - A hash to add a prefix to the request for nested URL's (e.g., <tt>:account_id => 19</tt>
480
+ # would yield a URL like <tt>/accounts/19/purchases.xml</tt>).
481
+ # * +query_options+ - A hash to add items to the query string for the request.
482
+ #
483
+ # ==== Examples
484
+ # Post.collection_path
485
+ # # => /posts.xml
486
+ #
487
+ # Comment.collection_path(:post_id => 5)
488
+ # # => /posts/5/comments.xml
489
+ #
490
+ # Comment.collection_path(:post_id => 5, :active => 1)
491
+ # # => /posts/5/comments.xml?active=1
492
+ #
493
+ # Comment.collection_path({:post_id => 5}, {:active => 1})
494
+ # # => /posts/5/comments.xml?active=1
495
+ #
496
+ def collection_path(prefix_options = {}, query_options = nil)
497
+ prefix_options, query_options = split_options(prefix_options) if query_options.nil?
498
+ "#{prefix(prefix_options)}#{collection_name}.#{format.extension}#{query_string(query_options)}"
499
+ end
500
+
501
+ alias_method :set_primary_key, :primary_key= #:nodoc:
502
+
503
+ # Creates a new resource instance and makes a request to the remote service
504
+ # that it be saved, making it equivalent to the following simultaneous calls:
505
+ #
506
+ # ryan = Person.new(:first => 'ryan')
507
+ # ryan.save
508
+ #
509
+ # Returns the newly created resource. If a failure has occurred an
510
+ # exception will be raised (see <tt>save</tt>). If the resource is invalid and
511
+ # has not been saved then <tt>valid?</tt> will return <tt>false</tt>,
512
+ # while <tt>new?</tt> will still return <tt>true</tt>.
513
+ #
514
+ # ==== Examples
515
+ # Person.create(:name => 'Jeremy', :email => 'myname@nospam.com', :enabled => true)
516
+ # my_person = Person.find(:first)
517
+ # my_person.email # => myname@nospam.com
518
+ #
519
+ # dhh = Person.create(:name => 'David', :email => 'dhh@nospam.com', :enabled => true)
520
+ # dhh.valid? # => true
521
+ # dhh.new? # => false
522
+ #
523
+ # # We'll assume that there's a validation that requires the name attribute
524
+ # that_guy = Person.create(:name => '', :email => 'thatguy@nospam.com', :enabled => true)
525
+ # that_guy.valid? # => false
526
+ # that_guy.new? # => true
527
+ def create(attributes = {})
528
+ self.new(attributes).tap { |resource| resource.save }
529
+ end
530
+
531
+ # Core method for finding resources. Used similarly to Active Record's +find+ method.
532
+ #
533
+ # ==== Arguments
534
+ # The first argument is considered to be the scope of the query. That is, how many
535
+ # resources are returned from the request. It can be one of the following.
536
+ #
537
+ # * <tt>:one</tt> - Returns a single resource.
538
+ # * <tt>:first</tt> - Returns the first resource found.
539
+ # * <tt>:last</tt> - Returns the last resource found.
540
+ # * <tt>:all</tt> - Returns every resource that matches the request.
541
+ #
542
+ # ==== Options
543
+ #
544
+ # * <tt>:from</tt> - Sets the path or custom method that resources will be fetched from.
545
+ # * <tt>:params</tt> - Sets query and \prefix (nested URL) parameters.
546
+ #
547
+ # ==== Examples
548
+ # Person.find(1)
549
+ # # => GET /people/1.xml
550
+ #
551
+ # Person.find(:all)
552
+ # # => GET /people.xml
553
+ #
554
+ # Person.find(:all, :params => { :title => "CEO" })
555
+ # # => GET /people.xml?title=CEO
556
+ #
557
+ # Person.find(:first, :from => :managers)
558
+ # # => GET /people/managers.xml
559
+ #
560
+ # Person.find(:last, :from => :managers)
561
+ # # => GET /people/managers.xml
562
+ #
563
+ # Person.find(:all, :from => "/companies/1/people.xml")
564
+ # # => GET /companies/1/people.xml
565
+ #
566
+ # Person.find(:one, :from => :leader)
567
+ # # => GET /people/leader.xml
568
+ #
569
+ # Person.find(:all, :from => :developers, :params => { :language => 'ruby' })
570
+ # # => GET /people/developers.xml?language=ruby
571
+ #
572
+ # Person.find(:one, :from => "/companies/1/manager.xml")
573
+ # # => GET /companies/1/manager.xml
574
+ #
575
+ # StreetAddress.find(1, :params => { :person_id => 1 })
576
+ # # => GET /people/1/street_addresses/1.xml
577
+ def find(*arguments)
578
+ scope = arguments.slice!(0)
579
+ options = arguments.slice!(0) || {}
580
+
581
+ case scope
582
+ when :all then find_every(options)
583
+ when :first then find_every(options).first
584
+ when :last then find_every(options).last
585
+ when :one then find_one(options)
586
+ else find_single(scope, options)
587
+ end
588
+ end
589
+
590
+ # Deletes the resources with the ID in the +id+ parameter.
591
+ #
592
+ # ==== Options
593
+ # All options specify \prefix and query parameters.
594
+ #
595
+ # ==== Examples
596
+ # Event.delete(2) # sends DELETE /events/2
597
+ #
598
+ # Event.create(:name => 'Free Concert', :location => 'Community Center')
599
+ # my_event = Event.find(:first) # let's assume this is event with ID 7
600
+ # Event.delete(my_event.id) # sends DELETE /events/7
601
+ #
602
+ # # Let's assume a request to events/5/cancel.xml
603
+ # Event.delete(params[:id]) # sends DELETE /events/5
604
+ def delete(id, options = {})
605
+ connection.delete(element_path(id, options))
606
+ end
607
+
608
+ # Asserts the existence of a resource, returning <tt>true</tt> if the resource is found.
609
+ #
610
+ # ==== Examples
611
+ # Note.create(:title => 'Hello, world.', :body => 'Nothing more for now...')
612
+ # Note.exists?(1) # => true
613
+ #
614
+ # Note.exists(1349) # => false
615
+ def exists?(id, options = {})
616
+ if id
617
+ prefix_options, query_options = split_options(options[:params])
618
+ path = element_path(id, prefix_options, query_options)
619
+ response = connection.head(path, headers)
620
+ response.code.to_i == 200
621
+ end
622
+ # id && !find_single(id, options).nil?
623
+ rescue ActiveResource::ResourceNotFound, ActiveResource::ResourceGone
624
+ false
625
+ end
626
+
627
+ private
628
+ # Find every resource
629
+ def find_every(options)
630
+ case from = options[:from]
631
+ when Symbol
632
+ instantiate_collection(get(from, options[:params]))
633
+ when String
634
+ path = "#{from}#{query_string(options[:params])}"
635
+ instantiate_collection(connection.get(path, headers) || [])
636
+ else
637
+ prefix_options, query_options = split_options(options[:params])
638
+ path = collection_path(prefix_options, query_options)
639
+ instantiate_collection( (connection.get(path, headers) || []), prefix_options )
640
+ end
641
+ end
642
+
643
+ # Find a single resource from a one-off URL
644
+ def find_one(options)
645
+ case from = options[:from]
646
+ when Symbol
647
+ instantiate_record(get(from, options[:params]))
648
+ when String
649
+ path = "#{from}#{query_string(options[:params])}"
650
+ instantiate_record(connection.get(path, headers))
651
+ end
652
+ end
653
+
654
+ # Find a single resource from the default URL
655
+ def find_single(scope, options)
656
+ prefix_options, query_options = split_options(options[:params])
657
+ path = element_path(scope, prefix_options, query_options)
658
+ instantiate_record(connection.get(path, headers), prefix_options)
659
+ end
660
+
661
+ def instantiate_collection(collection, prefix_options = {})
662
+ collection.collect! { |record| instantiate_record(record, prefix_options) }
663
+ end
664
+
665
+ def instantiate_record(record, prefix_options = {})
666
+ new(record).tap do |resource|
667
+ resource.prefix_options = prefix_options
668
+ end
669
+ end
670
+
671
+
672
+ # Accepts a URI and creates the site URI from that.
673
+ def create_site_uri_from(site)
674
+ site.is_a?(URI) ? site.dup : URI.parse(site)
675
+ end
676
+
677
+ # Accepts a URI and creates the proxy URI from that.
678
+ def create_proxy_uri_from(proxy)
679
+ proxy.is_a?(URI) ? proxy.dup : URI.parse(proxy)
680
+ end
681
+
682
+ # contains a set of the current prefix parameters.
683
+ def prefix_parameters
684
+ @prefix_parameters ||= prefix_source.scan(/:\w+/).map { |key| key[1..-1].to_sym }.to_set
685
+ end
686
+
687
+ # Builds the query string for the request.
688
+ def query_string(options)
689
+ "?#{options.to_query}" unless options.nil? || options.empty?
690
+ end
691
+
692
+ # split an option hash into two hashes, one containing the prefix options,
693
+ # and the other containing the leftovers.
694
+ def split_options(options = {})
695
+ prefix_options, query_options = {}, {}
696
+
697
+ (options || {}).each do |key, value|
698
+ next if key.blank?
699
+ (prefix_parameters.include?(key.to_sym) ? prefix_options : query_options)[key.to_sym] = value
700
+ end
701
+
702
+ [ prefix_options, query_options ]
703
+ end
704
+ end
705
+
706
+ attr_accessor :attributes #:nodoc:
707
+ attr_accessor :prefix_options #:nodoc:
708
+
709
+ # Constructor method for \new resources; the optional +attributes+ parameter takes a \hash
710
+ # of attributes for the \new resource.
711
+ #
712
+ # ==== Examples
713
+ # my_course = Course.new
714
+ # my_course.name = "Western Civilization"
715
+ # my_course.lecturer = "Don Trotter"
716
+ # my_course.save
717
+ #
718
+ # my_other_course = Course.new(:name => "Philosophy: Reason and Being", :lecturer => "Ralph Cling")
719
+ # my_other_course.save
720
+ def initialize(attributes = {})
721
+ @attributes = {}
722
+ @prefix_options = {}
723
+ load(attributes)
724
+ end
725
+
726
+ # Returns a \clone of the resource that hasn't been assigned an +id+ yet and
727
+ # is treated as a \new resource.
728
+ #
729
+ # ryan = Person.find(1)
730
+ # not_ryan = ryan.clone
731
+ # not_ryan.new? # => true
732
+ #
733
+ # Any active resource member attributes will NOT be cloned, though all other
734
+ # attributes are. This is to prevent the conflict between any +prefix_options+
735
+ # that refer to the original parent resource and the newly cloned parent
736
+ # resource that does not exist.
737
+ #
738
+ # ryan = Person.find(1)
739
+ # ryan.address = StreetAddress.find(1, :person_id => ryan.id)
740
+ # ryan.hash = {:not => "an ARes instance"}
741
+ #
742
+ # not_ryan = ryan.clone
743
+ # not_ryan.new? # => true
744
+ # not_ryan.address # => NoMethodError
745
+ # not_ryan.hash # => {:not => "an ARes instance"}
746
+ def clone
747
+ # Clone all attributes except the pk and any nested ARes
748
+ cloned = attributes.reject {|k,v| k == self.class.primary_key || v.is_a?(ActiveResource::Base)}.inject({}) do |attrs, (k, v)|
749
+ attrs[k] = v.clone
750
+ attrs
751
+ end
752
+ # Form the new resource - bypass initialize of resource with 'new' as that will call 'load' which
753
+ # attempts to convert hashes into member objects and arrays into collections of objects. We want
754
+ # the raw objects to be cloned so we bypass load by directly setting the attributes hash.
755
+ resource = self.class.new({})
756
+ resource.prefix_options = self.prefix_options
757
+ resource.send :instance_variable_set, '@attributes', cloned
758
+ resource
759
+ end
760
+
761
+
762
+ # A method to determine if the resource a \new object (i.e., it has not been POSTed to the remote service yet).
763
+ #
764
+ # ==== Examples
765
+ # not_new = Computer.create(:brand => 'Apple', :make => 'MacBook', :vendor => 'MacMall')
766
+ # not_new.new? # => false
767
+ #
768
+ # is_new = Computer.new(:brand => 'IBM', :make => 'Thinkpad', :vendor => 'IBM')
769
+ # is_new.new? # => true
770
+ #
771
+ # is_new.save
772
+ # is_new.new? # => false
773
+ #
774
+ def new?
775
+ id.nil?
776
+ end
777
+ alias :new_record? :new?
778
+
779
+ # Gets the <tt>\id</tt> attribute of the resource.
780
+ def id
781
+ attributes[self.class.primary_key]
782
+ end
783
+
784
+ # Sets the <tt>\id</tt> attribute of the resource.
785
+ def id=(id)
786
+ attributes[self.class.primary_key] = id
787
+ end
788
+
789
+ # Allows Active Resource objects to be used as parameters in Action Pack URL generation.
790
+ def to_param
791
+ id && id.to_s
792
+ end
793
+
794
+ # Test for equality. Resource are equal if and only if +other+ is the same object or
795
+ # is an instance of the same class, is not <tt>new?</tt>, and has the same +id+.
796
+ #
797
+ # ==== Examples
798
+ # ryan = Person.create(:name => 'Ryan')
799
+ # jamie = Person.create(:name => 'Jamie')
800
+ #
801
+ # ryan == jamie
802
+ # # => false (Different name attribute and id)
803
+ #
804
+ # ryan_again = Person.new(:name => 'Ryan')
805
+ # ryan == ryan_again
806
+ # # => false (ryan_again is new?)
807
+ #
808
+ # ryans_clone = Person.create(:name => 'Ryan')
809
+ # ryan == ryans_clone
810
+ # # => false (Different id attributes)
811
+ #
812
+ # ryans_twin = Person.find(ryan.id)
813
+ # ryan == ryans_twin
814
+ # # => true
815
+ #
816
+ def ==(other)
817
+ other.equal?(self) || (other.instance_of?(self.class) && other.id == id && other.prefix_options == prefix_options)
818
+ end
819
+
820
+ # Tests for equality (delegates to ==).
821
+ def eql?(other)
822
+ self == other
823
+ end
824
+
825
+ # Delegates to id in order to allow two resources of the same type and \id to work with something like:
826
+ # [Person.find(1), Person.find(2)] & [Person.find(1), Person.find(4)] # => [Person.find(1)]
827
+ def hash
828
+ id.hash
829
+ end
830
+
831
+ # Duplicate the current resource without saving it.
832
+ #
833
+ # ==== Examples
834
+ # my_invoice = Invoice.create(:customer => 'That Company')
835
+ # next_invoice = my_invoice.dup
836
+ # next_invoice.new? # => true
837
+ #
838
+ # next_invoice.save
839
+ # next_invoice == my_invoice # => false (different id attributes)
840
+ #
841
+ # my_invoice.customer # => That Company
842
+ # next_invoice.customer # => That Company
843
+ def dup
844
+ self.class.new.tap do |resource|
845
+ resource.attributes = @attributes
846
+ resource.prefix_options = @prefix_options
847
+ end
848
+ end
849
+
850
+ # A method to \save (+POST+) or \update (+PUT+) a resource. It delegates to +create+ if a \new object,
851
+ # +update+ if it is existing. If the response to the \save includes a body, it will be assumed that this body
852
+ # is XML for the final object as it looked after the \save (which would include attributes like +created_at+
853
+ # that weren't part of the original submit).
854
+ #
855
+ # ==== Examples
856
+ # my_company = Company.new(:name => 'RoleModel Software', :owner => 'Ken Auer', :size => 2)
857
+ # my_company.new? # => true
858
+ # my_company.save # sends POST /companies/ (create)
859
+ #
860
+ # my_company.new? # => false
861
+ # my_company.size = 10
862
+ # my_company.save # sends PUT /companies/1 (update)
863
+ def save
864
+ new? ? create : update
865
+ end
866
+
867
+ # Deletes the resource from the remote service.
868
+ #
869
+ # ==== Examples
870
+ # my_id = 3
871
+ # my_person = Person.find(my_id)
872
+ # my_person.destroy
873
+ # Person.find(my_id) # 404 (Resource Not Found)
874
+ #
875
+ # new_person = Person.create(:name => 'James')
876
+ # new_id = new_person.id # => 7
877
+ # new_person.destroy
878
+ # Person.find(new_id) # 404 (Resource Not Found)
879
+ def destroy
880
+ connection.delete(element_path, self.class.headers)
881
+ end
882
+
883
+ # Evaluates to <tt>true</tt> if this resource is not <tt>new?</tt> and is
884
+ # found on the remote service. Using this method, you can check for
885
+ # resources that may have been deleted between the object's instantiation
886
+ # and actions on it.
887
+ #
888
+ # ==== Examples
889
+ # Person.create(:name => 'Theodore Roosevelt')
890
+ # that_guy = Person.find(:first)
891
+ # that_guy.exists? # => true
892
+ #
893
+ # that_lady = Person.new(:name => 'Paul Bean')
894
+ # that_lady.exists? # => false
895
+ #
896
+ # guys_id = that_guy.id
897
+ # Person.delete(guys_id)
898
+ # that_guy.exists? # => false
899
+ def exists?
900
+ !new? && self.class.exists?(to_param, :params => prefix_options)
901
+ end
902
+
903
+ # Converts the resource to an XML string representation.
904
+ #
905
+ # ==== Options
906
+ # The +options+ parameter is handed off to the +to_xml+ method on each
907
+ # attribute, so it has the same options as the +to_xml+ methods in
908
+ # Active Support.
909
+ #
910
+ # * <tt>:indent</tt> - Set the indent level for the XML output (default is +2+).
911
+ # * <tt>:dasherize</tt> - Boolean option to determine whether or not element names should
912
+ # replace underscores with dashes. Default is <tt>true</tt>. The default can be set to <tt>false</tt>
913
+ # by setting the module attribute <tt>ActiveSupport.dasherize_xml = false</tt> in an initializer. Because save
914
+ # uses this method, and there are no options on save, then you will have to set the default if you don't
915
+ # want underscores in element names to become dashes when the resource is saved. This is important when
916
+ # integrating with non-Rails applications.
917
+ # * <tt>:camelize</tt> - Boolean option to determine whether or not element names should be converted
918
+ # to camel case, e.g some_name to SomeName. Default is <tt>false</tt>. Like <tt>:dasherize</tt> you can
919
+ # change the default by setting the module attribute <tt>ActiveSupport.camelise_xml = true</tt> in an initializer.
920
+ # * <tt>:skip_instruct</tt> - Toggle skipping the +instruct!+ call on the XML builder
921
+ # that generates the XML declaration (default is <tt>false</tt>).
922
+ #
923
+ # ==== Examples
924
+ # my_group = SubsidiaryGroup.find(:first)
925
+ # my_group.to_xml
926
+ # # => <?xml version="1.0" encoding="UTF-8"?>
927
+ # # <subsidiary_group> [...] </subsidiary_group>
928
+ #
929
+ # my_group.to_xml(:dasherize => true)
930
+ # # => <?xml version="1.0" encoding="UTF-8"?>
931
+ # # <subsidiary-group> [...] </subsidiary-group>
932
+ #
933
+ # my_group.to_xml(:skip_instruct => true)
934
+ # # => <subsidiary_group> [...] </subsidiary_group>
935
+ def to_xml(options={})
936
+ attributes.to_xml({:root => self.class.element_name}.merge(options))
937
+ end
938
+
939
+ # Coerces to a hash for JSON encoding.
940
+ #
941
+ # ==== Options
942
+ # The +options+ are passed to the +to_json+ method on each
943
+ # attribute, so the same options as the +to_json+ methods in
944
+ # Active Support.
945
+ #
946
+ # * <tt>:only</tt> - Only include the specified attribute or list of
947
+ # attributes in the serialized output. Attribute names must be specified
948
+ # as strings.
949
+ # * <tt>:except</tt> - Do not include the specified attribute or list of
950
+ # attributes in the serialized output. Attribute names must be specified
951
+ # as strings.
952
+ #
953
+ # ==== Examples
954
+ # person = Person.new(:first_name => "Jim", :last_name => "Smith")
955
+ # person.to_json
956
+ # # => {"first_name": "Jim", "last_name": "Smith"}
957
+ #
958
+ # person.to_json(:only => ["first_name"])
959
+ # # => {"first_name": "Jim"}
960
+ #
961
+ # person.to_json(:except => ["first_name"])
962
+ # # => {"last_name": "Smith"}
963
+ def as_json(options = nil)
964
+ attributes.as_json(options)
965
+ end
966
+
967
+ # Returns the serialized string representation of the resource in the configured
968
+ # serialization format specified in ActiveResource::Base.format. The options
969
+ # applicable depend on the configured encoding format.
970
+ def encode(options={})
971
+ case self.class.format
972
+ when ActiveResource::Formats[:xml]
973
+ self.class.format.encode(attributes, {:root => self.class.element_name}.merge(options))
974
+ else
975
+ self.class.format.encode(attributes, options)
976
+ end
977
+ end
978
+
979
+ # A method to \reload the attributes of this object from the remote web service.
980
+ #
981
+ # ==== Examples
982
+ # my_branch = Branch.find(:first)
983
+ # my_branch.name # => "Wislon Raod"
984
+ #
985
+ # # Another client fixes the typo...
986
+ #
987
+ # my_branch.name # => "Wislon Raod"
988
+ # my_branch.reload
989
+ # my_branch.name # => "Wilson Road"
990
+ def reload
991
+ self.load(self.class.find(to_param, :params => @prefix_options).attributes)
992
+ end
993
+
994
+ # A method to manually load attributes from a \hash. Recursively loads collections of
995
+ # resources. This method is called in +initialize+ and +create+ when a \hash of attributes
996
+ # is provided.
997
+ #
998
+ # ==== Examples
999
+ # my_attrs = {:name => 'J&J Textiles', :industry => 'Cloth and textiles'}
1000
+ # my_attrs = {:name => 'Marty', :colors => ["red", "green", "blue"]}
1001
+ #
1002
+ # the_supplier = Supplier.find(:first)
1003
+ # the_supplier.name # => 'J&M Textiles'
1004
+ # the_supplier.load(my_attrs)
1005
+ # the_supplier.name('J&J Textiles')
1006
+ #
1007
+ # # These two calls are the same as Supplier.new(my_attrs)
1008
+ # my_supplier = Supplier.new
1009
+ # my_supplier.load(my_attrs)
1010
+ #
1011
+ # # These three calls are the same as Supplier.create(my_attrs)
1012
+ # your_supplier = Supplier.new
1013
+ # your_supplier.load(my_attrs)
1014
+ # your_supplier.save
1015
+ def load(attributes)
1016
+ raise ArgumentError, "expected an attributes Hash, got #{attributes.inspect}" unless attributes.is_a?(Hash)
1017
+ @prefix_options, attributes = split_options(attributes)
1018
+ attributes.each do |key, value|
1019
+ @attributes[key.to_s] =
1020
+ case value
1021
+ when Array
1022
+ resource = find_or_create_resource_for_collection(key)
1023
+ value.map do |attrs|
1024
+ if attrs.is_a?(String) || attrs.is_a?(Numeric)
1025
+ attrs.duplicable? ? attrs.dup : attrs
1026
+ else
1027
+ resource.new(attrs)
1028
+ end
1029
+ end
1030
+ when Hash
1031
+ resource = find_or_create_resource_for(key)
1032
+ resource.new(value)
1033
+ else
1034
+ value.dup rescue value
1035
+ end
1036
+ end
1037
+ self
1038
+ end
1039
+
1040
+ # For checking <tt>respond_to?</tt> without searching the attributes (which is faster).
1041
+ alias_method :respond_to_without_attributes?, :respond_to?
1042
+
1043
+ # A method to determine if an object responds to a message (e.g., a method call). In Active Resource, a Person object with a
1044
+ # +name+ attribute can answer <tt>true</tt> to <tt>my_person.respond_to?(:name)</tt>, <tt>my_person.respond_to?(:name=)</tt>, and
1045
+ # <tt>my_person.respond_to?(:name?)</tt>.
1046
+ def respond_to?(method, include_priv = false)
1047
+ method_name = method.to_s
1048
+ if attributes.nil?
1049
+ return super
1050
+ elsif attributes.has_key?(method_name)
1051
+ return true
1052
+ elsif ['?','='].include?(method_name.last) && attributes.has_key?(method_name.first(-1))
1053
+ return true
1054
+ end
1055
+ # super must be called at the end of the method, because the inherited respond_to?
1056
+ # would return true for generated readers, even if the attribute wasn't present
1057
+ super
1058
+ end
1059
+
1060
+
1061
+ protected
1062
+ def connection(refresh = false)
1063
+ self.class.connection(refresh)
1064
+ end
1065
+
1066
+ # Update the resource on the remote service.
1067
+ def update
1068
+ connection.put(element_path(prefix_options), encode, self.class.headers).tap do |response|
1069
+ load_attributes_from_response(response)
1070
+ end
1071
+ end
1072
+
1073
+ # Create (i.e., \save to the remote service) the \new resource.
1074
+ def create
1075
+ connection.post(collection_path, encode, self.class.headers).tap do |response|
1076
+ self.id = id_from_response(response)
1077
+ load_attributes_from_response(response)
1078
+ end
1079
+ end
1080
+
1081
+ def load_attributes_from_response(response)
1082
+ if response['Content-Length'] != "0" && response.body.strip.size > 0
1083
+ load(self.class.format.decode(response.body))
1084
+ end
1085
+ end
1086
+
1087
+ # Takes a response from a typical create post and pulls the ID out
1088
+ def id_from_response(response)
1089
+ response['Location'][/\/([^\/]*?)(\.\w+)?$/, 1] if response['Location']
1090
+ end
1091
+
1092
+ def element_path(options = nil)
1093
+ self.class.element_path(to_param, options || prefix_options)
1094
+ end
1095
+
1096
+ def collection_path(options = nil)
1097
+ self.class.collection_path(options || prefix_options)
1098
+ end
1099
+
1100
+ private
1101
+ # Tries to find a resource for a given collection name; if it fails, then the resource is created
1102
+ def find_or_create_resource_for_collection(name)
1103
+ find_or_create_resource_for(name.to_s.singularize)
1104
+ end
1105
+
1106
+ # Tries to find a resource in a non empty list of nested modules
1107
+ # Raises a NameError if it was not found in any of the given nested modules
1108
+ def find_resource_in_modules(resource_name, module_names)
1109
+ receiver = Object
1110
+ namespaces = module_names[0, module_names.size-1].map do |module_name|
1111
+ receiver = receiver.const_get(module_name)
1112
+ end
1113
+ if namespace = namespaces.reverse.detect { |ns| ns.const_defined?(resource_name) }
1114
+ return namespace.const_get(resource_name)
1115
+ else
1116
+ raise NameError
1117
+ end
1118
+ end
1119
+
1120
+ # Tries to find a resource for a given name; if it fails, then the resource is created
1121
+ def find_or_create_resource_for(name)
1122
+ resource_name = name.to_s.camelize
1123
+ ancestors = self.class.name.split("::")
1124
+ if ancestors.size > 1
1125
+ find_resource_in_modules(resource_name, ancestors)
1126
+ else
1127
+ self.class.const_get(resource_name)
1128
+ end
1129
+ rescue NameError
1130
+ if self.class.const_defined?(resource_name)
1131
+ resource = self.class.const_get(resource_name)
1132
+ else
1133
+ resource = self.class.const_set(resource_name, Class.new(ActiveResource::Base))
1134
+ end
1135
+ resource.prefix = self.class.prefix
1136
+ resource.site = self.class.site
1137
+ resource
1138
+ end
1139
+
1140
+ def split_options(options = {})
1141
+ self.class.__send__(:split_options, options)
1142
+ end
1143
+
1144
+ def method_missing(method_symbol, *arguments) #:nodoc:
1145
+ method_name = method_symbol.to_s
1146
+
1147
+ case method_name.last
1148
+ when "="
1149
+ attributes[method_name.first(-1)] = arguments.first
1150
+ when "?"
1151
+ attributes[method_name.first(-1)]
1152
+ else
1153
+ attributes.has_key?(method_name) ? attributes[method_name] : super
1154
+ end
1155
+ end
1156
+ end
1157
+ end