public
Description: object-oriented activerecord validations and machine/human formatting
Clone URL: git://github.com/cainlevy/semantic-attributes.git
overhaul of test suite. now uses mocha and an sqlite3 database.
fixed performance/recursion problems related to association validation, by 
moving the recursion control to the ActiveRecord::Base#valid? method 
itself. huzzah!


git-svn-id: http://semanticattributes.googlecode.com/svn/trunk@47 
253fb8bd-cf2e-0410-89f4-458c444950b4
cainlevy (author)
Tue Mar 18 18:30:11 -0700 2008
commit  555c2f8b647ea67c6c605f4d960a21ac0325d60b
tree    055b1af8932b5770855c516d7d98a7de2abbba5b
parent  fab2d0d80b72185e5c594080d5012c6b21254a72
...
8
9
10
11
 
12
13
14
...
8
9
10
 
11
12
13
14
0
@@ -8,7 +8,7 @@ task :default => :test
0
 desc 'Test the SemanticAttributes plugin.'
0
 Rake::TestTask.new(:test) do |t|
0
   t.libs << 'lib'
0
- t.pattern = 'test/**/*_test.rb'
0
+ t.pattern = 'test/unit/**/*_test.rb'
0
   t.verbose = true
0
 end
0
 
...
 
 
1
2
3
4
5
 
6
7
8
...
13
14
15
 
16
 
17
18
19
...
1
2
3
4
5
6
7
8
9
10
11
...
16
17
18
19
20
21
22
23
24
0
@@ -1,8 +1,11 @@
0
+# TODO: organize the library code to take advantage of ActiveSupport's auto-loading
0
+
0
 require 'predicates'
0
 require 'semantic_attribute'
0
 require 'semantic_attributes'
0
 require 'active_record_predicates'
0
 require 'attribute_formats'
0
+require 'validation_recursion_control'
0
 
0
 module Predicates; end
0
 predicates_directory = "#{File.dirname __FILE__}/lib/predicates"
0
@@ -13,7 +16,9 @@ end
0
 
0
 ActiveRecord::Base.send(:include, ActiveRecord::Predicates)
0
 ActiveRecord::Base.send(:include, ActiveRecord::AttributeFormats)
0
+ActiveRecord::Base.send(:include, ActiveRecord::ValidationRecursion)
0
 
0
+# localization mock
0
 ActiveRecord::Base.class_eval do
0
   unless respond_to? :_
0
     def _(s); s; end
...
18
19
20
21
22
23
24
25
26
27
28
29
30
...
18
19
20
 
21
22
23
24
25
 
26
27
28
0
@@ -18,13 +18,11 @@ module ActiveRecord #:nodoc:
0
 
0
       # this method is private
0
       base.class_eval do
0
- unless private_instance_methods.include? 'write_attribute_without_formats' # was having stack problems when running tests
0
         def write_attribute_with_formats(attr, value)
0
           value = self.class.machinize(attr, value) if semantic_attributes and semantic_attributes.include? attr
0
           write_attribute_without_formats attr, value
0
         end
0
         alias_method_chain :write_attribute, :formats
0
- end
0
       end
0
     end
0
 
...
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
 
 
 
 
 
 
 
 
 
74
75
76
...
80
81
82
83
84
85
86
87
88
89
 
...
19
20
21
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
23
 
 
 
 
 
 
 
 
 
 
24
 
 
25
26
27
28
29
30
31
32
33
34
35
36
...
40
41
42
 
 
 
 
 
 
43
44
0
@@ -19,58 +19,18 @@ class Predicates::Association < Predicates::Base
0
   end
0
 
0
   def validate(value, record)
0
- # assume the best
0
- valid = true
0
-
0
- # This extra check is in case we recursed back by some other mechanism.
0
- # For example, ActiveRecord will automatically validate *new* records in a plural
0
- # association (e.g. has_many). So, suppose the following setup:
0
- #
0
- # Book
0
- # belongs_to :author
0
- # author_is_association :or_empty => false
0
- # end
0
- #
0
- # Author
0
- # has_many :books
0
- # end
0
- #
0
- # a = Author.new
0
- # a.books.create :author => a
0
- # a.save
0
- #
0
- # The ":author => a" setting, while duplicating some of the functionality of
0
- # a.books.create, is done to satisfy the book's author requirement. Without it the
0
- # book would not validate at all. But with it, there's a chance of an infinite loop.
0
- #
0
- # So what happens? During `a.save`, ActiveRecord tries to validate the new book
0
- # (simply because it's a new record in a plural association collection), and the new
0
- # book then tries to validate the new author. Since ActiveRecord doesn't have any
0
- # recursion control for this kind of thing (understandable, since this only happens for
0
- # plural associations) it validates the new author by returning directly to the new book.
0
- # And voila, we're looping. But this next line stops it from going further, even if
0
- # it's a little late.
0
- #
0
- # Why bother with recursion control? Why not just tell the programmer to rework the
0
- # code? Because we can deal with it. Why impose constraints on the programmer if we
0
- # can easily deal with it?
0
- return valid if recursion_stack.include? record
0
-
0
     # we treat singular and plural the same
0
     associated = [value].flatten
0
- invalid_new_records = associated.select{|r| r.new_record?}.select do |new_record|
0
- # if we're not coming *from* the new_record
0
- unless recursion_stack.include? new_record
0
- # add *this* record to the recursion stack to make sure we don't come back
0
- recursion_stack << record
0
- v = new_record.valid?
0
- recursion_stack.delete(record)
0
- !v # return true if not valid
0
- end
0
- end
0
 
0
- # first, count how many records we expect to persist, then validate against min/max
0
- quantity = associated.length - invalid_new_records.length
0
+ # we need to check the validity of new records in order to calculate how many
0
+ # will save properly. this lets us validate against the min/max parameters.
0
+ invalid_new_records = associated.select{|r| r.new_record? and not r.valid?}
0
+ valid_new_records = associated - invalid_new_records
0
+
0
+ valid = true
0
+
0
+ # then validate against min/max
0
+ quantity = valid_new_records.length
0
     valid &&= (!min or quantity >= min)
0
     valid &&= (!max or quantity <= max)
0
 
0
@@ -80,9 +40,4 @@ class Predicates::Association < Predicates::Base
0
 
0
     valid
0
   end
0
-
0
- private
0
-
0
- cattr_accessor :recursion_stack
0
- @@recursion_stack = []
0
-end
0
\ No newline at end of file
0
+end
...
28
29
30
31
32
33
 
 
34
35
 
36
37
38
 
39
40
41
 
42
43
44
...
28
29
30
 
 
 
31
32
33
 
34
35
36
 
37
38
39
 
40
41
42
43
0
@@ -28,17 +28,16 @@ class Predicates::Unique < Predicates::Base
0
     [scope].flatten.each { |attribute| fields_values << [attribute, record.send(attribute)] }
0
 
0
     conditions_array = ['']
0
- fields_values.each do |pair|
0
- attribute, attribute_value = *pair
0
- sql = "#{record.class.table_name}.#{attribute}"
0
+ fields_values.each do |(attribute, attribute_value)|
0
+ field_sql = "#{record.class.table_name}.#{attribute}"
0
       unless self.case_sensitive
0
- sql = "LOWER(#{sql})"
0
+ field_sql = "LOWER(#{field_sql})"
0
         attribute_value.downcase! unless attribute_value.nil?
0
       end
0
- sql << ' ' << record.class.send(:attribute_condition, attribute_value)
0
+ field_sql << ' ' << record.class.send(:attribute_condition, attribute_value)
0
 
0
       conditions_array.first << ' AND ' unless conditions_array.first.empty?
0
- conditions_array.first << sql
0
+ conditions_array.first << field_sql
0
       conditions_array << attribute_value
0
     end
0
 
...
1
 
2
 
 
3
4
5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
7
8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
10
11
12
13
 
14
15
...
 
1
2
3
4
5
6
 
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
 
 
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
 
 
 
 
63
64
65
0
@@ -1,15 +1,65 @@
0
-require 'test/unit'
0
+ENV["RAILS_ENV"] = "test"
0
 
0
+# load the support libraries
0
+require 'test/unit'
0
 require 'rubygems'
0
 require 'active_record'
0
-require 'active_support'
0
+require 'active_record/fixtures'
0
+require 'mocha'
0
+
0
+# establish the database connection
0
+ActiveRecord::Base.configurations = YAML::load(IO.read(File.dirname(__FILE__) + '/db/database.yml'))
0
+ActiveRecord::Base.establish_connection('semantic_attributes_test')
0
+
0
+# capture the logging
0
+ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + "/test.log")
0
+
0
+# load the code-to-be-tested
0
+$LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib/'
0
+require File.dirname(__FILE__) + '/../init'
0
+
0
+# load the schema ... silently
0
+ActiveRecord::Migration.verbose = false
0
+load(File.dirname(__FILE__) + "/db/schema.rb")
0
+
0
+# load the ActiveRecord models
0
+require File.dirname(__FILE__) + '/db/models'
0
+
0
+# configure the TestCase settings
0
+class Test::Unit::TestCase
0
+ include PluginTestModels
0
+
0
+ self.use_transactional_fixtures = true
0
+ self.use_instantiated_fixtures = false
0
+ self.fixture_path = File.dirname(__FILE__) + '/fixtures/'
0
+
0
+ fixtures :all
0
+end
0
 
0
-$LOAD_PATH << File.dirname(__FILE__) + '/../lib/'
0
-require File.dirname(__FILE__) + '/../init.rb'
0
+class ActiveRecord::Base
0
+ # Aids the management of per-test semantics.
0
+ #
0
+ # Examples:
0
+ #
0
+ # User.stub_semantics_with(:email => :email)
0
+ # User.stub_semantics_with(:email => [:email, :unique])
0
+ # User.stub_semantics_with(:email => {:length => {:above => 5}})
0
+ # User.stub_semantics_with(:email => [:email, {:length => {:above => 5}}])
0
+ def self.stub_semantics_with(attr_predicates = {})
0
+ semantics = SemanticAttributes.new
0
+ attr_predicates.each do |attr, predicates|
0
+ [predicates].flatten.each do |predicate|
0
+ case predicate
0
+ when String, Symbol
0
+ semantics[attr].add(predicate)
0
+ when Hash
0
+ semantics[attr].add(predicate.keys.first, predicate.values.first)
0
+ else
0
+ raise '???'
0
+ end
0
+ end
0
+ end
0
 
0
-class FakeModel < ActiveRecord::Base
0
- abstract_class = true
0
- def self.columns
0
- []
0
+ self.stubs(:semantic_attributes).returns(semantics)
0
   end
0
 end

Comments

    No one has commented yet.