Skip to content
This repository
Browse code

Mega documentation patches. #7025, #7069 [rwdaigle]

git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@5962 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
  • Loading branch information...
commit 932e7b003ce77d9265e910cff63a17589b8c02a2 1 parent 56c5535
risk danger olson authored
2  activeresource/CHANGELOG
... ...
@@ -1,5 +1,7 @@
1 1
 *SVN*
2 2
 
  3
+* Mega documentation patches. #7025, #7069 [rwdaigle]
  4
+
3 5
 * Base.exists?(id, options) and Base#exists? check whether the resource is found.  #6970 [rwdaigle]
4 6
 
5 7
 * Query string support.  [untext, Jeremy Kemper]
231  activeresource/README
... ...
@@ -1 +1,230 @@
1  
-= Active Resource -- Object-oriented REST services
  1
+= Active Resource -- Object-oriented REST services
  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).
  6
+
  7
+=== Configuration & Usage
  8
+
  9
+Configuration is as simple as inheriting from ActiveResource::Base and providing a site
  10
+class variable:
  11
+
  12
+   class Person < ActiveResource::Base
  13
+     self.site = "http://api.people.com:3000/"
  14
+   end
  15
+
  16
+Person is now REST enable and can invoke REST services very similarly to how ActiveRecord invokes
  17
+lifecycle methods that operate against a persistent store.
  18
+
  19
+   # Find a person with id = 1
  20
+   # This will invoke the following Http call:
  21
+   # GET http://api.people.com:3000/people/1.xml
  22
+   # and will load up the XML response into a new
  23
+   # Person object
  24
+   #
  25
+   ryan = Person.find(1)
  26
+   Person.exists?(1)  #=> true
  27
+
  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
  38
+
  39
+   # Updating is done with 'save' as well
  40
+   # PUT http://api.people.com:3000/people/1.xml
  41
+   #
  42
+   ryan = Person.find(1)
  43
+   ryan.first = 'Rizzle'
  44
+   ryan.save  #=> true
  45
+
  46
+   # And destruction
  47
+   # DELETE http://api.people.com:3000/people/1.xml
  48
+   #
  49
+   ryan = Person.find(1)
  50
+   ryan.destroy  #=> true  # Or Person.delete(ryan.id)
  51
+
  52
+
  53
+=== Protocol
  54
+
  55
+ARes is built on a standard XML format for requesting and submitting resources.  It mirrors the
  56
+RESTful routing built into ActionController, though it's useful to discuss what ARes expects
  57
+outside the context of ActionController as it is not dependent on a Rails-based RESTful implementation.
  58
+
  59
+==== Find
  60
+
  61
+GET Http requests expect the XML form of whatever resource/resources is/are being requested.  So,
  62
+for a request for a single element - the XML of that item is expected in response:
  63
+
  64
+   # Expects a response of
  65
+   #
  66
+   # <person><id>1</id><attribute1>value1</attribute1><attribute2>..</attribute2></person>
  67
+   #
  68
+   # for GET http://api.people.com:3000/people/1.xml
  69
+   #
  70
+   ryan = Person.find(1)
  71
+
  72
+The XML document that is received is used to build a new object of type Person, with each
  73
+XML element becoming an attribute on the object.
  74
+
  75
+   ryan.is_a? Person  #=> true
  76
+   ryan.attribute1  #=> 'value1'
  77
+
  78
+Any complex element (one that contains other elements) becomes its own object:
  79
+
  80
+   # With this response:
  81
+   #
  82
+   # <person><id>1</id><attribute1>value1</attribute1><complex><attribute2>value2</attribute2></complex></person>
  83
+   #
  84
+   # for GET http://api.people.com:3000/people/1.xml
  85
+   #
  86
+   ryan = Person.find(1)
  87
+   ryan.complex  #=> <Person::Complex::xxxxx>
  88
+   ryan.complex.attribute2  #=> 'value2'
  89
+
  90
+Collections can also be requested in a similar fashion
  91
+
  92
+   # Expects a response of
  93
+   #
  94
+   # <people>
  95
+   #  <person><id>1</id><first>Ryan</first></person>
  96
+   #  <person><id>2</id><first>Jim</first></person>
  97
+   # </people>
  98
+   #
  99
+   # for GET http://api.people.com:3000/people.xml
  100
+   #
  101
+   people = Person.find(:all)
  102
+   people.first  #=> <Person::xxx 'first' => 'Ryan' ...>
  103
+   people.last  #=> <Person::xxx 'first' => 'Jim' ...>
  104
+
  105
+==== Create
  106
+
  107
+Creating a new resource submits the xml form of the resource as the body of the request and expects
  108
+a 'Location' header in the response with the RESTful URL location of the newly created resource.  The
  109
+id of the newly created resource is parsed out of the Location response header and automatically set
  110
+as the id of the ARes object.
  111
+
  112
+  # <person><first>Ryan</first></person>
  113
+  #
  114
+  # is submitted as the body on
  115
+  #
  116
+  # POST http://api.people.com:3000/people.xml
  117
+  # 
  118
+  # when save is called on a new Person object.  An empty response is
  119
+  # is expected with a 'Location' header value:
  120
+  #
  121
+  # Response (200): Location: http://api.people.com:3000/people/2
  122
+  #
  123
+  ryan = Person.new(:first => 'Ryan')
  124
+  ryan.new?  #=> true
  125
+  ryan.save  #=> true
  126
+  ryan.new?  #=> false
  127
+  ryan.id    #=> 2
  128
+
  129
+==== Update
  130
+
  131
+'save' is also used to update an existing resource - and follows the same protocol as creating a resource
  132
+with the exception that no response headers are needed - just an empty response when the update on the
  133
+server side was successful.
  134
+
  135
+  # <person><first>Ryan</first></person>
  136
+  #
  137
+  # is submitted as the body on
  138
+  #
  139
+  # PUT http://api.people.com:3000/people/1.xml
  140
+  #
  141
+  # when save is called on an existing Person object.  An empty response is
  142
+  # is expected with code (204)
  143
+  #
  144
+  ryan = Person.find(1)
  145
+  ryan.first #=> 'Ryan'
  146
+  ryan.first = 'Rizzle'
  147
+  ryan.save  #=> true
  148
+
  149
+==== Delete
  150
+
  151
+Destruction of a resource can be invoked as a class and instance method of the resource.
  152
+
  153
+  # A request is made to
  154
+  #
  155
+  # DELETE http://api.people.com:3000/people/1.xml
  156
+  #
  157
+  # for both of these forms.  An empty response with
  158
+  # is expected with response code (200)
  159
+  #
  160
+  ryan = Person.find(1)
  161
+  ryan.destroy  #=> true
  162
+  ryan.exists?  #=> false
  163
+  Person.delete(2)  #=> true
  164
+  Person.exists?(2) #=> false
  165
+
  166
+
  167
+=== Errors & Validation
  168
+
  169
+Error handling and validation is handled in much the same manner as you're used to seeing in
  170
+ActiveRecord.  Both the response code in the Http response and the body of the response are used to
  171
+indicate that an error occurred.
  172
+
  173
+==== Resource errors
  174
+
  175
+When a get is requested for a resource that does not exist, the Http '404' (resource not found)
  176
+response code will be returned from the server which will raise an ActiveResource::ResourceNotFound
  177
+exception.
  178
+
  179
+  # GET http://api.people.com:3000/people/1.xml
  180
+  # #=> Response (404)
  181
+  #
  182
+  ryan = Person.find(1) #=> Raises ActiveResource::ResourceNotFound
  183
+
  184
+==== Validation errors
  185
+
  186
+Creating and updating resources can lead to validation errors - i.e. 'First name cannot be empty' etc...
  187
+These types of errors are denoted in the response by a response code of 400 and the xml representation
  188
+of the validation errors.  The save operation will then fail (with a 'false' return value) and the
  189
+validation errors can be accessed on the resource in question.
  190
+
  191
+  # When 
  192
+  #
  193
+  # PUT http://api.people.com:3000/people/1.xml
  194
+  #
  195
+  # is requested with invalid values, the expected response is:
  196
+  #
  197
+  # Response (400):
  198
+  # <errors><error>First cannot be empty</error></errors>
  199
+  #
  200
+  ryan = Person.find(1)
  201
+  ryan.first #=> ''
  202
+  ryan.save  #=> false
  203
+  ryan.errors.invalid?(:first)  #=> true
  204
+  ryan.errors.full_messages  #=> ['First cannot be empty']
  205
+
  206
+
  207
+==== Response errors
  208
+
  209
+If the underlying Http request for an ARes operation results in an error response code, an
  210
+exception will be raised.  The following Http response codes will result in these exceptions:
  211
+
  212
+   200 - 399: Valid response, no exception
  213
+   400: ActiveResource::ResourceInvalid (automatically caught by ARes validation)
  214
+   404: ActiveResource::ResourceNotFound
  215
+   409: ActiveResource::ResourceConflict
  216
+   401 - 499: ActiveResource::ClientError
  217
+   500 - 599: ActiveResource::ServerError
  218
+
  219
+   
  220
+=== Authentication
  221
+
  222
+Many REST apis will require username/password authentication, usually in the form of
  223
+Http authentication.  This can easily be specified by putting the username and password
  224
+in the Url of the ARes site:
  225
+
  226
+   class Person < ActiveResource::Base
  227
+     self.site = "http://ryan:password@api.people.com:3000/"
  228
+   end
  229
+
  230
+For obvious reasons it is best if such services are available over https.
62  activeresource/lib/active_resource/base.rb
@@ -8,6 +8,7 @@ class Base
8 8
     cattr_accessor :logger
9 9
 
10 10
     class << self
  11
+      # Gets the URI of the resource's site
11 12
       def site
12 13
         if defined?(@site)
13 14
           @site
@@ -16,20 +17,24 @@ def site
16 17
         end
17 18
       end
18 19
 
  20
+      # Set the URI for the REST resources
19 21
       def site=(site)
20 22
         @connection = nil
21 23
         @site = create_site_uri_from(site)
22 24
       end
23 25
 
  26
+      # Base connection to remote service
24 27
       def connection(refresh = false)
25 28
         @connection = Connection.new(site) if refresh || @connection.nil?
26 29
         @connection
27 30
       end
28 31
 
29  
-      attr_accessor_with_default(:element_name)    { to_s.underscore }
30  
-      attr_accessor_with_default(:collection_name) { element_name.pluralize }
31  
-      attr_accessor_with_default(:primary_key, 'id')
32  
-
  32
+      attr_accessor_with_default(:element_name)    { to_s.underscore } #:nodoc:
  33
+      attr_accessor_with_default(:collection_name) { element_name.pluralize } #:nodoc:
  34
+      attr_accessor_with_default(:primary_key, 'id') #:nodoc:
  35
+      
  36
+      # Gets the resource prefix
  37
+      #  prefix/collectionname/1.xml
33 38
       def prefix(options={})
34 39
         default = site.path
35 40
         default << '/' unless default[-1..-1] == '/'
@@ -37,6 +42,8 @@ def prefix(options={})
37 42
         prefix(options)
38 43
       end
39 44
 
  45
+      # Sets the resource prefix
  46
+      #  prefix/collectionname/1.xml
40 47
       def prefix=(value = '/')
41 48
         prefix_call = value.gsub(/:\w+/) { |key| "\#{options[#{key}]}" }
42 49
         instance_eval <<-end_eval, __FILE__, __LINE__
@@ -48,23 +55,24 @@ def prefix(options={}) "#{prefix_call}" end
48 55
         raise
49 56
       end
50 57
 
51  
-      alias_method :set_prefix, :prefix=
  58
+      alias_method :set_prefix, :prefix=  #:nodoc:
52 59
 
53  
-      alias_method :set_element_name, :element_name=
54  
-      alias_method :set_collection_name, :collection_name=
  60
+      alias_method :set_element_name, :element_name=  #:nodoc:
  61
+      alias_method :set_collection_name, :collection_name=  #:nodoc:
55 62
 
56 63
       def element_path(id, options = {})
57 64
         "#{prefix(options)}#{collection_name}/#{id}.xml#{query_string(options)}"
58 65
       end
59 66
 
60  
-      def collection_path(options = {})
  67
+      def collection_path(options = {}) 
61 68
         "#{prefix(options)}#{collection_name}.xml#{query_string(options)}"
62 69
       end
63 70
 
64  
-      alias_method :set_primary_key, :primary_key=
  71
+      alias_method :set_primary_key, :primary_key=  #:nodoc:
65 72
 
66  
-      # Person.find(1) # => GET /people/1.xml
67  
-      # StreetAddress.find(1, :person_id => 1) # => GET /people/1/street_addresses/1.xml
  73
+      # Core method for finding resources.  Used similarly to ActiveRecord's find method.
  74
+      #  Person.find(1) # => GET /people/1.xml
  75
+      #  StreetAddress.find(1, :person_id => 1) # => GET /people/1/street_addresses/1.xml
68 76
       def find(*arguments)
69 77
         scope   = arguments.slice!(0)
70 78
         options = arguments.slice!(0) || {}
@@ -80,7 +88,7 @@ def delete(id)
80 88
         connection.delete(element_path(id))
81 89
       end
82 90
 
83  
-      # True if the resource is found.
  91
+      # Evalutes to <tt>true</tt> if the resource is found.
84 92
       def exists?(id, options = {})
85 93
         id && !find_single(id, options).nil?
86 94
       rescue ActiveResource::ResourceNotFound
@@ -88,16 +96,19 @@ def exists?(id, options = {})
88 96
       end
89 97
 
90 98
       private
  99
+        # Find every resource.
91 100
         def find_every(options)
92 101
           collection = connection.get(collection_path(options)) || []
93 102
           collection.collect! { |element| new(element, options) }
94 103
         end
95 104
 
96  
-        # { :person => person1 }
  105
+        # Find a single resource.
  106
+        #  { :person => person1 }
97 107
         def find_single(scope, options)
98 108
           new(connection.get(element_path(scope, options)), options)
99 109
         end
100 110
 
  111
+        # Accepts a URI and creates the site URI from that.
101 112
         def create_site_uri_from(site)
102 113
           site.is_a?(URI) ? site.dup : URI.parse(site)
103 114
         end
@@ -106,6 +117,7 @@ def prefix_parameters
106 117
           @prefix_parameters ||= prefix_source.scan(/:\w+/).map { |key| key[1..-1].to_sym }.to_set
107 118
         end
108 119
 
  120
+        # Builds the query string for the request.
109 121
         def query_string(options)
110 122
           # Omit parameters which appear in the URI path.
111 123
           query_params = options.reject { |key, value| prefix_parameters.include?(key) }
@@ -129,8 +141,8 @@ def query_string(options)
129 141
         end
130 142
     end
131 143
 
132  
-    attr_accessor :attributes
133  
-    attr_accessor :prefix_options
  144
+    attr_accessor :attributes #:nodoc:
  145
+    attr_accessor :prefix_options #:nodoc:
134 146
 
135 147
     def initialize(attributes = {}, prefix_options = {})
136 148
       @attributes = {}
@@ -138,19 +150,22 @@ def initialize(attributes = {}, prefix_options = {})
138 150
       @prefix_options = prefix_options
139 151
     end
140 152
 
  153
+    # Is the resource a new object?
141 154
     def new?
142 155
       id.nil?
143 156
     end
144 157
 
  158
+    # Get the id of the object.
145 159
     def id
146 160
       attributes[self.class.primary_key]
147 161
     end
148 162
 
  163
+    # Set the id of the object.
149 164
     def id=(id)
150 165
       attributes[self.class.primary_key] = id
151 166
     end
152 167
 
153  
-    # 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.
  168
+    # 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+.
154 169
     def ==(other)
155 170
       other.equal?(self) || (other.instance_of?(self.class) && !other.new? && other.id == id)
156 171
     end
@@ -166,19 +181,22 @@ def hash
166 181
       id.hash
167 182
     end
168 183
 
  184
+    # Delegates to +create+ if a new object, +update+ if its old.
169 185
     def save
170 186
       new? ? create : update
171 187
     end
172 188
 
  189
+    # Delete the resource.
173 190
     def destroy
174 191
       connection.delete(element_path)
175 192
     end
176 193
 
177  
-    # True if this resource is found.
  194
+    # Evaluates to <tt>true</tt> if this resource is found.
178 195
     def exists?
179 196
       !new? && self.class.exists?(id, prefix_options)
180 197
     end
181 198
 
  199
+    # Convert the resource to an XML string
182 200
     def to_xml(options={})
183 201
       attributes.to_xml({:root => self.class.element_name}.merge(options))
184 202
     end
@@ -215,17 +233,19 @@ def connection(refresh = false)
215 233
         self.class.connection(refresh)
216 234
       end
217 235
 
  236
+      # Update the resource on the remote service.
218 237
       def update
219 238
         connection.put(element_path, to_xml)
220 239
       end
221 240
 
  241
+      # Create (i.e., save to the remote service) the new resource.
222 242
       def create
223 243
         returning connection.post(collection_path, to_xml) do |response|
224 244
           self.id = id_from_response(response)
225 245
         end
226 246
       end
227 247
 
228  
-      # takes a response from a typical create post and pulls the ID out
  248
+      # Takes a response from a typical create post and pulls the ID out
229 249
       def id_from_response(response)
230 250
         response['Location'][/\/([^\/]*?)(\.\w+)?$/, 1]
231 251
       end
@@ -239,10 +259,12 @@ def collection_path(options = nil)
239 259
       end
240 260
 
241 261
     private
  262
+      # Tries to find a resource for a given collection name; if it fails, then the resource is created
242 263
       def find_or_create_resource_for_collection(name)
243 264
         find_or_create_resource_for(name.to_s.singularize)
244 265
       end
245  
-
  266
+      
  267
+      # Tries to find a resource for a given name; if it fails, then the resource is created
246 268
       def find_or_create_resource_for(name)
247 269
         resource_name = name.to_s.camelize
248 270
         resource_name.constantize
@@ -253,7 +275,7 @@ def find_or_create_resource_for(name)
253 275
         resource
254 276
       end
255 277
 
256  
-      def method_missing(method_symbol, *arguments)
  278
+      def method_missing(method_symbol, *arguments) #:nodoc:
257 279
         method_name = method_symbol.to_s
258 280
 
259 281
         case method_name.last
19  activeresource/lib/active_resource/connection.rb
@@ -24,7 +24,7 @@ class ResourceConflict < ClientError; end  # 409 Conflict
24 24
 
25 25
   class ServerError < ConnectionError;  end  # 5xx Server Error
26 26
 
27  
-
  27
+  # Class to handle connections to remote services.
28 28
   class Connection
29 29
     attr_reader :site
30 30
 
@@ -44,27 +44,37 @@ def initialize(site)
44 44
       self.site = site
45 45
     end
46 46
 
  47
+    # Set URI for remote service.
47 48
     def site=(site)
48 49
       @site = site.is_a?(URI) ? site : URI.parse(site)
49 50
     end
50 51
 
  52
+    # Execute a GET request.
  53
+    # Used to get (find) resources.
51 54
     def get(path)
52 55
       from_xml_data(Hash.from_xml(request(:get, path, build_request_headers).body).values.first)
53 56
     end
54 57
 
  58
+    # Execute a DELETE request (see HTTP protocol documentation if unfamiliar).
  59
+    # Used to delete resources.
55 60
     def delete(path)
56 61
       request(:delete, path, build_request_headers)
57 62
     end
58 63
 
  64
+    # Execute a PUT request (see HTTP protocol documentation if unfamiliar).
  65
+    # Used to update resources.
59 66
     def put(path, body = '')
60 67
       request(:put, path, body, build_request_headers)
61 68
     end
62 69
 
  70
+    # Execute a POST request.
  71
+    # Used to create new resources.
63 72
     def post(path, body = '')
64 73
       request(:post, path, body, build_request_headers)
65 74
     end
66 75
 
67 76
     private
  77
+      # Makes request to remote service.
68 78
       def request(method, path, *arguments)
69 79
         logger.info "#{method.to_s.upcase} #{site.scheme}://#{site.host}:#{site.port}#{path}" if logger
70 80
         result = nil
@@ -73,6 +83,7 @@ def request(method, path, *arguments)
73 83
         handle_response(result)
74 84
       end
75 85
 
  86
+      # Handles response and error codes from remote service.
76 87
       def handle_response(response)
77 88
         case response.code.to_i
78 89
           when 200...400
@@ -92,6 +103,8 @@ def handle_response(response)
92 103
         end
93 104
       end
94 105
 
  106
+      # Creates new (or uses currently instantiated) Net::HTTP instance for communication with
  107
+      # remote service and resources.
95 108
       def http
96 109
         unless @http
97 110
           @http             = Net::HTTP.new(@site.host, @site.port)
@@ -102,15 +115,17 @@ def http
102 115
         @http
103 116
       end
104 117
       
  118
+      # Builds headers for request to remote service.
105 119
       def build_request_headers
106 120
         authorization_header.update(self.class.default_header)
107 121
       end
108 122
       
  123
+      # Sets authorization header; authentication information is pulled from credentials provided with site URI.
109 124
       def authorization_header
110 125
         (@site.user || @site.password ? { 'Authorization' => 'Basic ' + ["#{@site.user}:#{ @site.password}"].pack('m').delete("\r\n") } : {})
111 126
       end
112 127
 
113  
-      def logger
  128
+      def logger #:nodoc:
114 129
         ActiveResource::Base.logger
115 130
       end
116 131
 
2  activeresource/lib/active_resource/http_mock.rb
... ...
@@ -1,7 +1,7 @@
1 1
 require 'active_resource/connection'
2 2
 
3 3
 module ActiveResource
4  
-  class InvalidRequestError < StandardError; end
  4
+  class InvalidRequestError < StandardError; end #:nodoc:
5 5
 
6 6
   class HttpMock
7 7
     class Responder
9  activeresource/lib/active_resource/struct.rb
... ...
@@ -1,4 +1,13 @@
1 1
 module ActiveResource
  2
+  # Class that allows a connection to a remote resource.
  3
+  #  Person = ActiveResource::Struct.new do |p|
  4
+  #    p.uri "http://www.mypeople.com/people"
  5
+  #    p.credentials :username => "mycreds", :password => "wordofpassage"
  6
+  #  end
  7
+  #
  8
+  #  person = Person.find(1)
  9
+  #  person.name = "David"
  10
+  #  person.save!
2 11
   class Struct
3 12
     def self.create
4 13
       Class.new(Base)
36  activeresource/lib/active_resource/validations.rb
... ...
@@ -1,7 +1,9 @@
1 1
 module ActiveResource
2  
-  class ResourceInvalid < ClientError
  2
+  class ResourceInvalid < ClientError  #:nodoc:
3 3
   end
4 4
 
  5
+  # Active Resource validation is reported to and from this object, which is used by Base#save
  6
+  # to determine whether the object in a valid state to be saved. See usage example in Validations.  
5 7
   class Errors
6 8
     include Enumerable
7 9
     attr_reader :errors
@@ -100,6 +102,38 @@ def from_xml(xml)
100 102
     end
101 103
   end
102 104
   
  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:
  108
+  #
  109
+  #   class Person < ActiveResource::Base
  110
+  #      self.site = "http://www.localhost.com:3000/"
  111
+  #      protected
  112
+  #        def validate
  113
+  #          errors.add_on_empty %w( first_name last_name )
  114
+  #          errors.add("phone_number", "has invalid format") unless phone_number =~ /[0-9]*/
  115
+  #        end
  116
+  #
  117
+  #        def validate_on_create # is only run the first time a new object is saved
  118
+  #          unless valid_member?(self)
  119
+  #            errors.add("membership_discount", "has expired")
  120
+  #          end
  121
+  #        end
  122
+  #
  123
+  #        def validate_on_update
  124
+  #          errors.add_to_base("No changes have occurred") if unchanged_attributes?
  125
+  #        end
  126
+  #   end
  127
+  #   
  128
+  #   person = Person.new("first_name" => "Jim", "phone_number" => "I will not tell you.")
  129
+  #   person.save                         # => false (and doesn't do the save)
  130
+  #   person.errors.empty?                # => false
  131
+  #   person.errors.count                 # => 2
  132
+  #   person.errors.on "last_name"        # => "can't be empty"
  133
+  #   person.attributes = { "last_name" => "Halpert", "phone_number" => "555-5555" }
  134
+  #   person.save                         # => true (and person is now saved to the remote service)
  135
+  #
  136
+  # An Errors object is automatically created for every resource.
103 137
   module Validations
104 138
     def self.included(base) # :nodoc:
105 139
       base.class_eval do

0 notes on commit 932e7b0

Please sign in to comment.
Something went wrong with that request. Please try again.