Skip to content
This repository
Browse code

Added validations to ActiveResource. Added a smoke test to see if we …

…can add a validation and use it, and add a validates callback and use it.

Signed-off-by: Joshua Peek <josh@joshpeek.com>
  • Loading branch information...
commit c2f90d6530dfd0ed68df9f4c429d0f498235e1d4 1 parent ef93524
authored josh committed
64  activeresource/lib/active_resource/validations.rb
@@ -8,8 +8,10 @@ class ResourceInvalid < ClientError  #:nodoc:
8 8
   # to determine whether the object in a valid state to be saved. See usage example in Validations.  
9 9
   class Errors < ActiveModel::Errors
10 10
     # Grabs errors from an array of messages (like ActiveRecord::Validations)
11  
-    def from_array(messages)
12  
-      clear
  11
+    # The second parameter directs the errors cache to be cleared (default)
  12
+    # or not (by passing true)
  13
+    def from_array(messages, save_cache = false)
  14
+      clear unless save_cache
13 15
       humanized_attributes = @base.attributes.keys.inject({}) { |h, attr_name| h.update(attr_name.humanize => attr_name) }
14 16
       messages.each do |message|
15 17
         attr_message = humanized_attributes.keys.detect do |attr_name|
@@ -22,16 +24,16 @@ def from_array(messages)
22 24
       end
23 25
     end
24 26
 
25  
-    # Grabs errors from the json response.
26  
-    def from_json(json)
  27
+    # Grabs errors from a json response.
  28
+    def from_json(json, save_cache = false)
27 29
       array = ActiveSupport::JSON.decode(json)['errors'] rescue []
28  
-      from_array array
  30
+      from_array array, save_cache
29 31
     end
30 32
 
31  
-    # Grabs errors from the XML response.
32  
-    def from_xml(xml)
  33
+    # Grabs errors from an XML response.
  34
+    def from_xml(xml, save_cache = false)
33 35
       array = Array.wrap(Hash.from_xml(xml)['errors']['error']) rescue []
34  
-      from_array array
  36
+      from_array array, save_cache
35 37
     end
36 38
   end
37 39
   
@@ -57,26 +59,55 @@ def from_xml(xml)
57 59
   #
58 60
   module Validations
59 61
     extend ActiveSupport::Concern
  62
+    include ActiveModel::Validations
  63
+    extend ActiveModel::Validations::ClassMethods
60 64
 
61 65
     included do
62 66
       alias_method_chain :save, :validation
63 67
     end
64 68
 
65 69
     # Validate a resource and save (POST) it to the remote web service.
66  
-    def save_with_validation
67  
-      save_without_validation
68  
-      true
  70
+    # If any local validations fail - the save (POST) will not be attempted.
  71
+    def save_with_validation(perform_validation = true)
  72
+      # clear the remote validations so they don't interfere with the local
  73
+      # ones. Otherwise we get an endless loop and can never change the
  74
+      # fields so as to make the resource valid
  75
+      @remote_errors = nil
  76
+      if perform_validation && valid? || !perform_validation
  77
+        save_without_validation
  78
+        true
  79
+      else
  80
+        false
  81
+      end
69 82
     rescue ResourceInvalid => error
70  
-      case error.response['Content-Type']
  83
+      # cache the remote errors because every call to <tt>valid?</tt> clears
  84
+      # all errors. We must keep a copy to add these back after local
  85
+      # validations
  86
+      @remote_errors = error
  87
+      load_remote_errors(@remote_errors, true)
  88
+      false
  89
+    end
  90
+
  91
+
  92
+    # Loads the set of remote errors into the object's Errors based on the
  93
+    # content-type of the error-block received
  94
+    def load_remote_errors(remote_errors, save_cache = false ) #:nodoc:
  95
+      case remote_errors.response['Content-Type']
71 96
       when 'application/xml'
72  
-        errors.from_xml(error.response.body)
  97
+        errors.from_xml(remote_errors.response.body, save_cache)
73 98
       when 'application/json'
74  
-        errors.from_json(error.response.body)
  99
+        errors.from_json(remote_errors.response.body, save_cache)
75 100
       end
76  
-      false
77 101
     end
78 102
 
79 103
     # Checks for errors on an object (i.e., is resource.errors empty?).
  104
+    #
  105
+    # Runs all the specified local validations and returns true if no errors
  106
+    # were added, otherwise false.
  107
+    # Runs local validations (eg those on your Active Resource model), and
  108
+    # also any errors returned from the remote system the last time we
  109
+    # saved.
  110
+    # Remote errors can only be cleared by trying to re-save the resource.
80 111
     # 
81 112
     # ==== Examples
82 113
     #   my_person = Person.create(params[:person])
@@ -86,7 +117,10 @@ def save_with_validation
86 117
     #   my_person.errors.add('login', 'can not be empty') if my_person.login == ''
87 118
     #   my_person.valid?
88 119
     #   # => false
  120
+    #
89 121
     def valid?
  122
+      super
  123
+      load_remote_errors(@remote_errors, true) if defined?(@remote_errors) && @remote_errors.present?
90 124
       errors.empty?
91 125
     end
92 126
 
25  activeresource/test/fixtures/project.rb
... ...
@@ -0,0 +1,25 @@
  1
+# used to test validations
  2
+class Project < ActiveResource::Base
  3
+  self.site = "http://37s.sunrise.i:3000"
  4
+
  5
+  validates_presence_of :name
  6
+  validate :description_greater_than_three_letters
  7
+
  8
+  # to test the validate *callback* works
  9
+  def description_greater_than_three_letters
  10
+    errors.add :description, 'must be greater than three letters long' if description.length < 3 unless description.blank?
  11
+  end
  12
+
  13
+
  14
+  # stop-gap accessor to default this attribute to nil
  15
+  # Otherwise the validations fail saying that the method does not exist.
  16
+  # In future, method_missing will be updated to not explode on a known
  17
+  # attribute.
  18
+  def name
  19
+    attributes['name'] || nil
  20
+  end
  21
+  def description
  22
+    attributes['description'] || nil
  23
+  end
  24
+end
  25
+
49  activeresource/test/validations_test.rb
... ...
@@ -0,0 +1,49 @@
  1
+require 'abstract_unit'
  2
+require "fixtures/project"
  3
+
  4
+# The validations are tested thoroughly under ActiveModel::Validations
  5
+# This test case simply makes sur that they are all accessible by
  6
+# Active Resource objects.
  7
+class ValidationsTest < ActiveModel::TestCase
  8
+  VALID_PROJECT_HASH = { :name => "My Project", :description => "A project" }
  9
+  def setup
  10
+    @my_proj = VALID_PROJECT_HASH.to_xml(:root => "person")
  11
+    ActiveResource::HttpMock.respond_to do |mock|
  12
+      mock.post "/projects.xml", {}, @my_proj, 201, 'Location' => '/projects/5.xml'
  13
+    end
  14
+  end
  15
+
  16
+  def test_validates_presence_of
  17
+    p = new_project(:name => nil)
  18
+    assert !p.valid?, "should not be a valid record without name"
  19
+    assert !p.save, "should not have saved an invalid record"
  20
+    assert_equal ["can't be blank"], p.errors[:name], "should have an error on name"
  21
+
  22
+    p.name = "something"
  23
+
  24
+    assert p.save, "should have saved after fixing the validation, but had: #{p.errors.inspect}"
  25
+  end
  26
+
  27
+  def test_validate_callback
  28
+    # we have a callback ensuring the description is longer thn three letters
  29
+    p = new_project(:description => 'a')
  30
+    assert !p.valid?, "should not be a valid record when it fails a validation callback"
  31
+    assert !p.save, "should not have saved an invalid record"
  32
+    assert_equal ["must be greater than three letters long"], p.errors[:description], "should be an error on description"
  33
+
  34
+    # should now allow this description
  35
+    p.description = 'abcd'
  36
+    assert p.save, "should have saved after fixing the validation, but had: #{p.errors.inspect}"
  37
+  end
  38
+
  39
+  protected
  40
+
  41
+  # quickie helper to create a new project with all the required
  42
+  # attributes.
  43
+  # Pass in any params you specifically want to override
  44
+  def new_project(opts = {})
  45
+    Project.new(VALID_PROJECT_HASH.merge(opts))
  46
+  end
  47
+
  48
+end
  49
+

0 notes on commit c2f90d6

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