vet 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE.text +19 -0
- data/README.markdown +209 -0
- data/lib/vet.rb +184 -0
- data/test/vet_test.rb +331 -0
- data/vet.gemspec +18 -0
- metadata +86 -0
data/LICENSE.text
ADDED
@@ -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.
|
data/README.markdown
ADDED
@@ -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
|
data/lib/vet.rb
ADDED
@@ -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
|
data/test/vet_test.rb
ADDED
@@ -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
|
data/vet.gemspec
ADDED
@@ -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
|
+
|