各位同学对model中一坨坨的
validates_presence_of :name, :link
之类种种的代码不会觉得陌生。在执行save,update操作,rails会自动执行validation操作,并将错误信息存放在Model#errors中。通常,对于一般web程序来将,这就够了。我们可以将validation过程中的所有错误信息显示给用户,以进行修改。但是,在web api中,则没这么简单。一般来说,api通过xml返回结果,如果用户调用一个api的时候(比如通过调用api保存一个blog),如果这个时候validation出错了怎么办呢?如果只是简单的调用Model#errors#to_xml,返回给用户,那么其结果类似:
<errors> <error>name can't be blank</error> </errors>
当然,如果api用户只是发现有错误,将message简单显示,那没问题。如果客户端要针对错误进行更加灵活的操作应该怎么办呢(类似于我们在程序中自定义很多应用程序逻辑相关的Exception)?不可能针对每一个error的message来进行针对性处理,这个时候,我们就需要向error中添加和应用程序逻辑相关的error_code。rails并不支持该功能,那我们就只能挽起衣袖,自己动手。
首先,rails中关于model的validation,error。。。相关源代码请参见:activerecord-2.0.2\lib\activerecord\validation.rb。源代码不是很复杂,这里不详细阐述,只给出我目前找到的解决方案。
由于error这个东西在rails中的实现并不是十分OO(Model#errors是一个hash,rails将validation中出现的错误都塞到里面),因此我的办法颇费周折,可谓很暴力。正是由于errors是hash,不是一个一个error对象,因此,没办法很轻巧的增加一个error_code属性,我采用了增加另外一个hash:error_with_code来表示error信息和error_code信息。这样做的好处是不会影响原有的error逻辑。
class Errors @@default_error_codes = { :default => 101 } def initialize(base) # :nodoc: @base, @errors,@errors_with_code = base, {},{} end end
在Errors类中,我还定义了一个@@default_error_codes Hash,用来存放默认的error_codes,这样方便以后的扩展(先不详细讲述,先完成主干部分)。这里,你完全可以把@errors_with_code看成和@errors一样的东西,只不过,他多了一个error_code。那具体是怎么加入这个error_code的呢?我们继续
def add(attribute, msg = @@default_error_messages[:invalid],error_code = @@default_error_codes[:default]) @errors[attribute.to_s] = [] if @errors[attribute.to_s].nil? @errors[attribute.to_s] << msg @errors_with_code[attribute.to_s] = [] if @errors_with_code[attribute.to_s].nil? @errors_with_code[attribute.to_s] << [ msg, error_code] end
上面的add函数的前两行是rails的默认行为,他只是将error message存放在errors中,后面两行是我为rails增加的行为,将error_code和message封装成一个数组,存放在errors_with_code中。如果你查看rails validation的源代码,会发现,诸如validates_confirmation_of,validates_acceptance_of。。。之类我们熟悉的validation代码会调用这个add方法,为了使得可以自定义error_code,下面最最暴力的时候来了,在所有的validates_***_of的方法中,我们必须重写对add方法的调用,以将error_code塞进去,比如,针对validates_format_of方法,我们需要做如下重写:
def validates_format_of(*attr_names) configuration = { :message => ActiveRecord::Errors.default_error_messages[:invalid], :on => :save, :with => nil } configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash) raise(ArgumentError, "A regular expression must be supplied as the :with option of the configuration hash") unless configuration[:with].is_a?(Regexp) validates_each(attr_names, configuration) do |record, attr_name, value| record.errors.add(attr_name, configuration[:message],configuration[:code]) unless value.to_s =~ configuration[:with] end end
请注意"configuration[:code]",这是和rails实现唯一不同的地方。有了他,我们在写validates_format_of的时候,就可以这样用:
validates_format_of :link, :with => /.../,:message => "error format of link",:code => 3001
这里的:code就是和应用程序相关error_code。至此,error_code的算是增加到了rails中。下面来看看我们怎么将结果(api返回错误信息的结果)返回。同样,这里,我不打算重写rails的默认to_xml行为,因为可能会影响其他程序的运作,所以,我在Error类中增加了一个to_xml_with_error_code方法:
def to_xml_with_error_code(options={}) options[:root] ||= "errors" options[:indent] ||= 2 options[:builder] ||= Builder::XmlMarkup.new(:indent => options[:indent]) options[:builder].instruct! unless options.delete(:skip_instruct) options[:builder].errors do |e| full_messages_with_error_code.each { |msg,code| e.error(msg,"error_code" => code) } end end
和rails内部实现不同之处只是倒数第三行,我使用的是full_messages_with_error_code,而不是rails本身的full_messages,并且将error_code生成在了xml中("error_code" => code),下面是具体实现:
def full_messages_with_error_code # :nodoc: full_messages = {} @errors_with_code.each_key do |attr| @errors_with_code[attr].each do |msg| next if msg.nil? msg = [ msg ].flatten msg_text , msg_error_code = msg if attr == "base" full_messages[msg_text] = msg else full_messages[@base.class.human_attribute_name(attr) + " " + msg_text] = msg_error_code end end end return full_messages end
rails内部的full_messages是一个数组,这里我使用的是一个Hash,为了方便message和error_code的对用。OK!修改工作完成!(不要忘了十分暴力的部分)下面来完成的看一下如何使用。
首先,在我们的Model中所有的validates_***_of 方法中增加:code => 2008之类的error_code,
validates_format_of :link, :with => /..../,:message => "error format of link",:code => 3001
如果validation过程中有错误,则调用:
model.errors.to_xml_with_error_code
将错误信息返回给客户,这样,客户端可以得到如下的错误结果:
<errors> <error error_code="3001">Link error format of link</error> </errors>
OK!现在客户端就可以针对error_code执行相应的逻辑处理!
2008.8.5 22:48 星期二