Ruby on Rails | Screencasts | Download | Documentation | Weblog | Community | Source

root/trunk/activeresource/lib/active_resource/validations.rb

Revision 7098, 10.0 kB (checked in by david, 1 year ago)

Big documentation upgrade for ARes (closes #8694) [jeremymcanally]

Line 
1 module ActiveResource
2   class ResourceInvalid < ClientError  #:nodoc:
3   end
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. 
7   class Errors
8     include Enumerable
9     attr_reader :errors
10
11     delegate :empty?, :to => :errors
12    
13     def initialize(base) # :nodoc:
14       @base, @errors = base, {}
15     end
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     #
31     def add_to_base(msg)
32       add(:base, msg)
33     end
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     #
50     def add(attribute, msg)
51       @errors[attribute.to_s] = [] if @errors[attribute.to_s].nil?
52       @errors[attribute.to_s] << msg
53     end
54
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
67     def invalid?(attribute)
68       !@errors[attribute.to_s].nil?
69     end
70
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']
87     def on(attribute)
88       errors = @errors[attribute.to_s]
89       return nil if errors.nil?
90       errors.size == 1 ? errors.first : errors
91     end
92    
93     alias :[] :on
94
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     #
112     def on_base
113       on(:base)
114     end
115
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     #
128     def each
129       @errors.each_key { |attr| @errors[attr].each { |msg| yield attr, msg } }
130     end
131
132     # Yields each full error message added. So Person.errors.add("first_name", "can't be empty") will be returned
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     #
145     def each_full
146       full_messages.each { |msg| yield msg }
147     end
148
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     #
161     def full_messages
162       full_messages = []
163
164       @errors.each_key do |attr|
165         @errors[attr].each do |msg|
166           next if msg.nil?
167
168           if attr == "base"
169             full_messages << msg
170           else
171             full_messages << [attr.humanize, msg].join(' ')
172           end
173         end
174       end
175       full_messages
176     end
177
178     def clear
179       @errors = {}
180     end
181
182     # Returns the total number of errors added. Two errors added to the same attribute will be counted as such
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     #
195     def size
196       @errors.values.inject(0) { |error_count, attribute| error_count + attribute.size }
197     end
198
199     alias_method :count, :size
200     alias_method :length, :size
201    
202     # Grabs errors from the XML response.
203     def from_xml(xml)
204       clear
205       humanized_attributes = @base.attributes.keys.inject({}) { |h, attr_name| h.update(attr_name.humanize => attr_name) }
206       messages = Hash.from_xml(xml)['errors']['error'] rescue []
207       messages.each do |message|
208         attr_message = humanized_attributes.keys.detect do |attr_name|
209           if message[0, attr_name.size + 1] == "#{attr_name} "
210             add humanized_attributes[attr_name], message[(attr_name.size + 1)..-1]
211           end
212         end
213        
214         add_to_base message if attr_message.nil?
215       end
216     end
217   end
218  
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
225   #
226   #   class Person < ActiveResource::Base
227   #      self.site = "http://www.localhost.com:3000/"
228   #      protected
229   #        def validate
230   #          errors.add_on_empty %w( first_name last_name )
231   #          errors.add("phone_number", "has invalid format") unless phone_number =~ /[0-9]*/
232   #        end
233   #
234   #        def validate_on_create # is only run the first time a new object is saved
235   #          unless valid_member?(self)
236   #            errors.add("membership_discount", "has expired")
237   #          end
238   #        end
239   #
240   #        def validate_on_update
241   #          errors.add_to_base("No changes have occurred") if unchanged_attributes?
242   #        end
243   #   end
244   #   
245   #   person = Person.new("first_name" => "Jim", "phone_number" => "I will not tell you.")
246   #   person.save                         # => false (and doesn't do the save)
247   #   person.errors.empty?                # => false
248   #   person.errors.count                 # => 2
249   #   person.errors.on "last_name"        # => "can't be empty"
250   #   person.attributes = { "last_name" => "Halpert", "phone_number" => "555-5555" }
251   #   person.save                         # => true (and person is now saved to the remote service)
252   #
253   module Validations
254     def self.included(base) # :nodoc:
255       base.class_eval do
256         alias_method_chain :save, :validation
257       end
258     end
259
260     # Validate a resource and save (POST) it to the remote web service.
261     def save_with_validation
262       save_without_validation
263       true
264     rescue ResourceInvalid => error
265       errors.from_xml(error.response.body)
266       false
267     end
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
279     def valid?
280       errors.empty?
281     end
282
283     # Returns the Errors object that holds all information about attribute error messages.
284     def errors
285       @errors ||= Errors.new(self)
286     end
287   end
288 end
Note: See TracBrowser for help on using the browser.