vet 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (6) hide show
  1. data/LICENSE.text +19 -0
  2. data/README.markdown +209 -0
  3. data/lib/vet.rb +184 -0
  4. data/test/vet_test.rb +331 -0
  5. data/vet.gemspec +18 -0
  6. metadata +86 -0
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2010 Grant Heaslip
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
@@ -0,0 +1,209 @@
1
+ # Vet
2
+
3
+ Vet is a simple, lightweight, and ORM/framework-agnostic validation library that validates changes individually rather than atomically.
4
+
5
+ Instead of running validations altogether, like ActiveRecord or DataMapper, it checks the validity of values before they are applied to your object, keeping the in-memory instance clean. This allows valid changes to be accepted and safely written to a data store even when others are invalid, rather than rejecting every change because one didn't validate. It should work with any attribute that has a getter and setter method, and shouldn't interfere with existing validation libraries.
6
+
7
+ ## Installation
8
+
9
+ Install the gem from rubygems.org:
10
+
11
+ gem update --system
12
+ gem install vet
13
+
14
+ ## Basic usage
15
+
16
+ Simply add the line "include Vet" to any model. Here's an example:
17
+
18
+ class Mario
19
+ include Vet
20
+
21
+ attr_accessor :lives
22
+ attr_accessor :status
23
+ attr_accessor :cards
24
+ attr_accessor :items
25
+ end
26
+
27
+ From now on, whenever you want to modify an attribute of an instance of that class, do the following:
28
+
29
+ @instance.vet(attribute_name, new_value, *tests)
30
+
31
+ Here's an example:
32
+
33
+ @mario.vet(:lives, 2, :is_an_integer)
34
+
35
+ When a test needs to accept a parameter, you send the test as an array in the form [test_name, parameter]:
36
+
37
+ @mario.vet(:lives, 2, :is_an_integer, [:is_in_range, 0..99])
38
+
39
+ If the new value passes the test and is modified, the name of the attribute is added to the object's vet\_modified\_attributes array:
40
+
41
+ @mario.vet(:lives, 2, [:is_instance_of_class, Fixnum], [:is_in_range, 0..99])
42
+ puts @mario.vet_modified_attributes # => [:lives]
43
+
44
+ On the other hand, if the test fails, the error message the test returns will be added to object's vet\_errors hash, in an array filed under the name of the attribute:
45
+
46
+ @mario.vet(:lives, 100, [:is_instance_of_class, Fixnum], [:is_in_range, 0..99])
47
+ puts @mario.vet_errors # => {:lives => ["Lives must be between 0 and 99."]}
48
+
49
+ Lastly, if the new value passes the test but is not modified because it is identical to the old value, both the vet\_modified\_attributes array and the vet\_errors hash will be empty:
50
+
51
+ @mario.lives = 3
52
+ @mario.vet(:lives, 3, [:is_instance_of_class, Fixnum], [:is_in_range, 0..99])
53
+ puts @mario.vet_errors # => {}
54
+ puts @mario.vet_modified_attributes # => []
55
+
56
+ ## Configuration
57
+
58
+ ### Defining default tests
59
+
60
+ Obviously, typing out every test you want to run against an attribute every time it is modified would be repetitive, ugly, and error-prone. Instead, you can define a list of tests to always run against an attribute in the model. Here's an example:
61
+
62
+ class Mario
63
+ include Vet
64
+
65
+ attr_accessor :lives
66
+ attr_accessor :status
67
+ attr_accessor :cards
68
+ attr_accessor :items
69
+
70
+ def vet_attribute_tests
71
+ {
72
+ :lives =>
73
+ [
74
+ [:is_instance_of_class, Fixnum],
75
+ [:is_in_range, 0..99]
76
+ ],
77
+ :status =>
78
+ [
79
+ [:is_instance_of_class, Symbol],
80
+ ],
81
+ :cards =>
82
+ [
83
+ [:is_instance_of_class, Array],
84
+ [:has_length_in_range, 0..3],
85
+ [:only_contains_specified_objects, [:mushroom, :flower, :star]]
86
+ ],
87
+ :items =>
88
+ [
89
+ [:is_instance_of_class, Array],
90
+ [:has_length_in_range, 0..100]
91
+ ]
92
+ }
93
+ end
94
+ end
95
+
96
+ Vet will merge the tests passed through the standard vet method call with the ones specified in the model--duplicate tests aren't a problem. There are some tests, like is\_identical\_to\_confirmation, that you will still need to call in-controller because they require dynamic parameters.
97
+
98
+ ### Custom attribute names
99
+
100
+ By default, Vet will try to choose appropriate attribute names for use in error messages by replacing dashes and underscores with spaces and capitalizing them appropriately for the sentence. That said, it isn't perfect, and there will be times where your internal attribute names are different than the public ones. Vet lets you specify custom attribute names that will override the generated ones:
101
+
102
+ class Mario
103
+ include Vet
104
+
105
+ attr_accessor :lives
106
+ attr_accessor :status
107
+ attr_accessor :cards
108
+ attr_accessor :items
109
+
110
+ def vet_attribute_names
111
+ {
112
+ :lives => "Number of lives",
113
+ :status => "Status",
114
+ :cards => "Card collection",
115
+ :items => "Item collection"
116
+ }
117
+ end
118
+
119
+ def vet_attribute_tests
120
+ {
121
+ :lives =>
122
+ [
123
+ [:is_instance_of_class, Fixnum],
124
+ [:is_in_range, 0..99]
125
+ ],
126
+ :status =>
127
+ [
128
+ [:is_instance_of_class, Symbol],
129
+ ],
130
+ :cards =>
131
+ [
132
+ [:is_instance_of_class, Array],
133
+ [:has_length_in_range, 0..3],
134
+ [:only_contains_specified_objects, [:mushroom, :flower, :star]]
135
+ ],
136
+ :items =>
137
+ [
138
+ [:is_instance_of_class, Array],
139
+ [:has_length_in_range, 8..100]
140
+ ]
141
+ }
142
+ end
143
+ end
144
+
145
+ @mario = Mario.new()
146
+ @mario.vet(:lives, 100, [:is_instance_of_class, Fixnum], [:is_in_range, 0..99])
147
+ puts @mario.vet_errors # => {:lives => ["Number of lives must be between 0 and 99."]}
148
+ # RATHER THAN: => {:lives => ["Lives must be between 0 and 99."]}
149
+
150
+ ## Defining tests
151
+
152
+ Vet includes a bunch of useful built-in tests that you can check out by looking at the source, but it intentionally doesn't include ORM-specific tests, and there will be other tests that you will need that aren't included. Test definitions are very simple:
153
+
154
+ def is_not_empty(attribute, new_value)
155
+ unless new_value.empty? == false
156
+ add_vet_error(attribute, "must not be empty.") # => false
157
+ end
158
+ end
159
+
160
+ A test can take 2 or 3 parameters, depending on whether or not it needs to accept a value to test against. For example, here's another test from the source:
161
+
162
+ def is_equal_to_value(attribute, new_value, good_value)
163
+ unless new_value == good_value
164
+ add_vet_error(attribute, "must be #{good_value}.") # => false
165
+ end
166
+ end
167
+
168
+ If the test fails, it should return add\_vet\_error(attribute, "must be such and such."), which returns false. If the test passes, it should return anything except false, including nil (there's no need to write "...else return true end").
169
+
170
+ If you *really* want to be explicit, you could write that last test as follows:
171
+
172
+ def is_equal_to_value(attribute, new_value, good_value)
173
+ if new_value == good_value
174
+ return true
175
+ else
176
+ add_vet_error(attribute, "must be #{good_value}.") # => false
177
+ return false
178
+ end
179
+ end
180
+
181
+ Most of this is completely unnecessary--the add\_vet\_error function automatically returns false, and a nil return (i.e. which would be the case if the shorter version of the test passed), is interpreted as a pass since it isn't equal to false, so there's no need to return true.
182
+
183
+ ### Controlling add\_vet\_error behaviour
184
+
185
+ add\_vet\_error will generate errors of the form "#{attribute} must not be empty", and will try to make sure the capitalization jives. There are options for when you want to override this behaviour:
186
+
187
+ **{ATTRIBUTE\_NAME}**
188
+
189
+ If you put "{ATTRIBUTE\_NAME}" the body of an error, the attribute name will be put there as opposed to at the beginning of it:
190
+
191
+ # Generates errors like "Mario's lives must be between 0 and 99."
192
+
193
+ def must_be_between_0_and_99(attribute, new_value)
194
+ unless (0..99).include? new_value
195
+ add_vet_error(attribute, "Mario's {ATTRIBUTE_NAME} must between 0 and 99.")
196
+ end
197
+ end
198
+
199
+ **:exclude\_attribute\_name**
200
+
201
+ If you add the parameter ":exclude\_attribute\_name" to the end of an add\_vet\_error call, it won't try to use the attribute name, generated or specified, in the error message, and will spit out the specified error message text verbatim:
202
+
203
+ # Generates errors like "Mario must have between 0 and 99 lives."
204
+
205
+ def must_have_between_0_and_99_lives(attribute, new_value)
206
+ unless (0..99).include? new_value
207
+ add_vet_error(attribute, "Mario must have between 0 and 99 lives.", :exclude_attribute_name)
208
+ end
209
+ end
@@ -0,0 +1,184 @@
1
+ # encoding: utf-8
2
+ module Vet
3
+ module_function
4
+ public
5
+
6
+ attr_accessor :vet_errors # Hash {:attribute1=>["Error"], :attribute3=>["Error", "Error 2"]}
7
+ attr_accessor :vet_modified_attributes # Array [:attribute2, :attribute4]
8
+
9
+ # -----------------
10
+ # ----- TESTS -----
11
+ # -----------------
12
+
13
+ # ----- EQUALITY TESTS -----
14
+
15
+ def is_not_empty(attribute, new_value)
16
+ unless new_value.empty? == false
17
+ add_vet_error(attribute, "must not be empty.")
18
+ end
19
+ end
20
+
21
+ def is_equal_to_value(attribute, new_value, good_value)
22
+ unless new_value == good_value
23
+ add_vet_error(attribute, "must be #{good_value}.")
24
+ end
25
+ end
26
+
27
+ def is_identical_to_confirmation(attribute, new_value, confirmation)
28
+ unless new_value == confirmation
29
+ add_vet_error(attribute, "must match confirmation.")
30
+ end
31
+ end
32
+
33
+ def is_not_equal_to_value(attribute, new_value, bad_value)
34
+ unless new_value != bad_value
35
+ add_vet_error(attribute, "must not be #{bad_value}.")
36
+ end
37
+ end
38
+
39
+ # ----- PROPERTY TESTS -----
40
+
41
+ def is_instance_of_class(attribute, new_value, class_name)
42
+ unless new_value.class == class_name
43
+ add_vet_error(attribute, "must be a #{class_name}.")
44
+ end
45
+ end
46
+
47
+ def is_an_integer(attribute, new_value)
48
+ unless new_value.to_s.match(%r{\A[\d]+\z})
49
+ add_vet_error(attribute, "must be a whole number.")
50
+ end
51
+ end
52
+
53
+ # ----- RANGE TESTS -----
54
+
55
+ def is_in_range(attribute, new_value, range)
56
+ unless range.include? new_value
57
+ add_vet_error(attribute, "must be between #{range.first} and #{range.last}.")
58
+ end
59
+ end
60
+
61
+ def has_length_in_range(attribute, new_value, range)
62
+ unless range.include? new_value.length
63
+ add_vet_error(attribute, "must be between #{range.first} and #{range.last} characters long.")
64
+ end
65
+ end
66
+
67
+ def only_contains_specified_objects(attribute, new_value, objects)
68
+ is_clean = true
69
+ new_value.each do |object|
70
+ if objects.include?(object) == false
71
+ is_clean = false
72
+ end
73
+ end
74
+
75
+ if is_clean
76
+ return true
77
+ else
78
+ add_vet_error(attribute, "is not an acceptable value.")
79
+ end
80
+ end
81
+
82
+ # ----- REGEX MATCH TESTS -----
83
+
84
+ def is_email_address(attribute, new_value)
85
+ unless new_value.match(%r{\A([\S]+@[\S]+[\.][\S]{2,})\z})
86
+ add_vet_error(attribute, "must be a valid email address.")
87
+ end
88
+ end
89
+
90
+ def is_uri_friendly(attribute, new_value)
91
+ unless new_value.match(%r{\A[a-z\d]{1}[a-z\d_-]*[a-z\d]{1}\z|\A[a-z\d]{1}\z})
92
+ add_vet_error(attribute, "must contain only lower-case alphanumeric characters, underscores, and dashes.")
93
+ end
94
+ end
95
+
96
+ # ---------------------------
97
+ # ----- SUPPORT METHODS -----
98
+ # ---------------------------
99
+
100
+ # Run tests, and modify the attribute if all tests pass
101
+ def vet(attribute, new_value, *specified_tests)
102
+ # Make sure attribute is a symbol
103
+ attribute = attribute.to_sym
104
+
105
+ # Initialize variables if necessary
106
+ if self.vet_errors == nil
107
+ self.vet_errors = {}
108
+ end
109
+ if self.vet_modified_attributes == nil
110
+ self.vet_modified_attributes = []
111
+ end
112
+
113
+ # Only bother running through tests if the new value is different than the old one
114
+ if self.send(attribute) != new_value
115
+ passed_tests = true
116
+
117
+ # Combine tests defined in model with tests specificed on method call if applicable
118
+ if defined? self.vet_attribute_tests[attribute]
119
+ tests = specified_tests | self.vet_attribute_tests[attribute]
120
+ else
121
+ tests = specified_tests
122
+ end
123
+
124
+ tests.each do |test|
125
+ # If test call is an array (i.e. has parameters), use parameters from it
126
+ if test.class == Array
127
+ passed_tests = false if self.send(test[0], attribute, new_value, test[1]) == false
128
+ else
129
+ passed_tests = false if self.send(test, attribute, new_value) == false
130
+ end
131
+ end
132
+
133
+ # Only modify the attribute if no tests fail
134
+ if passed_tests
135
+ self.modify_attribute(attribute, new_value)
136
+ self.vet_modified_attributes << attribute
137
+ end
138
+
139
+ # Return true if attribute was changed, false if it wasn't
140
+ return passed_tests
141
+ else
142
+ # Return false if new value was identical to old one
143
+ return false
144
+ end
145
+ end
146
+
147
+ # Simple wrapper for attribute setter method
148
+ def modify_attribute(attribute, new_value)
149
+ self.send("#{attribute}=", new_value)
150
+ end
151
+
152
+ # Add error to appropriate location in vet_errors hash
153
+ def add_vet_error(attribute, message, *opts)
154
+ # If message includes {ATTRIBUTE_NAME} keyword, replace it with attribute name.
155
+ # Otherwise, generate message by prepending the attribute name to message
156
+ if opts == [:exclude_attribute_name]
157
+ error_message = message
158
+ elsif message.gsub!("{ATTRIBUTE_NAME}", attribute_name(attribute).downcase)
159
+ error_message = message
160
+ else
161
+ error_message = "#{attribute_name(attribute).capitalize} #{message}"
162
+ end
163
+
164
+ # If first error message, encapsulate in an array, otherwise append to existing array
165
+ if self.vet_errors[attribute] == nil
166
+ self.vet_errors[attribute] = [error_message]
167
+ else
168
+ self.vet_errors[attribute] << error_message
169
+ end
170
+
171
+ # Return false so test definitions don't all need to themselves (if you're adding an error the test didn't pass)
172
+ return false
173
+ end
174
+
175
+ # Return user-defined attribute name if available, otherwise return code attribute name with underscores and dashes removed
176
+ def attribute_name(attribute)
177
+ if defined? self.vet_attribute_names[attribute]
178
+ return self.vet_attribute_names[attribute]
179
+ else
180
+ return attribute.to_s.gsub(/[_-]/, " ")
181
+ end
182
+ end
183
+
184
+ end
@@ -0,0 +1,331 @@
1
+ # encoding: utf-8
2
+ require "rubygems"
3
+ require "shoulda"
4
+ require File.expand_path(File.dirname(__FILE__) + '/../lib/vet')
5
+
6
+ class Mario # From Mario 3 (obviously!)
7
+ include Vet
8
+
9
+ attr_accessor :lives # Fixnum
10
+ attr_accessor :status # Symbol (:normal, :super, :racoon, :fire, etc.)
11
+ attr_accessor :cards # Array of symbols (:mushroom, :flower, or :star), maximum length 3
12
+ attr_accessor :items # Array of symbols (:super_mushroom, :fire_flower, :super_leaf, etc.), maximum length 6
13
+
14
+ def vet_attribute_names
15
+ {
16
+ :lives => "Number of lives",
17
+ :status => "Status",
18
+ :cards => "Card collection",
19
+ :items => "Item collection"
20
+ }
21
+ end
22
+
23
+ def vet_attribute_tests
24
+ {
25
+ :lives =>
26
+ [
27
+ [:is_instance_of_class, Fixnum],
28
+ [:is_in_range, 0..99]
29
+ ],
30
+ :status =>
31
+ [
32
+ [:is_instance_of_class, Symbol],
33
+ ],
34
+ :cards =>
35
+ [
36
+ [:is_instance_of_class, Array],
37
+ [:has_length_in_range, 0..3],
38
+ [:only_contains_specified_objects, [:mushroom, :flower, :star]]
39
+ ],
40
+ :items =>
41
+ [
42
+ [:is_instance_of_class, Array],
43
+ [:has_length_in_range, 0..100]
44
+ ]
45
+ }
46
+ end
47
+
48
+ def initialize
49
+ self.lives = 3
50
+ self.status = :normal
51
+ self.cards = []
52
+ self.items = []
53
+ end
54
+ end
55
+
56
+ class MarioNoConfiguration # From Mario 3 (obviously!)
57
+ include Vet
58
+
59
+ attr_accessor :lives # Fixnum
60
+ attr_accessor :status # Symbol (:normal, :super, :racoon, :fire, etc.)
61
+ attr_accessor :cards # Array of symbols (:mushroom, :flower, or :star), maximum length 3
62
+ attr_accessor :items # Array of symbols (:super_mushroom, :fire_flower, :super_leaf, etc.), maximum length 6
63
+ attr_accessor :note # Extra attribute for miscellaneous tests (sorry, not creative enough to work email addresses into Mario 3 canon)
64
+
65
+ def must_be_between_0_and_99(attribute, new_value)
66
+ unless (0..99).include? new_value
67
+ add_vet_error(attribute, "Mario's attribute {ATTRIBUTE_NAME} must between 0 and 99.")
68
+ end
69
+ end
70
+
71
+ def must_have_between_0_and_99_lives(attribute, new_value)
72
+ unless (0..99).include? new_value
73
+ add_vet_error(attribute, "Mario must have between 0 and 99 lives.", :exclude_attribute_name)
74
+ end
75
+ end
76
+
77
+ def initialize
78
+ self.lives = 3
79
+ self.status = :normal
80
+ self.cards = []
81
+ self.items = []
82
+ self.note = ""
83
+ end
84
+ end
85
+
86
+ class FunctionalityTest < Test::Unit::TestCase
87
+ context "A class that includes vet" do
88
+ setup do
89
+ @mario = Mario.new
90
+ @mario_no_configuration = MarioNoConfiguration.new
91
+ end
92
+ # These tests are redundant to handle both Ruby 1.8 and 1.9
93
+ should "have method vet" do
94
+ assert (@mario.public_methods.include? "vet") || (@mario.public_methods.include? :vet)
95
+ end
96
+ should "have method vet_errors" do
97
+ assert (@mario.public_methods.include? "vet_errors") || (@mario.public_methods.include? :vet_errors)
98
+ end
99
+ should "have method vet_modified_attributes" do
100
+ assert (@mario.public_methods.include? "vet_modified_attributes") || (@mario.public_methods.include? :vet_modified_attributes)
101
+ end
102
+
103
+ should "run tests in vet_attribute_tests" do
104
+ assert @mario.vet(:lives, 150) == false
105
+ end
106
+ should "use attribute names specified in vet_attribute_names" do
107
+ @mario.vet(:lives, 150)
108
+ assert @mario.vet_errors[:lives] == ["Number of lives must be between 0 and 99."]
109
+ end
110
+ should "generate attribute name when one isn't specified in vet_attribute_names" do
111
+ @mario_no_configuration.vet(:lives, 150, [:is_in_range, 0..99])
112
+ assert @mario_no_configuration.vet_errors[:lives] == ["Lives must be between 0 and 99."]
113
+ end
114
+ should "replace {ATTRIBUTE_NAME} with attribute name in error message" do
115
+ @mario_no_configuration.vet(:lives, 150, :must_be_between_0_and_99)
116
+ assert @mario_no_configuration.vet_errors[:lives] == ["Mario's attribute lives must between 0 and 99."]
117
+ end
118
+ should "not include attribute name if :exclude_attribute_name passed to add_error_message in test definition" do
119
+ @mario_no_configuration.vet(:lives, 150, :must_have_between_0_and_99_lives)
120
+ assert @mario_no_configuration.vet_errors[:lives] == ["Mario must have between 0 and 99 lives."]
121
+ end
122
+
123
+ should "not modify attribute if new value is the same as the old one" do
124
+ assert @mario.vet(:lives, 3) == false
125
+ end
126
+ should "not add any errors if new value is the same as the old one" do
127
+ @mario.vet(:lives, 3, [:is_not_equal_to_value, 3])
128
+ assert @mario.vet_errors[:lives] == nil
129
+ end
130
+ should "not modify attribute if test fails" do
131
+ @mario.vet(:lives, 150)
132
+ assert @mario.lives != 150
133
+ end
134
+ should "run tests specified in vet call and tests in vet_attribute_tests at the same time" do
135
+ @mario.vet(:lives, 150, [:is_not_equal_to_value, 150])
136
+ assert @mario.vet_errors[:lives].length == 2
137
+ end
138
+ should "collect errors from multiple attributes" do
139
+ @mario.vet(:lives, 150)
140
+ @mario.vet(:status, "Crazy!")
141
+ assert @mario.vet_errors[:lives] != nil && @mario.vet_errors[:lives].length > 0
142
+ assert @mario.vet_errors[:status] != nil && @mario.vet_errors[:status].length > 0
143
+ end
144
+
145
+ should "modify attribute if tests pass" do
146
+ @mario.vet(:lives, 4)
147
+ assert @mario.lives == 4
148
+ end
149
+ should "add attribute name to vet_modified_attributes array when they are changed" do
150
+ @mario.vet(:lives, 4)
151
+ assert @mario.vet_modified_attributes.include? :lives
152
+ end
153
+ should "add multiple attribute names to vet_modified_attributes array when they are changed" do
154
+ @mario.vet(:lives, 4)
155
+ @mario.vet(:status, :racoon)
156
+ assert @mario.vet_modified_attributes.include?(:lives)
157
+ assert @mario.vet_modified_attributes.include?(:status)
158
+ end
159
+ end
160
+ end
161
+
162
+ class TestTests < Test::Unit::TestCase
163
+ context "Test" do
164
+ setup do
165
+ @mario = MarioNoConfiguration.new
166
+ end
167
+
168
+ context "is_not_empty" do
169
+ should "pass if value is not empty" do
170
+ assert @mario.vet(:cards, [:mushroom], :is_not_empty) != false
171
+ end
172
+
173
+ should "fail if value is empty" do
174
+ assert @mario.vet(:cards, [], :is_not_empty) == false
175
+ end
176
+ end
177
+
178
+ context "is_equal_to_value" do
179
+ should "pass if value matches parameter" do
180
+ assert @mario.vet(:lives, 2, [:is_equal_to_value, 2]) != false
181
+ end
182
+
183
+ should "fail if value doesn't match parameter" do
184
+ assert @mario.vet(:lives, 2, [:is_equal_to_value, 4]) == false
185
+ end
186
+ end
187
+
188
+ context "is_identical_to_confirmation" do
189
+ should "pass if value matches confirmation" do
190
+ confirmation = 2
191
+ assert @mario.vet(:lives, 2, [:is_identical_to_confirmation, confirmation]) != false
192
+ end
193
+ should "fail if value doesn't match confirmation" do
194
+ confirmation = 4
195
+ assert @mario.vet(:lives, 2, [:is_identical_to_confirmation, confirmation]) == false
196
+ end
197
+ end
198
+
199
+ context "is_not_equal_to_value" do
200
+ should "pass if value doesn't match parameter" do
201
+ assert @mario.vet(:lives, 2, [:is_not_equal_to_value, 4]) != false
202
+ end
203
+
204
+ should "fail if value matches parameter" do
205
+ assert @mario.vet(:lives, 2, [:is_not_equal_to_value, 2]) == false
206
+ end
207
+ end
208
+
209
+ context "is_instance_of_class" do
210
+ should "pass if value is of specified class" do
211
+ assert @mario.vet(:lives, 2, [:is_instance_of_class, Fixnum]) != false
212
+ end
213
+
214
+ should "fail if value isn't of specified class" do
215
+ assert @mario.vet(:lives, "Walrus", [:is_instance_of_class, Fixnum]) == false
216
+ end
217
+ end
218
+
219
+ context "is_an_integer" do
220
+ should "pass if value is an integer" do
221
+ assert @mario.vet(:lives, 2, :is_an_integer) != false
222
+ end
223
+
224
+ should "fail if value is not an integer" do
225
+ assert @mario.vet(:lives, "Donk", :is_an_integer) == false
226
+ end
227
+ end
228
+
229
+ context "is_in_range" do
230
+ should "pass if value is in range" do
231
+ assert @mario.vet(:lives, 99, [:is_in_range, 0..99]) != false
232
+ end
233
+
234
+ should "fail if value isn't in range" do
235
+ assert @mario.vet(:lives, 100, [:is_in_range, 0..99]) == false
236
+ end
237
+ end
238
+
239
+ context "has_length_in_range" do
240
+ should "pass if value has length in range" do
241
+ assert @mario.vet(:cards, [:mushroom, :flower, :star], [:has_length_in_range, 0..3]) != false
242
+ end
243
+
244
+ should "fail if value doesn't have length in range" do
245
+ assert @mario.vet(:cards, [:mushroom, :flower, :star, :star], [:has_length_in_range, 0..3]) == false
246
+ end
247
+ end
248
+
249
+ context "only_contains_specified_objects" do
250
+ should "pass if value only contains specified objects" do
251
+ assert @mario.vet(:cards, [:mushroom, :flower], [:only_contains_specified_objects, [:mushroom, :flower, :star]]) != false
252
+ end
253
+
254
+ should "fail if value contains non-specified objects" do
255
+ assert @mario.vet(:cards, [:mushroom, :flower, :narwhal], [:only_contains_specified_objects, [:mushroom, :flower, :star]]) == false
256
+ end
257
+ end
258
+
259
+ context "is_email_address" do
260
+ should "pass if value is an email address" do
261
+ assert @mario.vet(:note, "mario@nintendo.com", :is_email_address) != false
262
+ end
263
+ should "pass if value is an odd email address" do
264
+ assert @mario.vet(:note, "m@n.co", :is_email_address) != false
265
+ end
266
+ should "pass if value is an email address with more dots in it" do
267
+ assert @mario.vet(:note, "mario.mario@nintendo.co.jp", :is_email_address) != false
268
+ end
269
+ # This is debatable, but I don't want to get into policing people's email addresses, and false-positives are a worry
270
+ should "pass if value is an email address with weird characters in it" do
271
+ assert @mario.vet(:note, "mar%21a!o@nin%33do.com", :is_email_address) != false
272
+ end
273
+
274
+ should "fail if value doesn't have @ sign" do
275
+ assert @mario.vet(:note, "mario+nintendo.com", :is_email_address) == false
276
+ end
277
+ should "fail if value doesn't have anything after @ sign" do
278
+ assert @mario.vet(:note, "mario@", :is_email_address) == false
279
+ end
280
+ should "fail if value doesn't have anything before @ sign" do
281
+ assert @mario.vet(:note, "@nintendo.com", :is_email_address) == false
282
+ end
283
+ should "fail if value is empty" do
284
+ assert @mario.vet(:note, "", :is_email_address) == false
285
+ end
286
+ end
287
+
288
+ context "is_uri_friendly" do
289
+ should "pass if value is a nice uri" do
290
+ assert @mario.vet(:note, "mario_mario", :is_uri_friendly) != false
291
+ end
292
+ should "pass if value is a nice uri with a dash" do
293
+ assert @mario.vet(:note, "mario-mario", :is_uri_friendly) != false
294
+ end
295
+ should "pass if value is a nice uri with a dash and underscore" do
296
+ assert @mario.vet(:note, "mario-mario_nintendo", :is_uri_friendly) != false
297
+ end
298
+ should "pass if value has no punctuation" do
299
+ assert @mario.vet(:note, "mario", :is_uri_friendly) != false
300
+ end
301
+ should "pass if value has numbers in it" do
302
+ assert @mario.vet(:note, "mario64", :is_uri_friendly) != false
303
+ end
304
+ should "pass if value is only numbers" do
305
+ assert @mario.vet(:note, "64", :is_uri_friendly) != false
306
+ end
307
+ should "pass if value is one character long" do
308
+ assert @mario.vet(:note, "m", :is_uri_friendly) != false
309
+ end
310
+
311
+ should "fail if value contains invalid characters" do
312
+ assert @mario.vet(:note, "mario^64", :is_uri_friendly) == false
313
+ end
314
+ should "fail if value begins with an underscore" do
315
+ assert @mario.vet(:note, "_mario", :is_uri_friendly) == false
316
+ end
317
+ should "fail if value ends with a dash" do
318
+ assert @mario.vet(:note, "mario-", :is_uri_friendly) == false
319
+ end
320
+ should "fail if value is capitalized" do
321
+ assert @mario.vet(:note, "Mario", :is_uri_friendly) == false
322
+ end
323
+ should "fail if value contains upper-case character" do
324
+ assert @mario.vet(:note, "MarioMario", :is_uri_friendly) == false
325
+ end
326
+ should "fail if value contains space" do
327
+ assert @mario.vet(:note, "mario mario", :is_uri_friendly) == false
328
+ end
329
+ end
330
+ end
331
+ end
@@ -0,0 +1,18 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = "vet"
3
+ s.homepage = "http://github.com/grantheaslip/vet"
4
+ s.author = "Grant Heaslip"
5
+ s.email = "me@grantheaslip.com"
6
+ s.summary = "Validate changes individually, not atomically. ORM/framework-agnostic."
7
+ s.require_path = 'lib'
8
+ s.version = "0.1.1"
9
+ s.files = %w{
10
+ LICENSE.text
11
+ README.markdown
12
+ vet.gemspec
13
+ lib/vet.rb
14
+ test/vet_test.rb
15
+ }
16
+
17
+ s.add_development_dependency "shoulda", ">= 2.10.3"
18
+ end
metadata ADDED
@@ -0,0 +1,86 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: vet
3
+ version: !ruby/object:Gem::Version
4
+ hash: 25
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 1
9
+ - 1
10
+ version: 0.1.1
11
+ platform: ruby
12
+ authors:
13
+ - Grant Heaslip
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2010-06-04 00:00:00 -04:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: shoulda
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 33
30
+ segments:
31
+ - 2
32
+ - 10
33
+ - 3
34
+ version: 2.10.3
35
+ type: :development
36
+ version_requirements: *id001
37
+ description:
38
+ email: me@grantheaslip.com
39
+ executables: []
40
+
41
+ extensions: []
42
+
43
+ extra_rdoc_files: []
44
+
45
+ files:
46
+ - LICENSE.text
47
+ - README.markdown
48
+ - vet.gemspec
49
+ - lib/vet.rb
50
+ - test/vet_test.rb
51
+ has_rdoc: true
52
+ homepage: http://github.com/grantheaslip/vet
53
+ licenses: []
54
+
55
+ post_install_message:
56
+ rdoc_options: []
57
+
58
+ require_paths:
59
+ - lib
60
+ required_ruby_version: !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ hash: 3
66
+ segments:
67
+ - 0
68
+ version: "0"
69
+ required_rubygems_version: !ruby/object:Gem::Requirement
70
+ none: false
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ hash: 3
75
+ segments:
76
+ - 0
77
+ version: "0"
78
+ requirements: []
79
+
80
+ rubyforge_project:
81
+ rubygems_version: 1.3.7
82
+ signing_key:
83
+ specification_version: 3
84
+ summary: Validate changes individually, not atomically. ORM/framework-agnostic.
85
+ test_files: []
86
+