Changeset 7098
- Timestamp:
- 06/23/07 17:29:54 (2 years ago)
- Files:
-
- trunk/activeresource/lib/active_resource/base.rb (modified) (17 diffs)
- trunk/activeresource/lib/active_resource/connection.rb (modified) (6 diffs)
- trunk/activeresource/lib/active_resource/custom_methods.rb (modified) (2 diffs)
- trunk/activeresource/lib/active_resource/validations.rb (modified) (12 diffs)
- trunk/activeresource/README (modified) (3 diffs)
Legend:
- Unmodified
- Added
- Removed
- Modified
- Copied
- Moved
trunk/activeresource/lib/active_resource/base.rb
r7082 r7098 4 4 5 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><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 # 6 148 class Base 7 # The logger for diagnosing and tracing A Rescalls.149 # The logger for diagnosing and tracing Active Resource calls. 8 150 cattr_accessor :logger 9 151 10 152 class << self 11 # Gets the URI of the resource's site 153 # Gets the URI of the REST resources to map for this class. The site variable is required 154 # ActiveResource's mapping to work. 12 155 def site 13 156 if defined?(@site) … … 18 161 end 19 162 20 # Set the URI for the REST resources 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. 21 165 def site=(site) 22 166 @connection = nil … … 24 168 end 25 169 26 # Base connection to remote service 170 # An instance of ActiveResource::Connection that is the base connection to the remote service. 171 # The +refresh+ parameter toggles whether or not the connection is refreshed at every request 172 # or not (defaults to +false+). 27 173 def connection(refresh = false) 28 174 @connection = Connection.new(site) if refresh || @connection.nil? … … 41 187 attr_accessor_with_default(:primary_key, 'id') #:nodoc: 42 188 43 # Gets the resource prefix44 # prefix/collectionname/1.xml189 # Gets the prefix for a resource's nested URL (e.g., <tt>prefix/collectionname/1.xml</tt>) 190 # This method is regenerated at runtime based on what the prefix is set to. 45 191 def prefix(options={}) 46 192 default = site.path … … 51 197 end 52 198 199 # An attribute reader for the source string for the resource path prefix. This 200 # method is regenerated at runtime based on what the prefix is set to. 53 201 def prefix_source 54 202 prefix # generate #prefix and #prefix_source methods first … … 56 204 end 57 205 58 # Sets the resource prefix59 # prefix/collectionname/1.xml206 # Sets the prefix for a resource's nested URL (e.g., <tt>prefix/collectionname/1.xml</tt>). 207 # Default value is <tt>site.path</tt>. 60 208 def prefix=(value = '/') 61 209 # Replace :placeholders with '#{embedded options[:lookups]}' … … 78 226 alias_method :set_collection_name, :collection_name= #:nodoc: 79 227 80 # Gets the element path for the given ID. If no query_options are given, they are split from the prefix options: 81 # 82 # Post.element_path(1) # => /posts/1.xml 83 # Comment.element_path(1, :post_id => 5) # => /posts/5/comments/1.xml 84 # Comment.element_path(1, :post_id => 5, :active => 1) # => /posts/5/comments/1.xml?active=1 85 # Comment.element_path(1, {:post_id => 5}, {:active => 1}) # => /posts/5/comments/1.xml?active=1 228 # Gets the element path for the given ID in +id+. If the +query_options+ parameter is omitted, Rails 229 # will split from the prefix options. 230 # 231 # ==== Options 232 # +prefix_options+:: A hash to add a prefix to the request for nested URL's (e.g., <tt>:account_id => 19</tt> 233 # would yield a URL like <tt>/accounts/19/purchases.xml</tt>). 234 # +query_options+:: A hash to add items to the query string for the request. 235 # 236 # ==== Examples 237 # Post.element_path(1) 238 # # => /posts/1.xml 239 # 240 # Comment.element_path(1, :post_id => 5) 241 # # => /posts/5/comments/1.xml 242 # 243 # Comment.element_path(1, :post_id => 5, :active => 1) 244 # # => /posts/5/comments/1.xml?active=1 245 # 246 # Comment.element_path(1, {:post_id => 5}, {:active => 1}) 247 # # => /posts/5/comments/1.xml?active=1 248 # 86 249 def element_path(id, prefix_options = {}, query_options = nil) 87 250 prefix_options, query_options = split_options(prefix_options) if query_options.nil? … … 89 252 end 90 253 91 # Gets the collection path. If no query_options are given, they are split from the prefix options: 92 # 93 # Post.collection_path # => /posts.xml 94 # Comment.collection_path(:post_id => 5) # => /posts/5/comments.xml 95 # Comment.collection_path(:post_id => 5, :active => 1) # => /posts/5/comments.xml?active=1 96 # Comment.collection_path({:post_id => 5}, {:active => 1}) # => /posts/5/comments.xml?active=1 254 # Gets the collection path for the REST resources. 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.collection_path 264 # # => /posts.xml 265 # 266 # Comment.collection_path(:post_id => 5) 267 # # => /posts/5/comments.xml 268 # 269 # Comment.collection_path(:post_id => 5, :active => 1) 270 # # => /posts/5/comments.xml?active=1 271 # 272 # Comment.collection_path({:post_id => 5}, {:active => 1}) 273 # # => /posts/5/comments.xml?active=1 274 # 97 275 def collection_path(prefix_options = {}, query_options = nil) 98 276 prefix_options, query_options = split_options(prefix_options) if query_options.nil? … … 103 281 104 282 # Create a new resource instance and request to the remote service 105 # that it be saved . This isequivalent to the following simultaneous calls:283 # that it be saved, making it equivalent to the following simultaneous calls: 106 284 # 107 285 # ryan = Person.new(:first => 'ryan') … … 110 288 # The newly created resource is returned. If a failure has occurred an 111 289 # exception will be raised (see save). If the resource is invalid and 112 # has not been saved then <tt>resource.valid?</tt> will return <tt>false</tt>, 113 # while <tt>resource.new?</tt> will still return <tt>true</tt>. 114 # 290 # has not been saved then valid? will return <tt>false</tt>, 291 # while new? will still return <tt>true</tt>. 292 # 293 # ==== Examples 294 # Person.create(:name => 'Jeremy', :email => 'myname@nospam.com', :enabled => true) 295 # my_person = Person.find(:first) 296 # my_person.email 297 # # => myname@nospam.com 298 # 299 # dhh = Person.create(:name => 'David', :email => 'dhh@nospam.com', :enabled => true) 300 # dhh.valid? 301 # # => true 302 # dhh.new? 303 # # => false 304 # 305 # # We'll assume that there's a validation that requires the name attribute 306 # that_guy = Person.create(:name => '', :email => 'thatguy@nospam.com', :enabled => true) 307 # that_guy.valid? 308 # # => false 309 # that_guy.new? 310 # # => true 311 # 115 312 def create(attributes = {}) 116 313 returning(self.new(attributes)) { |res| res.save } … … 119 316 # Core method for finding resources. Used similarly to Active Record's find method. 120 317 # 121 # Person.find(1) # => GET /people/1.xml 122 # Person.find(:all) # => GET /people.xml 123 # Person.find(:all, :params => { :title => "CEO" }) # => GET /people.xml?title=CEO 124 # Person.find(:all, :from => :managers) # => GET /people/managers.xml 125 # Person.find(:all, :from => "/companies/1/people.xml") # => GET /companies/1/people.xml 126 # Person.find(:one, :from => :leader) # => GET /people/leader.xml 127 # Person.find(:one, :from => "/companies/1/manager.xml") # => GET /companies/1/manager.xml 128 # StreetAddress.find(1, :params => { :person_id => 1 }) # => GET /people/1/street_addresses/1.xml 318 # ==== Arguments 319 # The first argument is considered to be the scope of the query. That is, how many 320 # resources are returned from the request. It can be one of the following. 321 # 322 # +:one+:: Returns a single resource. 323 # +:first+:: Returns the first resource found. 324 # +:all+:: Returns every resource that matches the request. 325 # 326 # ==== Options 327 # +from+:: Sets the path or custom method that resources will be fetched from. 328 # +params+:: Sets query and prefix (nested URL) parameters. 329 # 330 # ==== Examples 331 # Person.find(1) 332 # # => GET /people/1.xml 333 # 334 # Person.find(:all) 335 # # => GET /people.xml 336 # 337 # Person.find(:all, :params => { :title => "CEO" }) 338 # # => GET /people.xml?title=CEO 339 # 340 # Person.find(:first, :from => :managers) 341 # # => GET /people/managers.xml 342 # 343 # Person.find(:all, :from => "/companies/1/people.xml") 344 # # => GET /companies/1/people.xml 345 # 346 # Person.find(:one, :from => :leader) 347 # # => GET /people/leader.xml 348 # 349 # Person.find(:one, :from => "/companies/1/manager.xml") 350 # # => GET /companies/1/manager.xml 351 # 352 # StreetAddress.find(1, :params => { :person_id => 1 }) 353 # # => GET /people/1/street_addresses/1.xml 129 354 def find(*arguments) 130 355 scope = arguments.slice!(0) … … 139 364 end 140 365 366 # Deletes the resources with the ID in the +id+ parameter. 367 # 368 # ==== Options 369 # All options specify prefix and query parameters. 370 # 371 # ==== Examples 372 # Event.delete(2) 373 # # => DELETE /events/2 374 # 375 # Event.create(:name => 'Free Concert', :location => 'Community Center') 376 # my_event = Event.find(:first) 377 # # => Events (id: 7) 378 # Event.delete(my_event.id) 379 # # => DELETE /events/7 380 # 381 # # Let's assume a request to events/5/cancel.xml 382 # Event.delete(params[:id]) 383 # # => DELETE /events/5 384 # 141 385 def delete(id, options = {}) 142 386 connection.delete(element_path(id, options)) 143 387 end 144 388 145 # Evalutes to <tt>true</tt> if the resource is found. 389 # Asserts the existence of a resource, returning <tt>true</tt> if the resource is found. 390 # 391 # ==== Examples 392 # Note.create(:title => 'Hello, world.', :body => 'Nothing more for now...') 393 # Note.exists?(1) 394 # # => true 395 # 396 # Note.exists(1349) 397 # # => false 146 398 def exists?(id, options = {}) 147 399 id && !find_single(id, options).nil? … … 227 479 attr_accessor :prefix_options #:nodoc: 228 480 481 # Constructor method for new resources; the optional +attributes+ parameter takes a +Hash+ 482 # of attributes for the new resource. 483 # 484 # ==== Examples 485 # my_course = Course.new 486 # my_course.name = "Western Civilization" 487 # my_course.lecturer = "Don Trotter" 488 # my_course.save 489 # 490 # my_other_course = Course.new(:name => "Philosophy: Reason and Being", :lecturer => "Ralph Cling") 491 # my_other_course.save 229 492 def initialize(attributes = {}) 230 493 @attributes = {} … … 233 496 end 234 497 235 # Is the resource a new object? 498 # A method to determine if the resource a new object (i.e., it has not been POSTed to the remote service yet). 499 # 500 # ==== Examples 501 # not_new = Computer.create(:brand => 'Apple', :make => 'MacBook', :vendor => 'MacMall') 502 # not_new.new? 503 # # => false 504 # 505 # is_new = Computer.new(:brand => 'IBM', :make => 'Thinkpad', :vendor => 'IBM') 506 # is_new.new? 507 # # => true 508 # 509 # is_new.save 510 # is_new.new? 511 # # => false 512 # 236 513 def new? 237 514 id.nil? 238 515 end 239 516 240 # Get the id of the object.517 # Get the +id+ attribute of the resource. 241 518 def id 242 519 attributes[self.class.primary_key] 243 520 end 244 521 245 # Set the id of the object.522 # Set the +id+ attribute of the resource. 246 523 def id=(id) 247 524 attributes[self.class.primary_key] = id 248 525 end 249 526 250 # True if and only if +other+ is the same object or is an instance of the same class, is not +new?+, and has the same +id+. 527 # Test for equality. Resource are equal if and only if +other+ is the same object or 528 # is an instance of the same class, is not +new?+, and has the same +id+. 529 # 530 # ==== Examples 531 # ryan = Person.create(:name => 'Ryan') 532 # jamie = Person.create(:name => 'Jamie') 533 # 534 # ryan == jamie 535 # # => false (Different name attribute and id) 536 # 537 # ryan_again = Person.new(:name => 'Ryan') 538 # ryan == ryan_again 539 # # => false (ryan_again is new?) 540 # 541 # ryans_clone = Person.create(:name => 'Ryan') 542 # ryan == ryans_clone 543 # # => false (Different id attributes) 544 # 545 # ryans_twin = Person.find(ryan.id) 546 # ryan == ryans_twin 547 # # => true 548 # 251 549 def ==(other) 252 550 other.equal?(self) || (other.instance_of?(self.class) && !other.new? && other.id == id) 253 551 end 254 552 255 # Delegates to ==553 # Tests for equality (delegates to ==). 256 554 def eql?(other) 257 555 self == other … … 264 562 end 265 563 564 # Duplicate the current resource without saving it. 565 # 566 # ==== Examples 567 # my_invoice = Invoice.create(:customer => 'That Company') 568 # next_invoice = my_invoice.dup 569 # next_invoice.new? 570 # # => true 571 # 572 # next_invoice.save 573 # next_invoice == my_invoice 574 # # => false (different id attributes) 575 # 576 # my_invoice.customer 577 # # => That Company 578 # next_invoice.customer 579 # # => That Company 266 580 def dup 267 581 returning new do |resource| … … 271 585 end 272 586 273 # Delegates to +create+ if a new object, +update+ if its old. If the response to the save includes a body, 274 # it will be assumed that this body is XML for the final object as it looked after the save (which would include 275 # attributes like created_at that wasn't part of the original submit). 587 # A method to save (+POST+) or update (+PUT+) a resource. It delegates to +create+ if a new object, 588 # +update+ if it is existing. If the response to the save includes a body, it will be assumed that this body 589 # is XML for the final object as it looked after the save (which would include attributes like +created_at+ 590 # that weren't part of the original submit). 591 # 592 # ==== Examples 593 # my_company = Company.new(:name => 'RoleModel Software', :owner => 'Ken Auer', :size => 2) 594 # my_company.new? 595 # # => true 596 # my_company.save 597 # # => POST /companies/ (create) 598 # 599 # my_company.new? 600 # # => false 601 # my_company.size = 10 602 # my_company.save 603 # # => PUT /companies/1 (update) 276 604 def save 277 605 new? ? create : update 278 606 end 279 607 280 # Delete the resource. 608 # Deletes the resource from the remote service. 609 # 610 # ==== Examples 611 # my_id = 3 612 # my_person = Person.find(my_id) 613 # my_person.destroy 614 # Person.find(my_id) 615 # # => 404 (Resource Not Found) 616 # 617 # new_person = Person.create(:name => 'James') 618 # new_id = new_person.id 619 # # => 7 620 # new_person.destroy 621 # Person.find(new_id) 622 # # => 404 (Resource Not Found) 281 623 def destroy 282 624 connection.delete(element_path, self.class.headers) 283 625 end 284 626 285 # Evaluates to <tt>true</tt> if this resource is found. 627 # Evaluates to <tt>true</tt> if this resource is not +new?+ and is 628 # found on the remote service. Using this method, you can check for 629 # resources that may have been deleted between the object's instantiation 630 # and actions on it. 631 # 632 # ==== Examples 633 # Person.create(:name => 'Theodore Roosevelt') 634 # that_guy = Person.find(:first) 635 # that_guy.exists? 636 # # => true 637 # 638 # that_lady = Person.new(:name => 'Paul Bean') 639 # that_lady.exists? 640 # # => false 641 # 642 # guys_id = that_guy.id 643 # Person.delete(guys_id) 644 # that_guy.exists? 645 # # => false 286 646 def exists? 287 647 !new? && self.class.exists?(id, :params => prefix_options) 288 648 end 289 649 290 # Convert the resource to an XML string 650 # A method to convert the the resource to an XML string. 651 # 652 # ==== Options 653 # The +options+ parameter is handed off to the +to_xml+ method on each 654 # attribute, so it has the same options as the +to_xml+ methods in 655 # ActiveSupport. 656 # 657 # indent:: Set the indent level for the XML output (default is +2+). 658 # dasherize:: Boolean option to determine whether or not element names should 659 # replace underscores with dashes (default is +false+). 660 # skip_instruct:: Toggle skipping the +instruct!+ call on the XML builder 661 # that generates the XML declaration (default is +false+). 662 # 663 # ==== Examples 664 # my_group = SubsidiaryGroup.find(:first) 665 # my_group.to_xml 666 # # => <?xml version="1.0" encoding="UTF-8"?> 667 # # <subsidiary_group> [...] </subsidiary_group> 668 # 669 # my_group.to_xml(:dasherize => true) 670 # # => <?xml version="1.0" encoding="UTF-8"?> 671 # # <subsidiary-group> [...] </subsidiary-group> 672 # 673 # my_group.to_xml(:skip_instruct => true) 674 # # => <subsidiary_group> [...] </subsidiary_group> 291 675 def to_xml(options={}) 292 676 attributes.to_xml({:root => self.class.element_name}.merge(options)) 293 677 end 294 678 295 # Reloads the attributes of this object from the remote web service. 679 # A method to reload the attributes of this object from the remote web service. 680 # 681 # ==== Examples 682 # my_branch = Branch.find(:first) 683 # my_branch.name 684 # # => Wislon Raod 685 # 686 # # Another client fixes the typo... 687 # 688 # my_branch.name 689 # # => Wislon Raod 690 # my_branch.reload 691 # my_branch.name 692 # # => Wilson Road 296 693 def reload 297 694 self.load(self.class.find(id, :params => @prefix_options).attributes) 298 695 end 299 696 300 # Manually load attributes from a hash. Recursively loads collections of 301 # resources. 697 # A method to manually load attributes from a hash. Recursively loads collections of 698 # resources. This method is called in initialize and create when a +Hash+ of attributes 699 # is provided. 700 # 701 # ==== Examples 702 # my_attrs = {:name => 'J&J Textiles', :industry => 'Cloth and textiles'} 703 # 704 # the_supplier = Supplier.find(:first) 705 # the_supplier.name 706 # # => 'J&M Textiles' 707 # the_supplier.load(my_attrs) 708 # the_supplier.name('J&J Textiles') 709 # 710 # # These two calls are the same as Supplier.new(my_attrs) 711 # my_supplier = Supplier.new 712 # my_supplier.load(my_attrs) 713 # 714 # # These three calls are the same as Supplier.create(my_attrs) 715 # your_supplier = Supplier.new 716 # your_supplier.load(my_attrs) 717 # your_supplier.save 302 718 def load(attributes) 303 719 raise ArgumentError, "expected an attributes Hash, got #{attributes.inspect}" unless attributes.is_a?(Hash) … … 322 738 alias_method :respond_to_without_attributes?, :respond_to? 323 739 324 # A Person object with a name attribute can ask person.respond_to?("name"), person.respond_to?("name="), and 325 # person.respond_to?("name?") which will all return true. 740 # A method to determine if an object responds to a message (e.g., a method call). In Active Resource, a +Person+ object with a 741 # +name+ attribute can answer +true+ to +my_person.respond_to?("name")+, +my_person.respond_to?("name=")+, and 742 # +my_person.respond_to?("name?")+. 326 743 def respond_to?(method, include_priv = false) 327 744 method_name = method.to_s trunk/activeresource/lib/active_resource/connection.rb
r7074 r7098 6 6 7 7 module ActiveResource 8 class ConnectionError < StandardError 8 class ConnectionError < StandardError # :nodoc: 9 9 attr_reader :response 10 10 … … 19 19 end 20 20 21 class ClientError < ConnectionError; end # 4xx Client Error 22 class ResourceNotFound < ClientError; end # 404 Not Found 23 class ResourceConflict < ClientError; end # 409 Conflict 21 # 4xx Client Error 22 class ClientError < ConnectionError; end # :nodoc: 23 24 # 404 Not Found 25 class ResourceNotFound < ClientError; end # :nodoc: 26 27 # 409 Conflict 28 class ResourceConflict < ClientError; end # :nodoc: 24 29 25 class ServerError < ConnectionError; end # 5xx Server Error 30 # 5xx Server Error 31 class ServerError < ConnectionError; end # :nodoc: 26 32 27 33 # 405 Method Not Allowed 28 class MethodNotAllowed < ClientError 34 class MethodNotAllowed < ClientError # :nodoc: 29 35 def allowed_methods 30 36 @response['Allow'].split(',').map { |verb| verb.strip.downcase.to_sym } … … 32 38 end 33 39 34 # Class to handle connections to remote services. 40 # Class to handle connections to remote web services. 41 # This class is used by ActiveResource::Base to interface with REST 42 # services. 35 43 class Connection 36 44 attr_reader :site … … 47 55 end 48 56 57 # The +site+ parameter is required and will set the +site+ 58 # attribute to the URI for the remote resource service. 49 59 def initialize(site) 50 60 raise ArgumentError, 'Missing site URI' unless site … … 84 94 from_xml_data(Hash.from_xml(response.body)) 85 95 end 86 87 96 88 97 private … … 153 162 end 154 163 end 155 156 164 end 157 165 end trunk/activeresource/lib/active_resource/custom_methods.rb
r7064 r7098 1 # Support custom methods and sub-resources for REST. 1 # A module to support custom REST methods and sub-resources, allowing you to break out 2 # of the "default" REST methods with your own custom resource requests. For example, 3 # say you use Rails to expose a REST service and configure your routes with: 2 4 # 3 # Say you on the server configure your routes with: 5 # map.resources :people, :new => { :register => :post }, 6 # :element => { :promote => :put, :deactivate => :delete } 7 # :collection => { :active => :get } 4 8 # 5 # map.resources :people, :new => { :register => :post }, 6 # :element => { :promote => :put, :deactivate => :delete } 7 # :collection => { :active => :get } 9 # This route set creates routes for the following http requests: 8 10 # 9 # Which creates routes for the following http requests: 11 # POST /people/new/register.xml #=> PeopleController.register 12 # PUT /people/1/promote.xml #=> PeopleController.promote with :id => 1 13 # DELETE /people/1/deactivate.xml #=> PeopleController.deactivate with :id => 1 14 # GET /people/active.xml #=> PeopleController.active 10 15 # 11 # POST /people/new/register.xml #=> PeopleController.register 12 # PUT /people/1/promote.xml #=> PeopleController.promote with :id => 1 13 # DELETE /people/1/deactivate.xml #=> PeopleController.deactivate with :id => 1 14 # GET /people/active.xml #=> PeopleController.active 15 # 16 # This module provides the ability for Active Resource to call these 17 # custom REST methods and get the response back. 16 # Using this module, Active Resource can use these custom REST methods just like the 17 # standard methods. 18 18 # 19 19 # class Person < ActiveResource::Base … … 21 21 # end 22 22 # 23 # Person.new(:name => 'Ryan).post(:register) #=> { :id => 1, :name => 'Ryan' } 23 # Person.new(:name => 'Ryan).post(:register) # POST /people/new/register.xml 24 # # => { :id => 1, :name => 'Ryan' } 24 25 # 25 # Person.find(1).put(:promote, :position => 'Manager') 26 # Person.find(1).delete(:deactivate) 26 # Person.find(1).put(:promote, :position => 'Manager') # PUT /people/1/promote.xml 27 # Person.find(1).delete(:deactivate) # DELETE /people/1/deactivate.xml 27 28 # 28 # Person.get(:active) #=> [{:id => 1, :name => 'Ryan'}, {:id => 2, :name => 'Joe'}] 29 # Person.get(:active) # GET /people/active.xml 30 # # => [{:id => 1, :name => 'Ryan'}, {:id => 2, :name => 'Joe'}] 31 # 29 32 module ActiveResource 30 33 module CustomMethods trunk/activeresource/lib/active_resource/validations.rb
r5962 r7098 15 15 end 16 16 17 # Add an error to the base Active Resource object rather than an attribute. 18 # 19 # ==== Examples 20 # my_folder = Folder.find(1) 21 # my_folder.errors.add_to_base("You can't edit an existing folder") 22 # my_folder.errors.on_base 23 # # => "You can't edit an existing folder" 24 # 25 # my_folder.errors.add_to_base("This folder has been tagged as frozen") 26 # my_folder.valid? 27 # # => false 28 # my_folder.errors.on_base 29 # # => ["You can't edit an existing folder", "This folder has been tagged as frozen"] 30 # 17 31 def add_to_base(msg) 18 32 add(:base, msg) 19 33 end 20 34 35 # Adds an error to an Active Resource object's attribute (named for the +attribute+ parameter) 36 # with the error message in +msg+. 37 # 38 # ==== Examples 39 # my_resource = Node.find(1) 40 # my_resource.errors.add('name', 'can not be "base"') if my_resource.name == 'base' 41 # my_resource.errors.on('name') 42 # # => 'can not be "base"!' 43 # 44 # my_resource.errors.add('desc', 'can not be blank') if my_resource.desc == '' 45 # my_resource.valid? 46 # # => false 47 # my_resource.errors.on('desc') 48 # # => 'can not be blank!' 49 # 21 50 def add(attribute, msg) 22 51 @errors[attribute.to_s] = [] if @errors[attribute.to_s].nil? … … 25 54 26 55 # Returns true if the specified +attribute+ has errors associated with it. 56 # 57 # ==== Examples 58 # my_resource = Disk.find(1) 59 # my_resource.errors.add('location', 'must be Main') unless my_resource.location == 'Main' 60 # my_resource.errors.on('location') 61 # # => 'must be Main!' 62 # 63 # my_resource.errors.invalid?('location') 64 # # => true 65 # my_resource.errors.invalid?('name') 66 # # => false 27 67 def invalid?(attribute) 28 68 !@errors[attribute.to_s].nil? 29 69 end 30 70 31 # * Returns nil, if no errors are associated with the specified +attribute+. 32 # * Returns the error message, if one error is associated with the specified +attribute+. 33 # * Returns an array of error messages, if more than one error is associated with the specified +attribute+. 71 # A method to return the errors associated with +attribute+, which returns nil, if no errors are 72 # associated with the specified +attribute+, the error message if one error is associated with the specified +attribute+, 73 # or an array of error messages if more than one error is associated with the specified +attribute+. 74 # 75 # ==== Examples 76 # my_person = Person.new(params[:person]) 77 # my_person.errors.on('login') 78 # # => nil 79 # 80 # my_person.errors.add('login', 'can not be empty') if my_person.login == '' 81 # my_person.errors.on('login') 82 # # => 'can not be empty' 83 # 84 # my_person.errors.add('login', 'can not be longer than 10 characters') if my_person.login.length > 10 85 # my_person.errors.on('login') 86 # # => ['can not be empty', 'can not be longer than 10 characters'] 34 87 def on(attribute) 35 88 errors = @errors[attribute.to_s] … … 40 93 alias :[] :on 41 94 42 # Returns errors assigned to base object through add_to_base according to the normal rules of on(attribute). 95 # A method to return errors assigned to +base+ object through add_to_base, which returns nil, if no errors are 96 # associated with the specified +attribute+, the error message if one error is associated with the specified +attribute+, 97 # or an array of error messages if more than one error is associated with the specified +attribute+. 98 # 99 # ==== Examples 100 # my_account = Account.find(1) 101 # my_account.errors.on_base 102 # # => nil 103 # 104 # my_account.errors.add_to_base("This account is frozen") 105 # my_account.errors.on_base 106 # # => "This account is frozen" 107 # 108 # my_account.errors.add_to_base("This account has been closed") 109 # my_account.errors.on_base 110 # # => ["This account is frozen", "This account has been closed"] 111 # 43 112 def on_base 44 113 on(:base) … … 46 115 47 116 # Yields each attribute and associated message per error added. 117 # 118 # ==== Examples 119 # my_person = Person.new(params[:person]) 120 # 121 # my_person.errors.add('login', 'can not be empty') if my_person.login == '' 122 # my_person.errors.add('password', 'can not be empty') if my_person.password == '' 123 # messages = '' 124 # my_person.errors.each {|attr, msg| messages += attr.humanize + " " + msg + "<br />"} 125 # messages 126 # # => "Login can not be empty<br />Password can not be empty<br />" 127 # 48 128 def each 49 129 @errors.each_key { |attr| @errors[attr].each { |msg| yield attr, msg } } … … 52 132 # Yields each full error message added. So Person.errors.add("first_name", "can't be empty") will be returned 53 133 # through iteration as "First name can't be empty". 134 # 135 # ==== Examples 136 # my_person = Person.new(params[:person]) 137 # 138 # my_person.errors.add('login', 'can not be empty') if my_person.login == '' 139 # my_person.errors.add('password', 'can not be empty') if my_person.password == '' 140 # messages = '' 141 # my_person.errors.each_full {|msg| messages += msg + "<br/>"} 142 # messages 143 # # => "Login can not be empty<br />Password can not be empty<br />" 144 # 54 145 def each_full 55 146 full_messages.each { |msg| yield msg } … … 57 148 58 149 # Returns all the full error messages in an array. 150 # 151 # ==== Examples 152 # my_person = Person.new(params[:person]) 153 # 154 # my_person.errors.add('login', 'can not be empty') if my_person.login == '' 155 # my_person.errors.add('password', 'can not be empty') if my_person.password == '' 156 # messages = '' 157 # my_person.errors.full_messages.each {|msg| messages += msg + "<br/>"} 158 # messages 159 # # => "Login can not be empty<br />Password can not be empty<br />" 160 # 59 161 def full_messages 60 162 full_messages = [] … … 80 182 # Returns the total number of errors added. Two errors added to the same attribute will be counted as such 81 183 # with this as well. 184 # 185 # ==== Examples 186 # my_person = Person.new(params[:person]) 187 # my_person.errors.size 188 # # => 0 189 # 190 # my_person.errors.add('login', 'can not be empty') if my_person.login == '' 191 # my_person.errors.add('password', 'can not be empty') if my_person.password == '' 192 # my_person.error.size 193 # # => 2 194 # 82 195 def size 83 196 @errors.values.inject(0) { |error_count, attribute| error_count + attribute.size } … … 87 200 alias_method :length, :size 88 201 202 # Grabs errors from the XML response. 89 203 def from_xml(xml) 90 204 clear … … 103 217 end 104 218 105 # Module to allow validation of ActiveResource objects, which are implemented by overriding +Base#validate+ or its variants. 106 # Each of these methods can inspect the state of the object, which usually means ensuring that a number of 107 # attributes have a certain value (such as not empty, within a given range, matching a certain regular expression). For example: 219 # Module to allow validation of ActiveResource objects, which creates an Errors instance for every resource. 220 # Methods are implemented by overriding +Base#validate+ or its variants Each of these methods can inspect 221 # the state of the object, which usually means ensuring that a number of attributes have a certain value 222 # (such as not empty, within a given range, matching a certain regular expression and so on). 223 # 224 # ==== Example 108 225 # 109 226 # class Person < ActiveResource::Base … … 134 251 # person.save # => true (and person is now saved to the remote service) 135 252 # 136 # An Errors object is automatically created for every resource.137 253 module Validations 138 254 def self.included(base) # :nodoc: … … 142 258 end 143 259 260 # Validate a resource and save (POST) it to the remote web service. 144 261 def save_with_validation 145 262 save_without_validation … … 150 267 end 151 268 269 # Checks for errors on an object (i.e., is resource.errors empty?). 270 # 271 # ==== Examples 272 # my_person = Person.create(params[:person]) 273 # my_person.valid? 274 # # => true 275 # 276 # my_person.errors.add('login', 'can not be empty') if my_person.login == '' 277 # my_person.valid? 278 # # => false 152 279 def valid? 153 280 errors.empty? trunk/activeresource/README
r6025 r7098 1 = Active Resource -- Object-oriented REST services1 = Active Resource 2 2 3 Active Resource (ARes) connects business objects and REST web services. It is a library 4 intended to provide transparent proxying capabilities between a client and a RESTful 5 service (for which Rails provides the {Simply RESTful routing}[http://dev.rubyonrails.org/browser/trunk/actionpack/lib/action_controller/resources.rb] implementation). 3 Active Resource (ARes) connects business objects and Representational State Transfer (REST) 4 web services. It implements object-relational mapping for REST webservices to provide transparent 5 proxying capabilities between a client (ActiveResource) and a RESTful service (which is provided by Simply RESTful routing 6 in ActionController::Resources). 6 7 7 == = Configuration & Usage8 == Philosophy 8 9 9 Configuration is as simple as inheriting from ActiveResource::Base and providing a site 10 class variable: 10 Active Resource attempts to provide a coherent wrapper object-relational mapping for REST 11 web services. It follows the same philosophy as Active Record, in that one of its prime aims 12 is to reduce the amount of code needed to map to these resources. This is made possible 13 by relying on a number of code- and protocol-based conventions that make it easy for Active Resource 14 to infer complex relations and structures. These conventions are outlined in detail in the documentation 15 for ActiveResource::Base. 16 17 == Overview 18 19 Model classes are mapped to remote REST resources by Active Resource much the same way Active Record maps model classes to database 20 tables. When a request is made to a remote resource, a REST XML request is generated, transmitted, and the result 21 received and serialized into a usable Ruby object. 22 23 === Configuration and Usage 24 25 Putting ActiveResource to use is very similar to ActiveRecord. It's as simple as creating a model class 26 that inherits from ActiveResource::Base and providing a <tt>site</tt> class variable to it: 11 27 12 28 class Person < ActiveResource::Base … … 14 30 end 15 31 16 Person is now REST enableand can invoke REST services very similarly to how ActiveRecord invokes32 Now the Person class is REST enabled and can invoke REST services very similarly to how ActiveRecord invokes 17 33 lifecycle methods that operate against a persistent store. 18 34 19 35 # Find a person with id = 1 20 # This will invoke the following Http call:21 # GET http://api.people.com:3000/people/1.xml22 # and will load up the XML response into a new23 # Person object24 #25 36 ryan = Person.find(1) 26 37 Person.exists?(1) #=> true 27 38 28 # To create a new person - instantiate the object and call 'save', 29 # which will invoke this Http call: 30 # POST http://api.people.com:3000/people.xml 31 # (and will submit the XML format of the person object in the request) 32 # 33 ryan = Person.new(:first => 'Ryan', :last => 'Daigle') 34 ryan.save #=> true 35 ryan.id #=> 2 36 Person.exists?(ryan.id) #=> true 37 ryan.exists? #=> true 39 As you can see, the methods are quite similar to Active Record's methods for dealing with database 40 records. But rather than dealing with 38 41 39 # Resource creation can also use the convenience <tt>create</tt> method which 40 # will request a resource save after instantiation. 41 ryan = Person.create(:first => 'Ryan', :last => 'Daigle') 42 ryan.exists? #=> true 42 ==== Protocol 43 43 44 # Updating is done with 'save' as well 45 # PUT http://api.people.com:3000/people/1.xml 46 # 47 ryan = Person.find(1) 48 ryan.first = 'Rizzle' 49 ryan.save #=> true 44 Active Resource is built on a standard XML format for requesting and submitting resources over HTTP. It mirrors the RESTful routing 45 built into ActionController but will also work with any other REST service that properly implements the protocol. 46 REST uses HTTP, but unlike "typical" web applications, it makes use of all the verbs available in the HTTP specification: 50 47 51 # And destruction 52 # DELETE http://api.people.com:3000/people/1.xml 53 # 54 ryan = Person.find(1) 55 ryan.destroy #=> true # Or Person.delete(ryan.id) 48 * GET requests are used for finding and retrieving resources. 49 * POST requests are used to create new resources. 50 * PUT requests are used to update existing resources. 51 * DELETE requests are used to delete resources. 56 52 57 58 === Protocol 59 60 ARes is built on a standard XML format for requesting and submitting resources. It mirrors the 61 RESTful routing built into ActionController, though it's useful to discuss what ARes expects 62 outside the context of ActionController as it is not dependent on a Rails-based RESTful implementation. 53 For more information on how this protocol works with Active Resource, see the ActiveResource::Base documentation; 54 for more general information on REST web services, see the article here[http://en.wikipedia.org/wiki/Representational_State_Transfer]. 63 55 64 56 ==== Find … … 170 162 171 163 172 === Errors & Validation 164 You can find more usage information in the ActiveResource::Base documentation. 173 165 174 Error handling and validation is handled in much the same manner as you're used to seeing in175 ActiveRecord. Both the response code in the Http response and the body of the response are used to176 indicate that an error occurred.177 178 ==== Resource errors179 180 When a get is requested for a resource that does not exist, the Http '404' (resource not found)181 response code will be returned from the server which will raise an ActiveResource::ResourceNotFound182 exception.183 184 # GET http://api.people.com:3000/people/1.xml185 # #=> Response (404)186 #187 ryan = Person.find(1) #=> Raises ActiveResource::ResourceNotFound188 189 ==== Validation errors190 191 Creating and updating resources can lead to validation errors - i.e. 'First name cannot be empty' etc...192 These types of errors are denoted in the response by a response code of 422 and the xml representation193 of the validation errors. The save operation will then fail (with a 'false' return value) and the194 validation errors can be accessed on the resource in question.195 196 # When197 #198 # PUT http://api.people.com:3000/people/1.xml199 #200 # is requested with invalid values, the expected response is:201 #202 # Response (422):203 # <errors><error>First cannot be empty</error></errors>204 #205 ryan = Person.find(1)206 ryan.first #=> ''207 ryan.save #=> false208 ryan.errors.invalid?(:first) #=> true209 ryan.errors.full_messages #=> ['First cannot be empty']210 211 212 ==== Response errors213 214 If the underlying Http request for an ARes operation results in an error response code, an215 exception will be raised. The following Http response codes will result in these exceptions:216 217 200 - 399: Valid response, no exception218 404: ActiveResource::ResourceNotFound219 409: ActiveResource::ResourceConflict220 422: ActiveResource::ResourceInvalid (rescued by save as validation errors)221 401 - 499: ActiveResource::ClientError222 500 - 599: ActiveResource::ServerError223 224 225 === Authentication226 227 Many REST apis will require username/password authentication, usually in the form of228 Http authentication. This can easily be specified by putting the username and password229 in the Url of the ARes site:230 231 class Person < ActiveResource::Base232 self.site = "http://ryan:password@api.people.com:3000/"233 end234 235 For obvious reasons it is best if such services are available over https.