activeresource 2.0.1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of activeresource might be problematic. Click here for more details.

@@ -0,0 +1,47 @@
1
+ #--
2
+ # Copyright (c) 2006 David Heinemeier Hansson
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ #++
23
+
24
+ $:.unshift(File.dirname(__FILE__)) unless
25
+ $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
26
+
27
+ unless defined?(ActiveSupport)
28
+ begin
29
+ $:.unshift(File.dirname(__FILE__) + "/../../activesupport/lib")
30
+ require 'active_support'
31
+ rescue LoadError
32
+ require 'rubygems'
33
+ gem 'activesupport'
34
+ end
35
+ end
36
+
37
+ require 'active_resource/formats'
38
+ require 'active_resource/base'
39
+ require 'active_resource/validations'
40
+ require 'active_resource/custom_methods'
41
+
42
+ module ActiveResource
43
+ Base.class_eval do
44
+ include Validations
45
+ include CustomMethods
46
+ end
47
+ end
@@ -0,0 +1,872 @@
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/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.
23
+ #
24
+ # == Lifecycle methods
25
+ #
26
+ # Active Resource exposes methods for creating, finding, updating, and deleting resources
27
+ # from REST web services.
28
+ #
29
+ # ryan = Person.new(:first => 'Ryan', :last => 'Daigle')
30
+ # ryan.save #=> true
31
+ # ryan.id #=> 2
32
+ # Person.exists?(ryan.id) #=> true
33
+ # ryan.exists? #=> true
34
+ #
35
+ # ryan = Person.find(1)
36
+ # # => Resource holding our newly create Person object
37
+ #
38
+ # ryan.first = 'Rizzle'
39
+ # ryan.save #=> true
40
+ #
41
+ # ryan.destroy #=> true
42
+ #
43
+ # As you can see, these are very similar to Active Record's lifecycle methods for database records.
44
+ # You can read more about each of these methods in their respective documentation.
45
+ #
46
+ # === Custom REST methods
47
+ #
48
+ # Since simple CRUD/lifecycle methods can't accomplish every task, Active Resource also supports
49
+ # defining your own custom REST methods.
50
+ #
51
+ # Person.new(:name => 'Ryan).post(:register)
52
+ # # => { :id => 1, :name => 'Ryan', :position => 'Clerk' }
53
+ #
54
+ # Person.find(1).put(:promote, :position => 'Manager')
55
+ # # => { :id => 1, :name => 'Ryan', :position => 'Manager' }
56
+ #
57
+ # For more information on creating and using custom REST methods, see the
58
+ # ActiveResource::CustomMethods documentation.
59
+ #
60
+ # == Validations
61
+ #
62
+ # You can validate resources client side by overriding validation methods in the base class.
63
+ #
64
+ # class Person < ActiveResource::Base
65
+ # self.site = "http://api.people.com:3000/"
66
+ # protected
67
+ # def validate
68
+ # errors.add("last", "has invalid characters") unless last =~ /[a-zA-Z]*/
69
+ # end
70
+ # end
71
+ #
72
+ # See the ActiveResource::Validations documentation for more information.
73
+ #
74
+ # == Authentication
75
+ #
76
+ # Many REST APIs will require authentication, usually in the form of basic
77
+ # HTTP authentication. Authentication can be specified by putting the credentials
78
+ # in the +site+ variable of the Active Resource class you need to authenticate.
79
+ #
80
+ # class Person < ActiveResource::Base
81
+ # self.site = "http://ryan:password@api.people.com:3000/"
82
+ # end
83
+ #
84
+ # For obvious security reasons, it is probably best if such services are available
85
+ # over HTTPS.
86
+ #
87
+ # == Errors & Validation
88
+ #
89
+ # Error handling and validation is handled in much the same manner as you're used to seeing in
90
+ # Active Record. Both the response code in the Http response and the body of the response are used to
91
+ # indicate that an error occurred.
92
+ #
93
+ # === Resource errors
94
+ #
95
+ # When a get is requested for a resource that does not exist, the HTTP +404+ (Resource Not Found)
96
+ # response code will be returned from the server which will raise an ActiveResource::ResourceNotFound
97
+ # exception.
98
+ #
99
+ # # GET http://api.people.com:3000/people/999.xml
100
+ # ryan = Person.find(999) # => Raises ActiveResource::ResourceNotFound
101
+ # # => Response = 404
102
+ #
103
+ # +404+ is just one of the HTTP error response codes that ActiveResource will handle with its own exception. The
104
+ # following HTTP response codes will also result in these exceptions:
105
+ #
106
+ # 200 - 399:: Valid response, no exception
107
+ # 404:: ActiveResource::ResourceNotFound
108
+ # 409:: ActiveResource::ResourceConflict
109
+ # 422:: ActiveResource::ResourceInvalid (rescued by save as validation errors)
110
+ # 401 - 499:: ActiveResource::ClientError
111
+ # 500 - 599:: ActiveResource::ServerError
112
+ #
113
+ # These custom exceptions allow you to deal with resource errors more naturally and with more precision
114
+ # rather than returning a general HTTP error. For example:
115
+ #
116
+ # begin
117
+ # ryan = Person.find(my_id)
118
+ # rescue ActiveResource::ResourceNotFound
119
+ # redirect_to :action => 'not_found'
120
+ # rescue ActiveResource::ResourceConflict, ActiveResource::ResourceInvalid
121
+ # redirect_to :action => 'new'
122
+ # end
123
+ #
124
+ # === Validation errors
125
+ #
126
+ # Active Resource supports validations on resources and will return errors if any these validations fail
127
+ # (e.g., "First name can not be blank" and so on). These types of errors are denoted in the response by
128
+ # a response code of +422+ and an XML representation of the validation errors. The save operation will
129
+ # then fail (with a +false+ return value) and the validation errors can be accessed on the resource in question.
130
+ #
131
+ # ryan = Person.find(1)
132
+ # ryan.first #=> ''
133
+ # ryan.save #=> false
134
+ #
135
+ # # When
136
+ # # PUT http://api.people.com:3000/people/1.xml
137
+ # # is requested with invalid values, the response is:
138
+ # #
139
+ # # Response (422):
140
+ # # <errors type="array"><error>First cannot be empty</error></errors>
141
+ # #
142
+ #
143
+ # ryan.errors.invalid?(:first) #=> true
144
+ # ryan.errors.full_messages #=> ['First cannot be empty']
145
+ #
146
+ # Learn more about Active Resource's validation features in the ActiveResource::Validations documentation.
147
+ #
148
+ class Base
149
+ # The logger for diagnosing and tracing Active Resource calls.
150
+ cattr_accessor :logger
151
+
152
+ class << self
153
+ # Gets the URI of the REST resources to map for this class. The site variable is required
154
+ # ActiveResource's mapping to work.
155
+ def site
156
+ if defined?(@site)
157
+ @site
158
+ elsif superclass != Object && superclass.site
159
+ superclass.site.dup.freeze
160
+ end
161
+ end
162
+
163
+ # Sets the URI of the REST resources to map for this class to the value in the +site+ argument.
164
+ # The site variable is required ActiveResource's mapping to work.
165
+ def site=(site)
166
+ @connection = nil
167
+ @site = site.nil? ? nil : create_site_uri_from(site)
168
+ end
169
+
170
+ # Sets the format that attributes are sent and received in from a mime type reference. Example:
171
+ #
172
+ # Person.format = :json
173
+ # Person.find(1) # => GET /people/1.json
174
+ #
175
+ # Person.format = ActiveResource::Formats::XmlFormat
176
+ # Person.find(1) # => GET /people/1.xml
177
+ #
178
+ # Default format is :xml.
179
+ def format=(mime_type_reference_or_format)
180
+ format = mime_type_reference_or_format.is_a?(Symbol) ?
181
+ ActiveResource::Formats[mime_type_reference_or_format] : mime_type_reference_or_format
182
+
183
+ write_inheritable_attribute("format", format)
184
+ connection.format = format
185
+ end
186
+
187
+ # Returns the current format, default is ActiveResource::Formats::XmlFormat
188
+ def format # :nodoc:
189
+ read_inheritable_attribute("format") || ActiveResource::Formats[:xml]
190
+ end
191
+
192
+ # An instance of ActiveResource::Connection that is the base connection to the remote service.
193
+ # The +refresh+ parameter toggles whether or not the connection is refreshed at every request
194
+ # or not (defaults to +false+).
195
+ def connection(refresh = false)
196
+ if defined?(@connection) || superclass == Object
197
+ @connection = Connection.new(site, format) if refresh || @connection.nil?
198
+ @connection
199
+ else
200
+ superclass.connection
201
+ end
202
+ end
203
+
204
+ def headers
205
+ @headers ||= {}
206
+ end
207
+
208
+ # Do not include any modules in the default element name. This makes it easier to seclude ARes objects
209
+ # in a separate namespace without having to set element_name repeatedly.
210
+ attr_accessor_with_default(:element_name) { to_s.split("::").last.underscore } #:nodoc:
211
+
212
+ attr_accessor_with_default(:collection_name) { element_name.pluralize } #:nodoc:
213
+ attr_accessor_with_default(:primary_key, 'id') #:nodoc:
214
+
215
+ # Gets the prefix for a resource's nested URL (e.g., <tt>prefix/collectionname/1.xml</tt>)
216
+ # This method is regenerated at runtime based on what the prefix is set to.
217
+ def prefix(options={})
218
+ default = site.path
219
+ default << '/' unless default[-1..-1] == '/'
220
+ # generate the actual method based on the current site path
221
+ self.prefix = default
222
+ prefix(options)
223
+ end
224
+
225
+ # An attribute reader for the source string for the resource path prefix. This
226
+ # method is regenerated at runtime based on what the prefix is set to.
227
+ def prefix_source
228
+ prefix # generate #prefix and #prefix_source methods first
229
+ prefix_source
230
+ end
231
+
232
+ # Sets the prefix for a resource's nested URL (e.g., <tt>prefix/collectionname/1.xml</tt>).
233
+ # Default value is <tt>site.path</tt>.
234
+ def prefix=(value = '/')
235
+ # Replace :placeholders with '#{embedded options[:lookups]}'
236
+ prefix_call = value.gsub(/:\w+/) { |key| "\#{options[#{key}]}" }
237
+
238
+ # Redefine the new methods.
239
+ code = <<-end_code
240
+ def prefix_source() "#{value}" end
241
+ def prefix(options={}) "#{prefix_call}" end
242
+ end_code
243
+ silence_warnings { instance_eval code, __FILE__, __LINE__ }
244
+ rescue
245
+ logger.error "Couldn't set prefix: #{$!}\n #{code}"
246
+ raise
247
+ end
248
+
249
+ alias_method :set_prefix, :prefix= #:nodoc:
250
+
251
+ alias_method :set_element_name, :element_name= #:nodoc:
252
+ alias_method :set_collection_name, :collection_name= #:nodoc:
253
+
254
+ # Gets the element path for the given ID in +id+. If the +query_options+ parameter is omitted, Rails
255
+ # will split from the prefix options.
256
+ #
257
+ # ==== Options
258
+ # +prefix_options+:: A hash to add a prefix to the request for nested URL's (e.g., <tt>:account_id => 19</tt>
259
+ # would yield a URL like <tt>/accounts/19/purchases.xml</tt>).
260
+ # +query_options+:: A hash to add items to the query string for the request.
261
+ #
262
+ # ==== Examples
263
+ # Post.element_path(1)
264
+ # # => /posts/1.xml
265
+ #
266
+ # Comment.element_path(1, :post_id => 5)
267
+ # # => /posts/5/comments/1.xml
268
+ #
269
+ # Comment.element_path(1, :post_id => 5, :active => 1)
270
+ # # => /posts/5/comments/1.xml?active=1
271
+ #
272
+ # Comment.element_path(1, {:post_id => 5}, {:active => 1})
273
+ # # => /posts/5/comments/1.xml?active=1
274
+ #
275
+ def element_path(id, prefix_options = {}, query_options = nil)
276
+ prefix_options, query_options = split_options(prefix_options) if query_options.nil?
277
+ "#{prefix(prefix_options)}#{collection_name}/#{id}.#{format.extension}#{query_string(query_options)}"
278
+ end
279
+
280
+ # Gets the collection path for the REST resources. If the +query_options+ parameter is omitted, Rails
281
+ # will split from the +prefix_options+.
282
+ #
283
+ # ==== Options
284
+ # +prefix_options+:: A hash to add a prefix to the request for nested URL's (e.g., <tt>:account_id => 19</tt>
285
+ # would yield a URL like <tt>/accounts/19/purchases.xml</tt>).
286
+ # +query_options+:: A hash to add items to the query string for the request.
287
+ #
288
+ # ==== Examples
289
+ # Post.collection_path
290
+ # # => /posts.xml
291
+ #
292
+ # Comment.collection_path(:post_id => 5)
293
+ # # => /posts/5/comments.xml
294
+ #
295
+ # Comment.collection_path(:post_id => 5, :active => 1)
296
+ # # => /posts/5/comments.xml?active=1
297
+ #
298
+ # Comment.collection_path({:post_id => 5}, {:active => 1})
299
+ # # => /posts/5/comments.xml?active=1
300
+ #
301
+ def collection_path(prefix_options = {}, query_options = nil)
302
+ prefix_options, query_options = split_options(prefix_options) if query_options.nil?
303
+ "#{prefix(prefix_options)}#{collection_name}.#{format.extension}#{query_string(query_options)}"
304
+ end
305
+
306
+ alias_method :set_primary_key, :primary_key= #:nodoc:
307
+
308
+ # Create a new resource instance and request to the remote service
309
+ # that it be saved, making it equivalent to the following simultaneous calls:
310
+ #
311
+ # ryan = Person.new(:first => 'ryan')
312
+ # ryan.save
313
+ #
314
+ # The newly created resource is returned. If a failure has occurred an
315
+ # exception will be raised (see save). If the resource is invalid and
316
+ # has not been saved then valid? will return <tt>false</tt>,
317
+ # while new? will still return <tt>true</tt>.
318
+ #
319
+ # ==== Examples
320
+ # Person.create(:name => 'Jeremy', :email => 'myname@nospam.com', :enabled => true)
321
+ # my_person = Person.find(:first)
322
+ # my_person.email
323
+ # # => myname@nospam.com
324
+ #
325
+ # dhh = Person.create(:name => 'David', :email => 'dhh@nospam.com', :enabled => true)
326
+ # dhh.valid?
327
+ # # => true
328
+ # dhh.new?
329
+ # # => false
330
+ #
331
+ # # We'll assume that there's a validation that requires the name attribute
332
+ # that_guy = Person.create(:name => '', :email => 'thatguy@nospam.com', :enabled => true)
333
+ # that_guy.valid?
334
+ # # => false
335
+ # that_guy.new?
336
+ # # => true
337
+ #
338
+ def create(attributes = {})
339
+ returning(self.new(attributes)) { |res| res.save }
340
+ end
341
+
342
+ # Core method for finding resources. Used similarly to Active Record's find method.
343
+ #
344
+ # ==== Arguments
345
+ # The first argument is considered to be the scope of the query. That is, how many
346
+ # resources are returned from the request. It can be one of the following.
347
+ #
348
+ # +:one+:: Returns a single resource.
349
+ # +:first+:: Returns the first resource found.
350
+ # +:all+:: Returns every resource that matches the request.
351
+ #
352
+ # ==== Options
353
+ # +from+:: Sets the path or custom method that resources will be fetched from.
354
+ # +params+:: Sets query and prefix (nested URL) parameters.
355
+ #
356
+ # ==== Examples
357
+ # Person.find(1)
358
+ # # => GET /people/1.xml
359
+ #
360
+ # Person.find(:all)
361
+ # # => GET /people.xml
362
+ #
363
+ # Person.find(:all, :params => { :title => "CEO" })
364
+ # # => GET /people.xml?title=CEO
365
+ #
366
+ # Person.find(:first, :from => :managers)
367
+ # # => GET /people/managers.xml
368
+ #
369
+ # Person.find(:all, :from => "/companies/1/people.xml")
370
+ # # => GET /companies/1/people.xml
371
+ #
372
+ # Person.find(:one, :from => :leader)
373
+ # # => GET /people/leader.xml
374
+ #
375
+ # Person.find(:one, :from => "/companies/1/manager.xml")
376
+ # # => GET /companies/1/manager.xml
377
+ #
378
+ # StreetAddress.find(1, :params => { :person_id => 1 })
379
+ # # => GET /people/1/street_addresses/1.xml
380
+ def find(*arguments)
381
+ scope = arguments.slice!(0)
382
+ options = arguments.slice!(0) || {}
383
+
384
+ case scope
385
+ when :all then find_every(options)
386
+ when :first then find_every(options).first
387
+ when :one then find_one(options)
388
+ else find_single(scope, options)
389
+ end
390
+ end
391
+
392
+ # Deletes the resources with the ID in the +id+ parameter.
393
+ #
394
+ # ==== Options
395
+ # All options specify prefix and query parameters.
396
+ #
397
+ # ==== Examples
398
+ # Event.delete(2)
399
+ # # => DELETE /events/2
400
+ #
401
+ # Event.create(:name => 'Free Concert', :location => 'Community Center')
402
+ # my_event = Event.find(:first)
403
+ # # => Events (id: 7)
404
+ # Event.delete(my_event.id)
405
+ # # => DELETE /events/7
406
+ #
407
+ # # Let's assume a request to events/5/cancel.xml
408
+ # Event.delete(params[:id])
409
+ # # => DELETE /events/5
410
+ #
411
+ def delete(id, options = {})
412
+ connection.delete(element_path(id, options))
413
+ end
414
+
415
+ # Asserts the existence of a resource, returning <tt>true</tt> if the resource is found.
416
+ #
417
+ # ==== Examples
418
+ # Note.create(:title => 'Hello, world.', :body => 'Nothing more for now...')
419
+ # Note.exists?(1)
420
+ # # => true
421
+ #
422
+ # Note.exists(1349)
423
+ # # => false
424
+ def exists?(id, options = {})
425
+ id && !find_single(id, options).nil?
426
+ rescue ActiveResource::ResourceNotFound
427
+ false
428
+ end
429
+
430
+ private
431
+ # Find every resource
432
+ def find_every(options)
433
+ case from = options[:from]
434
+ when Symbol
435
+ instantiate_collection(get(from, options[:params]))
436
+ when String
437
+ path = "#{from}#{query_string(options[:params])}"
438
+ instantiate_collection(connection.get(path, headers) || [])
439
+ else
440
+ prefix_options, query_options = split_options(options[:params])
441
+ path = collection_path(prefix_options, query_options)
442
+ instantiate_collection( (connection.get(path, headers) || []), prefix_options )
443
+ end
444
+ end
445
+
446
+ # Find a single resource from a one-off URL
447
+ def find_one(options)
448
+ case from = options[:from]
449
+ when Symbol
450
+ instantiate_record(get(from, options[:params]))
451
+ when String
452
+ path = "#{from}#{query_string(options[:params])}"
453
+ instantiate_record(connection.get(path, headers))
454
+ end
455
+ end
456
+
457
+ # Find a single resource from the default URL
458
+ def find_single(scope, options)
459
+ prefix_options, query_options = split_options(options[:params])
460
+ path = element_path(scope, prefix_options, query_options)
461
+ instantiate_record(connection.get(path, headers), prefix_options)
462
+ end
463
+
464
+ def instantiate_collection(collection, prefix_options = {})
465
+ collection.collect! { |record| instantiate_record(record, prefix_options) }
466
+ end
467
+
468
+ def instantiate_record(record, prefix_options = {})
469
+ returning new(record) do |resource|
470
+ resource.prefix_options = prefix_options
471
+ end
472
+ end
473
+
474
+
475
+ # Accepts a URI and creates the site URI from that.
476
+ def create_site_uri_from(site)
477
+ site.is_a?(URI) ? site.dup : URI.parse(site)
478
+ end
479
+
480
+ # contains a set of the current prefix parameters.
481
+ def prefix_parameters
482
+ @prefix_parameters ||= prefix_source.scan(/:\w+/).map { |key| key[1..-1].to_sym }.to_set
483
+ end
484
+
485
+ # Builds the query string for the request.
486
+ def query_string(options)
487
+ "?#{options.to_query}" unless options.nil? || options.empty?
488
+ end
489
+
490
+ # split an option hash into two hashes, one containing the prefix options,
491
+ # and the other containing the leftovers.
492
+ def split_options(options = {})
493
+ prefix_options, query_options = {}, {}
494
+
495
+ (options || {}).each do |key, value|
496
+ next if key.blank?
497
+ (prefix_parameters.include?(key.to_sym) ? prefix_options : query_options)[key.to_sym] = value
498
+ end
499
+
500
+ [ prefix_options, query_options ]
501
+ end
502
+ end
503
+
504
+ attr_accessor :attributes #:nodoc:
505
+ attr_accessor :prefix_options #:nodoc:
506
+
507
+ # Constructor method for new resources; the optional +attributes+ parameter takes a +Hash+
508
+ # of attributes for the new resource.
509
+ #
510
+ # ==== Examples
511
+ # my_course = Course.new
512
+ # my_course.name = "Western Civilization"
513
+ # my_course.lecturer = "Don Trotter"
514
+ # my_course.save
515
+ #
516
+ # my_other_course = Course.new(:name => "Philosophy: Reason and Being", :lecturer => "Ralph Cling")
517
+ # my_other_course.save
518
+ def initialize(attributes = {})
519
+ @attributes = {}
520
+ @prefix_options = {}
521
+ load(attributes)
522
+ end
523
+
524
+ # A method to determine if the resource a new object (i.e., it has not been POSTed to the remote service yet).
525
+ #
526
+ # ==== Examples
527
+ # not_new = Computer.create(:brand => 'Apple', :make => 'MacBook', :vendor => 'MacMall')
528
+ # not_new.new?
529
+ # # => false
530
+ #
531
+ # is_new = Computer.new(:brand => 'IBM', :make => 'Thinkpad', :vendor => 'IBM')
532
+ # is_new.new?
533
+ # # => true
534
+ #
535
+ # is_new.save
536
+ # is_new.new?
537
+ # # => false
538
+ #
539
+ def new?
540
+ id.nil?
541
+ end
542
+
543
+ # Get the +id+ attribute of the resource.
544
+ def id
545
+ attributes[self.class.primary_key]
546
+ end
547
+
548
+ # Set the +id+ attribute of the resource.
549
+ def id=(id)
550
+ attributes[self.class.primary_key] = id
551
+ end
552
+
553
+ # Allows ActiveResource objects to be used as parameters in ActionPack URL generation.
554
+ def to_param
555
+ id && id.to_s
556
+ end
557
+
558
+ # Test for equality. Resource are equal if and only if +other+ is the same object or
559
+ # is an instance of the same class, is not +new?+, and has the same +id+.
560
+ #
561
+ # ==== Examples
562
+ # ryan = Person.create(:name => 'Ryan')
563
+ # jamie = Person.create(:name => 'Jamie')
564
+ #
565
+ # ryan == jamie
566
+ # # => false (Different name attribute and id)
567
+ #
568
+ # ryan_again = Person.new(:name => 'Ryan')
569
+ # ryan == ryan_again
570
+ # # => false (ryan_again is new?)
571
+ #
572
+ # ryans_clone = Person.create(:name => 'Ryan')
573
+ # ryan == ryans_clone
574
+ # # => false (Different id attributes)
575
+ #
576
+ # ryans_twin = Person.find(ryan.id)
577
+ # ryan == ryans_twin
578
+ # # => true
579
+ #
580
+ def ==(other)
581
+ other.equal?(self) || (other.instance_of?(self.class) && !other.new? && other.id == id)
582
+ end
583
+
584
+ # Tests for equality (delegates to ==).
585
+ def eql?(other)
586
+ self == other
587
+ end
588
+
589
+ # Delegates to id in order to allow two resources of the same type and id to work with something like:
590
+ # [Person.find(1), Person.find(2)] & [Person.find(1), Person.find(4)] # => [Person.find(1)]
591
+ def hash
592
+ id.hash
593
+ end
594
+
595
+ # Duplicate the current resource without saving it.
596
+ #
597
+ # ==== Examples
598
+ # my_invoice = Invoice.create(:customer => 'That Company')
599
+ # next_invoice = my_invoice.dup
600
+ # next_invoice.new?
601
+ # # => true
602
+ #
603
+ # next_invoice.save
604
+ # next_invoice == my_invoice
605
+ # # => false (different id attributes)
606
+ #
607
+ # my_invoice.customer
608
+ # # => That Company
609
+ # next_invoice.customer
610
+ # # => That Company
611
+ def dup
612
+ returning self.class.new do |resource|
613
+ resource.attributes = @attributes
614
+ resource.prefix_options = @prefix_options
615
+ end
616
+ end
617
+
618
+ # A method to save (+POST+) or update (+PUT+) a resource. It delegates to +create+ if a new object,
619
+ # +update+ if it is existing. If the response to the save includes a body, it will be assumed that this body
620
+ # is XML for the final object as it looked after the save (which would include attributes like +created_at+
621
+ # that weren't part of the original submit).
622
+ #
623
+ # ==== Examples
624
+ # my_company = Company.new(:name => 'RoleModel Software', :owner => 'Ken Auer', :size => 2)
625
+ # my_company.new?
626
+ # # => true
627
+ # my_company.save
628
+ # # => POST /companies/ (create)
629
+ #
630
+ # my_company.new?
631
+ # # => false
632
+ # my_company.size = 10
633
+ # my_company.save
634
+ # # => PUT /companies/1 (update)
635
+ def save
636
+ new? ? create : update
637
+ end
638
+
639
+ # Deletes the resource from the remote service.
640
+ #
641
+ # ==== Examples
642
+ # my_id = 3
643
+ # my_person = Person.find(my_id)
644
+ # my_person.destroy
645
+ # Person.find(my_id)
646
+ # # => 404 (Resource Not Found)
647
+ #
648
+ # new_person = Person.create(:name => 'James')
649
+ # new_id = new_person.id
650
+ # # => 7
651
+ # new_person.destroy
652
+ # Person.find(new_id)
653
+ # # => 404 (Resource Not Found)
654
+ def destroy
655
+ connection.delete(element_path, self.class.headers)
656
+ end
657
+
658
+ # Evaluates to <tt>true</tt> if this resource is not +new?+ and is
659
+ # found on the remote service. Using this method, you can check for
660
+ # resources that may have been deleted between the object's instantiation
661
+ # and actions on it.
662
+ #
663
+ # ==== Examples
664
+ # Person.create(:name => 'Theodore Roosevelt')
665
+ # that_guy = Person.find(:first)
666
+ # that_guy.exists?
667
+ # # => true
668
+ #
669
+ # that_lady = Person.new(:name => 'Paul Bean')
670
+ # that_lady.exists?
671
+ # # => false
672
+ #
673
+ # guys_id = that_guy.id
674
+ # Person.delete(guys_id)
675
+ # that_guy.exists?
676
+ # # => false
677
+ def exists?
678
+ !new? && self.class.exists?(id, :params => prefix_options)
679
+ end
680
+
681
+ # A method to convert the the resource to an XML string.
682
+ #
683
+ # ==== Options
684
+ # The +options+ parameter is handed off to the +to_xml+ method on each
685
+ # attribute, so it has the same options as the +to_xml+ methods in
686
+ # ActiveSupport.
687
+ #
688
+ # indent:: Set the indent level for the XML output (default is +2+).
689
+ # dasherize:: Boolean option to determine whether or not element names should
690
+ # replace underscores with dashes (default is +false+).
691
+ # skip_instruct:: Toggle skipping the +instruct!+ call on the XML builder
692
+ # that generates the XML declaration (default is +false+).
693
+ #
694
+ # ==== Examples
695
+ # my_group = SubsidiaryGroup.find(:first)
696
+ # my_group.to_xml
697
+ # # => <?xml version="1.0" encoding="UTF-8"?>
698
+ # # <subsidiary_group> [...] </subsidiary_group>
699
+ #
700
+ # my_group.to_xml(:dasherize => true)
701
+ # # => <?xml version="1.0" encoding="UTF-8"?>
702
+ # # <subsidiary-group> [...] </subsidiary-group>
703
+ #
704
+ # my_group.to_xml(:skip_instruct => true)
705
+ # # => <subsidiary_group> [...] </subsidiary_group>
706
+ def to_xml(options={})
707
+ attributes.to_xml({:root => self.class.element_name}.merge(options))
708
+ end
709
+
710
+ # A method to reload the attributes of this object from the remote web service.
711
+ #
712
+ # ==== Examples
713
+ # my_branch = Branch.find(:first)
714
+ # my_branch.name
715
+ # # => Wislon Raod
716
+ #
717
+ # # Another client fixes the typo...
718
+ #
719
+ # my_branch.name
720
+ # # => Wislon Raod
721
+ # my_branch.reload
722
+ # my_branch.name
723
+ # # => Wilson Road
724
+ def reload
725
+ self.load(self.class.find(id, :params => @prefix_options).attributes)
726
+ end
727
+
728
+ # A method to manually load attributes from a hash. Recursively loads collections of
729
+ # resources. This method is called in initialize and create when a +Hash+ of attributes
730
+ # is provided.
731
+ #
732
+ # ==== Examples
733
+ # my_attrs = {:name => 'J&J Textiles', :industry => 'Cloth and textiles'}
734
+ #
735
+ # the_supplier = Supplier.find(:first)
736
+ # the_supplier.name
737
+ # # => 'J&M Textiles'
738
+ # the_supplier.load(my_attrs)
739
+ # the_supplier.name('J&J Textiles')
740
+ #
741
+ # # These two calls are the same as Supplier.new(my_attrs)
742
+ # my_supplier = Supplier.new
743
+ # my_supplier.load(my_attrs)
744
+ #
745
+ # # These three calls are the same as Supplier.create(my_attrs)
746
+ # your_supplier = Supplier.new
747
+ # your_supplier.load(my_attrs)
748
+ # your_supplier.save
749
+ def load(attributes)
750
+ raise ArgumentError, "expected an attributes Hash, got #{attributes.inspect}" unless attributes.is_a?(Hash)
751
+ @prefix_options, attributes = split_options(attributes)
752
+ attributes.each do |key, value|
753
+ @attributes[key.to_s] =
754
+ case value
755
+ when Array
756
+ resource = find_or_create_resource_for_collection(key)
757
+ value.map { |attrs| resource.new(attrs) }
758
+ when Hash
759
+ resource = find_or_create_resource_for(key)
760
+ resource.new(value)
761
+ else
762
+ value.dup rescue value
763
+ end
764
+ end
765
+ self
766
+ end
767
+
768
+ # For checking respond_to? without searching the attributes (which is faster).
769
+ alias_method :respond_to_without_attributes?, :respond_to?
770
+
771
+ # A method to determine if an object responds to a message (e.g., a method call). In Active Resource, a +Person+ object with a
772
+ # +name+ attribute can answer +true+ to +my_person.respond_to?("name")+, +my_person.respond_to?("name=")+, and
773
+ # +my_person.respond_to?("name?")+.
774
+ def respond_to?(method, include_priv = false)
775
+ method_name = method.to_s
776
+ if attributes.nil?
777
+ return super
778
+ elsif attributes.has_key?(method_name)
779
+ return true
780
+ elsif ['?','='].include?(method_name.last) && attributes.has_key?(method_name.first(-1))
781
+ return true
782
+ end
783
+ # super must be called at the end of the method, because the inherited respond_to?
784
+ # would return true for generated readers, even if the attribute wasn't present
785
+ super
786
+ end
787
+
788
+
789
+ protected
790
+ def connection(refresh = false)
791
+ self.class.connection(refresh)
792
+ end
793
+
794
+ # Update the resource on the remote service.
795
+ def update
796
+ returning connection.put(element_path(prefix_options), to_xml, self.class.headers) do |response|
797
+ load_attributes_from_response(response)
798
+ end
799
+ end
800
+
801
+ # Create (i.e., save to the remote service) the new resource.
802
+ def create
803
+ returning connection.post(collection_path, to_xml, self.class.headers) do |response|
804
+ self.id = id_from_response(response)
805
+ load_attributes_from_response(response)
806
+ end
807
+ end
808
+
809
+ def load_attributes_from_response(response)
810
+ if response['Content-size'] != "0" && response.body.strip.size > 0
811
+ load(self.class.format.decode(response.body))
812
+ end
813
+ end
814
+
815
+ # Takes a response from a typical create post and pulls the ID out
816
+ def id_from_response(response)
817
+ response['Location'][/\/([^\/]*?)(\.\w+)?$/, 1]
818
+ end
819
+
820
+ def element_path(options = nil)
821
+ self.class.element_path(id, options || prefix_options)
822
+ end
823
+
824
+ def collection_path(options = nil)
825
+ self.class.collection_path(options || prefix_options)
826
+ end
827
+
828
+ private
829
+ # Tries to find a resource for a given collection name; if it fails, then the resource is created
830
+ def find_or_create_resource_for_collection(name)
831
+ find_or_create_resource_for(name.to_s.singularize)
832
+ end
833
+
834
+ # Tries to find a resource for a given name; if it fails, then the resource is created
835
+ def find_or_create_resource_for(name)
836
+ resource_name = name.to_s.camelize
837
+
838
+ # FIXME: Make it generic enough to support any depth of module nesting
839
+ if (ancestors = self.class.name.split("::")).size > 1
840
+ begin
841
+ ancestors.first.constantize.const_get(resource_name)
842
+ rescue NameError
843
+ self.class.const_get(resource_name)
844
+ end
845
+ else
846
+ self.class.const_get(resource_name)
847
+ end
848
+ rescue NameError
849
+ resource = self.class.const_set(resource_name, Class.new(ActiveResource::Base))
850
+ resource.prefix = self.class.prefix
851
+ resource.site = self.class.site
852
+ resource
853
+ end
854
+
855
+ def split_options(options = {})
856
+ self.class.send!(:split_options, options)
857
+ end
858
+
859
+ def method_missing(method_symbol, *arguments) #:nodoc:
860
+ method_name = method_symbol.to_s
861
+
862
+ case method_name.last
863
+ when "="
864
+ attributes[method_name.first(-1)] = arguments.first
865
+ when "?"
866
+ attributes[method_name.first(-1)]
867
+ else
868
+ attributes.has_key?(method_name) ? attributes[method_name] : super
869
+ end
870
+ end
871
+ end
872
+ end