为Rails中的validation error增加error_code

  各位同学对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 星期二

你可能感兴趣的:(Web,xml,OO,Rails,ActiveRecord)