semantic_attributes 1.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +2 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +99 -0
- data/MIT-LICENSE +20 -0
- data/README +54 -0
- data/Rakefile +37 -0
- data/gist.rdoc +208 -0
- data/lib/active_record/validation_recursion_control.rb +33 -0
- data/lib/core_ext/class.rb +14 -0
- data/lib/predicates/aliased.rb +22 -0
- data/lib/predicates/association.rb +43 -0
- data/lib/predicates/base.rb +93 -0
- data/lib/predicates/blacklisted.rb +23 -0
- data/lib/predicates/domain.rb +31 -0
- data/lib/predicates/email.rb +42 -0
- data/lib/predicates/enumerated.rb +23 -0
- data/lib/predicates/hex_color.rb +24 -0
- data/lib/predicates/length.rb +71 -0
- data/lib/predicates/number.rb +104 -0
- data/lib/predicates/pattern.rb +22 -0
- data/lib/predicates/phone_number.rb +62 -0
- data/lib/predicates/required.rb +22 -0
- data/lib/predicates/same_as.rb +17 -0
- data/lib/predicates/size.rb +2 -0
- data/lib/predicates/time.rb +43 -0
- data/lib/predicates/unique.rb +71 -0
- data/lib/predicates/url.rb +62 -0
- data/lib/predicates/usa_state.rb +87 -0
- data/lib/predicates/usa_zip_code.rb +25 -0
- data/lib/predicates/whitelisted.rb +2 -0
- data/lib/predicates.rb +3 -0
- data/lib/semantic_attributes/attribute.rb +46 -0
- data/lib/semantic_attributes/attribute_formats.rb +67 -0
- data/lib/semantic_attributes/locale/en.yml +31 -0
- data/lib/semantic_attributes/predicates.rb +170 -0
- data/lib/semantic_attributes/set.rb +40 -0
- data/lib/semantic_attributes/version.rb +3 -0
- data/lib/semantic_attributes.rb +37 -0
- data/semantic_attributes.gemspec +29 -0
- data/test/db/database.yml +3 -0
- data/test/db/models.rb +38 -0
- data/test/db/schema.rb +33 -0
- data/test/fixtures/addresses.yml +15 -0
- data/test/fixtures/roles.yml +4 -0
- data/test/fixtures/roles_users.yml +6 -0
- data/test/fixtures/services.yml +6 -0
- data/test/fixtures/subscriptions.yml +16 -0
- data/test/fixtures/users.yml +20 -0
- data/test/test_helper.rb +67 -0
- data/test/unit/active_record_predicates_test.rb +88 -0
- data/test/unit/attribute_formats_test.rb +40 -0
- data/test/unit/inheritance_test.rb +23 -0
- data/test/unit/predicates/aliased_test.rb +17 -0
- data/test/unit/predicates/association_predicate_test.rb +51 -0
- data/test/unit/predicates/base_test.rb +53 -0
- data/test/unit/predicates/blacklisted_predicate_test.rb +28 -0
- data/test/unit/predicates/domain_predicate_test.rb +27 -0
- data/test/unit/predicates/email_test.rb +82 -0
- data/test/unit/predicates/enumerated_predicate_test.rb +22 -0
- data/test/unit/predicates/hex_color_predicate_test.rb +29 -0
- data/test/unit/predicates/length_predicate_test.rb +85 -0
- data/test/unit/predicates/number_test.rb +109 -0
- data/test/unit/predicates/pattern_predicate_test.rb +29 -0
- data/test/unit/predicates/phone_number_predicate_test.rb +41 -0
- data/test/unit/predicates/required_predicate_test.rb +13 -0
- data/test/unit/predicates/same_as_predicate_test.rb +19 -0
- data/test/unit/predicates/time_test.rb +49 -0
- data/test/unit/predicates/unique_test.rb +58 -0
- data/test/unit/predicates/url_test.rb +86 -0
- data/test/unit/predicates/usa_state_test.rb +31 -0
- data/test/unit/predicates/usa_zip_code_test.rb +42 -0
- data/test/unit/semantic_attribute_test.rb +18 -0
- data/test/unit/semantic_attributes_test.rb +29 -0
- data/test/unit/validations_test.rb +121 -0
- metadata +235 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,99 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
semantic_attributes (1.0.1)
|
5
|
+
rails (~> 3.2.2)
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: http://rubygems.org/
|
9
|
+
specs:
|
10
|
+
actionmailer (3.2.2)
|
11
|
+
actionpack (= 3.2.2)
|
12
|
+
mail (~> 2.4.0)
|
13
|
+
actionpack (3.2.2)
|
14
|
+
activemodel (= 3.2.2)
|
15
|
+
activesupport (= 3.2.2)
|
16
|
+
builder (~> 3.0.0)
|
17
|
+
erubis (~> 2.7.0)
|
18
|
+
journey (~> 1.0.1)
|
19
|
+
rack (~> 1.4.0)
|
20
|
+
rack-cache (~> 1.1)
|
21
|
+
rack-test (~> 0.6.1)
|
22
|
+
sprockets (~> 2.1.2)
|
23
|
+
activemodel (3.2.2)
|
24
|
+
activesupport (= 3.2.2)
|
25
|
+
builder (~> 3.0.0)
|
26
|
+
activerecord (3.2.2)
|
27
|
+
activemodel (= 3.2.2)
|
28
|
+
activesupport (= 3.2.2)
|
29
|
+
arel (~> 3.0.2)
|
30
|
+
tzinfo (~> 0.3.29)
|
31
|
+
activeresource (3.2.2)
|
32
|
+
activemodel (= 3.2.2)
|
33
|
+
activesupport (= 3.2.2)
|
34
|
+
activesupport (3.2.2)
|
35
|
+
i18n (~> 0.6)
|
36
|
+
multi_json (~> 1.0)
|
37
|
+
arel (3.0.2)
|
38
|
+
builder (3.0.0)
|
39
|
+
erubis (2.7.0)
|
40
|
+
hike (1.2.1)
|
41
|
+
i18n (0.6.0)
|
42
|
+
journey (1.0.3)
|
43
|
+
json (1.6.6)
|
44
|
+
mail (2.4.4)
|
45
|
+
i18n (>= 0.4.0)
|
46
|
+
mime-types (~> 1.16)
|
47
|
+
treetop (~> 1.4.8)
|
48
|
+
metaclass (0.0.1)
|
49
|
+
mime-types (1.18)
|
50
|
+
mocha (0.10.5)
|
51
|
+
metaclass (~> 0.0.1)
|
52
|
+
multi_json (1.2.0)
|
53
|
+
polyglot (0.3.3)
|
54
|
+
rack (1.4.1)
|
55
|
+
rack-cache (1.2)
|
56
|
+
rack (>= 0.4)
|
57
|
+
rack-ssl (1.3.2)
|
58
|
+
rack
|
59
|
+
rack-test (0.6.1)
|
60
|
+
rack (>= 1.0)
|
61
|
+
rails (3.2.2)
|
62
|
+
actionmailer (= 3.2.2)
|
63
|
+
actionpack (= 3.2.2)
|
64
|
+
activerecord (= 3.2.2)
|
65
|
+
activeresource (= 3.2.2)
|
66
|
+
activesupport (= 3.2.2)
|
67
|
+
bundler (~> 1.0)
|
68
|
+
railties (= 3.2.2)
|
69
|
+
railties (3.2.2)
|
70
|
+
actionpack (= 3.2.2)
|
71
|
+
activesupport (= 3.2.2)
|
72
|
+
rack-ssl (~> 1.3.2)
|
73
|
+
rake (>= 0.8.7)
|
74
|
+
rdoc (~> 3.4)
|
75
|
+
thor (~> 0.14.6)
|
76
|
+
rake (0.8.7)
|
77
|
+
rdoc (3.12)
|
78
|
+
json (~> 1.4)
|
79
|
+
sprockets (2.1.2)
|
80
|
+
hike (~> 1.2)
|
81
|
+
rack (~> 1.0)
|
82
|
+
tilt (~> 1.1, != 1.3.0)
|
83
|
+
sqlite3 (1.3.5)
|
84
|
+
thor (0.14.6)
|
85
|
+
tilt (1.3.3)
|
86
|
+
treetop (1.4.10)
|
87
|
+
polyglot
|
88
|
+
polyglot (>= 0.3.1)
|
89
|
+
tzinfo (0.3.32)
|
90
|
+
|
91
|
+
PLATFORMS
|
92
|
+
ruby
|
93
|
+
|
94
|
+
DEPENDENCIES
|
95
|
+
bundler (>= 1.0.0)
|
96
|
+
mocha (>= 0.10.5)
|
97
|
+
rake (= 0.8.7)
|
98
|
+
semantic_attributes!
|
99
|
+
sqlite3
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2007-2012 Lance Ivy
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
SemanticAttributes
|
2
|
+
==================
|
3
|
+
-by Lance Ivy, 2007
|
4
|
+
|
5
|
+
http://github.com/cainlevy/semantic-attributes
|
6
|
+
|
7
|
+
http://code.google.com/p/semanticattributes/
|
8
|
+
|
9
|
+
==Summary
|
10
|
+
|
11
|
+
A validation library that allows introspection (User.name_is_required?) and supports database normalization (aka "form input cleaning").
|
12
|
+
|
13
|
+
==Philosophy
|
14
|
+
|
15
|
+
The method-chained validation routine built into ActiveRecord must die! It's time for an object-oriented approach to attribute validations. The Semantic Attributes plugin provides this approach by letting you attach predicates to your attributes with a tasty DSL. These predicates package up some really sweet behavior, where validations are really only the beginning. I've also discovered that it can be really useful to use these predicates to convert between human and machine formats: for example, with the phone number predicate you can let your users enter phone numbers with whatever formatting they want, always save the values to the database as numeric strings, and then present the values back to the user with standard formatting.
|
16
|
+
|
17
|
+
I've also found other nifty uses for object-oriented predicates that package up validation. For example, it becomes easy to run a quick validation check on a field with a sample value and report true/false. This is exactly what the <tt>expected_error_for(:field, value)</tt> method does, and it lets you build a validation routine that listens to form data as it's being typed and report problems without duplicating your validation code client-side. In a similar vein, the <tt>_valid?</tt> attribute suffix lets you do single-attribute validation on a record anytime you want.
|
18
|
+
|
19
|
+
==Example
|
20
|
+
|
21
|
+
class User < ActiveRecord::Base
|
22
|
+
email_is_an_email
|
23
|
+
home_page_is_a_url :domains => ['com', 'net', 'org'], :allow_ip_address => false
|
24
|
+
mobile_is_a_phone_number
|
25
|
+
end
|
26
|
+
|
27
|
+
Now imagine a sample script/console session:
|
28
|
+
|
29
|
+
>> User.name_is_required?
|
30
|
+
=> true
|
31
|
+
>> User.mobile_is_required?
|
32
|
+
=> false
|
33
|
+
|
34
|
+
Ok, we have a DSL for introspection. What if we want to retrieve configuration details?
|
35
|
+
|
36
|
+
>> User.semantic_attributes[:home_page].get(:url).domains
|
37
|
+
=> ['com', 'net', 'org']
|
38
|
+
|
39
|
+
Let's create a user and play around with some instance methods:
|
40
|
+
|
41
|
+
>> user = User.new
|
42
|
+
>> user.mobile = '222 333.4444'
|
43
|
+
>> user.mobile_valid?
|
44
|
+
=> true
|
45
|
+
>> user.mobile
|
46
|
+
=> '+12223334444'
|
47
|
+
>> user.mobile_for_human
|
48
|
+
=> '(222) 333-4444'
|
49
|
+
|
50
|
+
==See Also
|
51
|
+
* gist.rdoc
|
52
|
+
* Predicates
|
53
|
+
* ActiveRecord::Predicates::ClassMethods (see #method_missing)
|
54
|
+
* ActiveRecord::AttributeFormats
|
data/Rakefile
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
begin
|
3
|
+
require 'bundler/setup'
|
4
|
+
rescue LoadError
|
5
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
6
|
+
end
|
7
|
+
require 'rake/testtask'
|
8
|
+
begin
|
9
|
+
require 'rdoc/task'
|
10
|
+
rescue LoadError
|
11
|
+
require 'rdoc/rdoc'
|
12
|
+
require 'rake/rdoctask'
|
13
|
+
RDoc::Task = Rake::RDocTask
|
14
|
+
end
|
15
|
+
|
16
|
+
desc 'Default: run unit tests.'
|
17
|
+
task :default => :test
|
18
|
+
|
19
|
+
desc 'Test the SemanticAttributes plugin.'
|
20
|
+
Rake::TestTask.new(:test) do |t|
|
21
|
+
t.libs << 'lib'
|
22
|
+
t.pattern = 'test/unit/**/*_test.rb'
|
23
|
+
t.verbose = true
|
24
|
+
end
|
25
|
+
|
26
|
+
desc 'Generate documentation for the SemanticAttributes plugin.'
|
27
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
28
|
+
rdoc.rdoc_dir = 'rdoc'
|
29
|
+
rdoc.title = 'SemanticAttributes'
|
30
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
31
|
+
rdoc.rdoc_files.include('README')
|
32
|
+
rdoc.rdoc_files.include('gist.rdoc')
|
33
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
34
|
+
end
|
35
|
+
|
36
|
+
Bundler::GemHelper.install_tasks
|
37
|
+
|
data/gist.rdoc
ADDED
@@ -0,0 +1,208 @@
|
|
1
|
+
==Describing
|
2
|
+
|
3
|
+
Format:
|
4
|
+
|
5
|
+
#{attribute_name}_#{verb}_#{required}?_#{predicate}(options = {})
|
6
|
+
|
7
|
+
Example:
|
8
|
+
|
9
|
+
class Post < ActiveRecord::Base
|
10
|
+
belongs_to :author
|
11
|
+
|
12
|
+
title_is_required
|
13
|
+
author_is_a_required_association
|
14
|
+
body_has_length :below => 256
|
15
|
+
end
|
16
|
+
|
17
|
+
==Predicates
|
18
|
+
|
19
|
+
===aliased
|
20
|
+
Use when the attribute may only contain certain values, but those values have human labels.
|
21
|
+
|
22
|
+
Example:
|
23
|
+
|
24
|
+
class User < ActiveRecord::Base
|
25
|
+
classification_is_aliased :options => {
|
26
|
+
1 => "user",
|
27
|
+
2 => "admin",
|
28
|
+
3 => "superadmin"
|
29
|
+
}
|
30
|
+
end
|
31
|
+
|
32
|
+
===association
|
33
|
+
Use when an association has a minimum or maximum number of records, or when you need to require that it exists. Note that it's ok for two records to require each other -- there won't be any infinitely recursive validation problems.
|
34
|
+
|
35
|
+
Example:
|
36
|
+
|
37
|
+
class User < ActiveRecord::Base
|
38
|
+
has_many :quotes
|
39
|
+
|
40
|
+
quotes_is_an_association :max => 3
|
41
|
+
end
|
42
|
+
|
43
|
+
class Quote < ActiveRecord::Base
|
44
|
+
belongs_to :user
|
45
|
+
|
46
|
+
user_is_a_required_association
|
47
|
+
end
|
48
|
+
|
49
|
+
===blacklisted
|
50
|
+
Use when you need to make sure some value is NOT saved. Maybe you've reserved some for yourself?
|
51
|
+
|
52
|
+
By default this is not case sensitive. Which means that by default this assumes you only have strings. If you need to blacklist other data types, set :case_sensitive => false.
|
53
|
+
|
54
|
+
Example:
|
55
|
+
|
56
|
+
class User < ActiveRecord::Base
|
57
|
+
username_is_blacklisted :restricted => %w(admin superadmin god user anonymous)
|
58
|
+
end
|
59
|
+
|
60
|
+
===domain
|
61
|
+
Use when you need to capture a domain name, without protocol or path information.
|
62
|
+
|
63
|
+
Example:
|
64
|
+
|
65
|
+
class Account < ActiveRecord::Base
|
66
|
+
cname_is_domain
|
67
|
+
end
|
68
|
+
|
69
|
+
===email
|
70
|
+
Use when you want to eliminate malformed email addresses. This will _not_ ensure deliverability -- that requires a field test.
|
71
|
+
|
72
|
+
Example:
|
73
|
+
|
74
|
+
class User < ActiveRecord::Base
|
75
|
+
email_address_is_email
|
76
|
+
end
|
77
|
+
|
78
|
+
===enumerated aka whitelisted
|
79
|
+
Use when you want to limit the values a field may have. Useful for constraining polymorphic associations! Note that because of how required-ness is handled, if a field is empty this predicate will not be evaluated.
|
80
|
+
|
81
|
+
Note that this predicate implies required-ness.
|
82
|
+
|
83
|
+
Example:
|
84
|
+
|
85
|
+
class Favorite < ActiveRecord::Base
|
86
|
+
belongs_to :favoritable, :polymorphic => true
|
87
|
+
|
88
|
+
# only allow favoriting of a User or Project
|
89
|
+
favoritable_type_is_enumerated :options => %w(User Project)
|
90
|
+
end
|
91
|
+
|
92
|
+
===hex_color
|
93
|
+
Use when you need to capture hex colors. Useful for theming! All colors will be stored in the database with a leading pound sign, expanded to the full six character size (e.g. "a1e" becomes "#aa11ee").
|
94
|
+
|
95
|
+
Example:
|
96
|
+
|
97
|
+
class Account < ActiveRecord::Base
|
98
|
+
background_is_hex_color
|
99
|
+
end
|
100
|
+
|
101
|
+
===length aka size
|
102
|
+
Use when you need to set an upper or lower boundary on the length of a field. Note that this also works on arrays and hashes.
|
103
|
+
|
104
|
+
Example:
|
105
|
+
|
106
|
+
class User < ActiveRecord::Base
|
107
|
+
username_has_length :range => 3..20
|
108
|
+
# the following are identical:
|
109
|
+
password_has_length :above => 3
|
110
|
+
password_has_length :above => 4, :exactly => true
|
111
|
+
end
|
112
|
+
|
113
|
+
===number
|
114
|
+
Use when you have a numeric field that needs to be constrained on the number line.
|
115
|
+
|
116
|
+
Example:
|
117
|
+
|
118
|
+
class Auction < ActiveRecord::Base
|
119
|
+
buyout_is_number :integer => true
|
120
|
+
bid_increment_is_number :at_least => 5
|
121
|
+
quantity_is_number :range => 1..10
|
122
|
+
end
|
123
|
+
|
124
|
+
===pattern
|
125
|
+
Use when you need to define a regular expression pattern for a field. Actually, DON'T USE THIS. Instead, extend it and create a new predicate!
|
126
|
+
|
127
|
+
===phone_number
|
128
|
+
Use when you want to validate phone numbers against a formal numbering plan. Currently only supports NANP (North American Numbering Plan), which uses the +1 prefix. This predicate is smart enough to exclude the bogus 555-01xx numbers.
|
129
|
+
|
130
|
+
If you use Semantic Attributes in an international application before I do, please help by contributing back to this predicate.
|
131
|
+
|
132
|
+
Example:
|
133
|
+
|
134
|
+
class User < ActiveRecord::Base
|
135
|
+
mobile_is_a_phone_number
|
136
|
+
end
|
137
|
+
|
138
|
+
===required
|
139
|
+
Use when you simple need a field to be required. Note that if the field has any other semantics, you should add required-ness to those!
|
140
|
+
|
141
|
+
Example:
|
142
|
+
|
143
|
+
class User < ActiveRecord::Base
|
144
|
+
password_is_required
|
145
|
+
end
|
146
|
+
|
147
|
+
===same_as
|
148
|
+
Use when you need some attribute to be the same as another attribute, aka this-is-how-you-do-password-confirmation.
|
149
|
+
|
150
|
+
Example:
|
151
|
+
|
152
|
+
class User < ActiveRecord::Base
|
153
|
+
password_confirmation_is_same_as :method => :password
|
154
|
+
end
|
155
|
+
|
156
|
+
===time
|
157
|
+
Use when you have a time field that needs to be constrained on the timeline. You may set your constraint either absolutely (e.g. after Jan 1, 2005) or relatively (e.g. no older than 5 minutes from now).
|
158
|
+
|
159
|
+
Example:
|
160
|
+
|
161
|
+
class Project < ActiveRecord::Base
|
162
|
+
# this deadline must be after Jan 1, 2005
|
163
|
+
deadline_is_time :after => Time.parse("2005-01-01 00:00:00")
|
164
|
+
end
|
165
|
+
|
166
|
+
class Project < ActiveRecord::Base
|
167
|
+
# this deadline must be no older than 5 days and no further in the future than 1 week, as of the time of validation.
|
168
|
+
deadline_is_time :distance => (-5.days)..(1.week)
|
169
|
+
end
|
170
|
+
|
171
|
+
===unique
|
172
|
+
Use when you need an attribute to be unique, possibly in the scope of some other attributes. By default this is not case sensitive.
|
173
|
+
|
174
|
+
Example:
|
175
|
+
|
176
|
+
class User < ActiveRecord::Base
|
177
|
+
email_is_unique :scope => [:account_id]
|
178
|
+
end
|
179
|
+
|
180
|
+
===url
|
181
|
+
Use when you have url. Please, use it! URLs can be complex. You may constrain your url to a list of domains, schemes, or ports. You may allow or disallow ip addresses.
|
182
|
+
|
183
|
+
Example:
|
184
|
+
|
185
|
+
class User < ActiveRecord::Base
|
186
|
+
homepage_is_url :domain => %w(com net biz info edu)
|
187
|
+
backup_is_url :schemes => %w(https), :ports => [443], :implied_scheme => 'https'
|
188
|
+
end
|
189
|
+
|
190
|
+
===usa_state
|
191
|
+
Use when you have a USA state (or territory). Stores all states using USPS abbreviation.
|
192
|
+
|
193
|
+
Example:
|
194
|
+
|
195
|
+
class Address < ActiveRecord::Base
|
196
|
+
state_is_a_usa_state :with_territories => true
|
197
|
+
end
|
198
|
+
|
199
|
+
===usa_zip_code
|
200
|
+
Use when you have a USA postal code, possibly with the extended +4 syntax.
|
201
|
+
|
202
|
+
Example:
|
203
|
+
|
204
|
+
class Address < ActiveRecord::Base
|
205
|
+
# :extended may be any of :allowed, :required, or false (default)
|
206
|
+
postal_code_is_a_usa_zip_code :extended => :allowed
|
207
|
+
end
|
208
|
+
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module ActiveRecord #:nodoc:
|
2
|
+
module ValidationRecursionControl #:nodoc:
|
3
|
+
def self.included(base)
|
4
|
+
base.class_eval do
|
5
|
+
alias_method_chain :valid?, :recursion_control
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
# It's easy to break out of circular validation dependencies. All we need to do
|
10
|
+
# is suppose that if a record's validity depends in some way on itself, then we
|
11
|
+
# can assume that circular condition is satisfied. That assumption will change
|
12
|
+
# nothing about the actual validity of the record.
|
13
|
+
def valid_with_recursion_control?(*args, &block)
|
14
|
+
assumed_valid? or with_recursion_control do valid_without_recursion_control?(*args, &block) end
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
mattr_accessor :recursion_stack
|
20
|
+
@@recursion_stack = []
|
21
|
+
|
22
|
+
def assumed_valid?
|
23
|
+
recursion_stack.include? self
|
24
|
+
end
|
25
|
+
|
26
|
+
def with_recursion_control(&block)
|
27
|
+
recursion_stack << self
|
28
|
+
result = yield
|
29
|
+
recursion_stack.delete(self)
|
30
|
+
result
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# A special case of enumeration where the values are actually aliased for humans.
|
2
|
+
# Create this like a normal enumeration, but make :options a Hash of {value => alias}
|
3
|
+
#
|
4
|
+
# ==Example
|
5
|
+
# field_is_aliased :options => {'a' => 'Alpha', 'b' => 'Beta'}
|
6
|
+
class Predicates::Aliased < Predicates::Enumerated
|
7
|
+
def to_human(v)
|
8
|
+
options[v]
|
9
|
+
end
|
10
|
+
|
11
|
+
def validate(value, record)
|
12
|
+
self.options.has_value? value
|
13
|
+
end
|
14
|
+
|
15
|
+
def normalize(v)
|
16
|
+
if RUBY_VERSION < "1.9"
|
17
|
+
options.index(v)
|
18
|
+
else
|
19
|
+
options.key(v)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# Marks an attribute as being an association. Has options for controlling how many associated objects there can be.
|
2
|
+
#
|
3
|
+
# You can require associations by name.
|
4
|
+
#
|
5
|
+
# Example:
|
6
|
+
# class Comment < ActiveRecord::Base
|
7
|
+
# has_one :owner
|
8
|
+
# owner_is_association :or_empty => true
|
9
|
+
# end
|
10
|
+
class Predicates::Association < Predicates::Base
|
11
|
+
# if there's a minimum to the number of associated records
|
12
|
+
attr_accessor :min
|
13
|
+
|
14
|
+
# if there's a maximum to the number of associated records
|
15
|
+
attr_accessor :max
|
16
|
+
|
17
|
+
def error_message
|
18
|
+
@error_message || :required
|
19
|
+
end
|
20
|
+
|
21
|
+
def validate(value, record)
|
22
|
+
# we treat singular and plural the same
|
23
|
+
associated = [value].flatten
|
24
|
+
|
25
|
+
# we need to check the validity of new records in order to calculate how many
|
26
|
+
# will save properly. this lets us validate against the min/max parameters.
|
27
|
+
invalid_new_records = associated.select{|r| r.new_record? and not r.valid?}
|
28
|
+
valid_new_records = associated - invalid_new_records
|
29
|
+
|
30
|
+
valid = true
|
31
|
+
|
32
|
+
# then validate against min/max
|
33
|
+
quantity = valid_new_records.length
|
34
|
+
valid &&= (!min or quantity >= min)
|
35
|
+
valid &&= (!max or quantity <= max)
|
36
|
+
|
37
|
+
# if we can't allow empty associations, then we need to do an extra check: if these
|
38
|
+
# records are new and invalid then they might not persist.
|
39
|
+
valid &&= (allow_empty? or quantity > 0)
|
40
|
+
|
41
|
+
valid
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
module Predicates
|
2
|
+
# The base class for all predicates. Defines the interface and standard settings.
|
3
|
+
#
|
4
|
+
# All predicates that inherit from Base get the following options:
|
5
|
+
#
|
6
|
+
# :error_message Feedback for the user if the validation fails. Remember that Rails will prefix the attribute name.
|
7
|
+
# :validate_if Restricts when the validation can happen. If it returns false, validation will not happen. May be a proc (with the record object as the argument) or a symbol that names a method on the record to call.
|
8
|
+
# :validate_on When to do the validation, during :update, :create, or both (default).
|
9
|
+
# :or_empty Whether to allow empty/nil values during validation (default: true)
|
10
|
+
class Base
|
11
|
+
##
|
12
|
+
## Standard Configuration Options
|
13
|
+
##
|
14
|
+
|
15
|
+
# the error string when validation fails
|
16
|
+
def error_message
|
17
|
+
@error_message || :invalid
|
18
|
+
end
|
19
|
+
attr_writer :error_message
|
20
|
+
alias_accessor :message, :error_message
|
21
|
+
|
22
|
+
# available interpolation variables for the error message (see I18n.translate)
|
23
|
+
def error_binds
|
24
|
+
{}
|
25
|
+
end
|
26
|
+
|
27
|
+
# a message that won't be pre-interpolated by semantic-attributes, so that it
|
28
|
+
# can work with ActiveRecord::Error#generate_full_message's translation lookup
|
29
|
+
attr_accessor :full_message
|
30
|
+
|
31
|
+
def error
|
32
|
+
if full_message
|
33
|
+
full_message
|
34
|
+
elsif error_message.is_a?(Symbol)
|
35
|
+
I18n.t(error_message, error_binds.merge(:scope => 'semantic-attributes.errors.messages'))
|
36
|
+
else
|
37
|
+
error_message
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# a condition to restrict when validation should occur. if it returns false, the validation will not happen.
|
42
|
+
# if the value is a proc, then the proc will be called and the record object passed as the argument
|
43
|
+
# if the value is a symbol, then a method by that name will be called on the record
|
44
|
+
attr_accessor :validate_if
|
45
|
+
alias_accessor :if, :validate_if
|
46
|
+
|
47
|
+
# defines when to do the validation - during :update or :create (default is both, signified by absence of specification)
|
48
|
+
# options: :update, :create, and :both
|
49
|
+
attr_reader :validate_on
|
50
|
+
def validate_on=(val)
|
51
|
+
raise ArgumentError('unknown value for :validate_on parameter') unless [:update, :create, :both].include? val
|
52
|
+
@validate_on = val
|
53
|
+
end
|
54
|
+
alias_accessor :on, :validate_on
|
55
|
+
|
56
|
+
# whether to allow empty (and nil) values during validation (default: true)
|
57
|
+
attr_writer :or_empty
|
58
|
+
def allow_empty?
|
59
|
+
@or_empty ? true : false
|
60
|
+
end
|
61
|
+
|
62
|
+
##
|
63
|
+
## Internal
|
64
|
+
##
|
65
|
+
|
66
|
+
# the initialization method provides quick support for assigning options using existing methods
|
67
|
+
def initialize(attribute_name, options = {})
|
68
|
+
@attribute = attribute_name
|
69
|
+
@validate_on = :both
|
70
|
+
@or_empty = true
|
71
|
+
options.each_pair do |k, v|
|
72
|
+
self.send("#{k}=", v)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# define this in the concrete class to provide a validation routine for your predicate
|
77
|
+
def validate(value, record)
|
78
|
+
raise NotImplementedError
|
79
|
+
end
|
80
|
+
|
81
|
+
# define this in the concrete class to provide a method for normalizing human inputs.
|
82
|
+
# this gives you the ability to be very forgiving of formatting variations in form data.
|
83
|
+
def normalize(value)
|
84
|
+
value
|
85
|
+
end
|
86
|
+
|
87
|
+
# define this in the concrete class to provide a method for converting from a storage format to a human readable format
|
88
|
+
# this is good for presenting your clean, logical data in a way that people like to read.
|
89
|
+
def to_human(value)
|
90
|
+
value
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# Blacklisted is the inverse of Enumerated. This is how you can say that a field may _not_ be certain values.
|
2
|
+
#
|
3
|
+
# ==Example
|
4
|
+
# field_is_blacklisted :not => ['disallowed_value_one', 'disallowed_value_two']
|
5
|
+
class Predicates::Blacklisted < Predicates::Base
|
6
|
+
# whether the comparison is case-sensitive
|
7
|
+
attr_accessor :case_sensitive
|
8
|
+
|
9
|
+
# the blacklist
|
10
|
+
attr_accessor :restricted
|
11
|
+
|
12
|
+
def error_message
|
13
|
+
@error_message || :exclusion
|
14
|
+
end
|
15
|
+
|
16
|
+
def validate(val, record)
|
17
|
+
if self.case_sensitive
|
18
|
+
!self.restricted.include? val
|
19
|
+
else
|
20
|
+
!self.restricted.any? {|r| r.to_s.downcase == val.to_s.downcase }
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'uri'
|
2
|
+
|
3
|
+
# Defines a field as a simple domain (not URL).
|
4
|
+
class Predicates::Domain < Predicates::Base
|
5
|
+
def error_message
|
6
|
+
@error_message || :domain
|
7
|
+
end
|
8
|
+
|
9
|
+
def validate(value, record)
|
10
|
+
url = URI.parse(with_protocol(value))
|
11
|
+
valid = (url.host == value)
|
12
|
+
valid &&= (value.match /\..+\Z/) # to catch "http://example" or similar
|
13
|
+
valid &&= (!value.match /^([0-9]{1,3}\.){3}[0-9]{1,3}$/) # to catch ip addresses
|
14
|
+
|
15
|
+
valid
|
16
|
+
rescue URI::InvalidURIError
|
17
|
+
false
|
18
|
+
end
|
19
|
+
|
20
|
+
def normalize(v)
|
21
|
+
URI.parse(with_protocol(v)).host || v
|
22
|
+
rescue URI::InvalidURIError
|
23
|
+
v
|
24
|
+
end
|
25
|
+
|
26
|
+
protected
|
27
|
+
|
28
|
+
def with_protocol(value)
|
29
|
+
!value or value.include?("://") ? value : "http://#{value}"
|
30
|
+
end
|
31
|
+
end
|