semantic_attributes 1.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (75) hide show
  1. data/.gitignore +2 -0
  2. data/Gemfile +3 -0
  3. data/Gemfile.lock +99 -0
  4. data/MIT-LICENSE +20 -0
  5. data/README +54 -0
  6. data/Rakefile +37 -0
  7. data/gist.rdoc +208 -0
  8. data/lib/active_record/validation_recursion_control.rb +33 -0
  9. data/lib/core_ext/class.rb +14 -0
  10. data/lib/predicates/aliased.rb +22 -0
  11. data/lib/predicates/association.rb +43 -0
  12. data/lib/predicates/base.rb +93 -0
  13. data/lib/predicates/blacklisted.rb +23 -0
  14. data/lib/predicates/domain.rb +31 -0
  15. data/lib/predicates/email.rb +42 -0
  16. data/lib/predicates/enumerated.rb +23 -0
  17. data/lib/predicates/hex_color.rb +24 -0
  18. data/lib/predicates/length.rb +71 -0
  19. data/lib/predicates/number.rb +104 -0
  20. data/lib/predicates/pattern.rb +22 -0
  21. data/lib/predicates/phone_number.rb +62 -0
  22. data/lib/predicates/required.rb +22 -0
  23. data/lib/predicates/same_as.rb +17 -0
  24. data/lib/predicates/size.rb +2 -0
  25. data/lib/predicates/time.rb +43 -0
  26. data/lib/predicates/unique.rb +71 -0
  27. data/lib/predicates/url.rb +62 -0
  28. data/lib/predicates/usa_state.rb +87 -0
  29. data/lib/predicates/usa_zip_code.rb +25 -0
  30. data/lib/predicates/whitelisted.rb +2 -0
  31. data/lib/predicates.rb +3 -0
  32. data/lib/semantic_attributes/attribute.rb +46 -0
  33. data/lib/semantic_attributes/attribute_formats.rb +67 -0
  34. data/lib/semantic_attributes/locale/en.yml +31 -0
  35. data/lib/semantic_attributes/predicates.rb +170 -0
  36. data/lib/semantic_attributes/set.rb +40 -0
  37. data/lib/semantic_attributes/version.rb +3 -0
  38. data/lib/semantic_attributes.rb +37 -0
  39. data/semantic_attributes.gemspec +29 -0
  40. data/test/db/database.yml +3 -0
  41. data/test/db/models.rb +38 -0
  42. data/test/db/schema.rb +33 -0
  43. data/test/fixtures/addresses.yml +15 -0
  44. data/test/fixtures/roles.yml +4 -0
  45. data/test/fixtures/roles_users.yml +6 -0
  46. data/test/fixtures/services.yml +6 -0
  47. data/test/fixtures/subscriptions.yml +16 -0
  48. data/test/fixtures/users.yml +20 -0
  49. data/test/test_helper.rb +67 -0
  50. data/test/unit/active_record_predicates_test.rb +88 -0
  51. data/test/unit/attribute_formats_test.rb +40 -0
  52. data/test/unit/inheritance_test.rb +23 -0
  53. data/test/unit/predicates/aliased_test.rb +17 -0
  54. data/test/unit/predicates/association_predicate_test.rb +51 -0
  55. data/test/unit/predicates/base_test.rb +53 -0
  56. data/test/unit/predicates/blacklisted_predicate_test.rb +28 -0
  57. data/test/unit/predicates/domain_predicate_test.rb +27 -0
  58. data/test/unit/predicates/email_test.rb +82 -0
  59. data/test/unit/predicates/enumerated_predicate_test.rb +22 -0
  60. data/test/unit/predicates/hex_color_predicate_test.rb +29 -0
  61. data/test/unit/predicates/length_predicate_test.rb +85 -0
  62. data/test/unit/predicates/number_test.rb +109 -0
  63. data/test/unit/predicates/pattern_predicate_test.rb +29 -0
  64. data/test/unit/predicates/phone_number_predicate_test.rb +41 -0
  65. data/test/unit/predicates/required_predicate_test.rb +13 -0
  66. data/test/unit/predicates/same_as_predicate_test.rb +19 -0
  67. data/test/unit/predicates/time_test.rb +49 -0
  68. data/test/unit/predicates/unique_test.rb +58 -0
  69. data/test/unit/predicates/url_test.rb +86 -0
  70. data/test/unit/predicates/usa_state_test.rb +31 -0
  71. data/test/unit/predicates/usa_zip_code_test.rb +42 -0
  72. data/test/unit/semantic_attribute_test.rb +18 -0
  73. data/test/unit/semantic_attributes_test.rb +29 -0
  74. data/test/unit/validations_test.rb +121 -0
  75. metadata +235 -0
data/.gitignore ADDED
@@ -0,0 +1,2 @@
1
+ rdoc
2
+ test/test.log
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source :rubygems
2
+
3
+ gemspec
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,14 @@
1
+ class Class #:nodoc:
2
+ def alias_accessor(new, old)
3
+ alias_reader(new, old)
4
+ alias_writer(new, old)
5
+ end
6
+
7
+ def alias_reader(new, old)
8
+ alias_method new, old
9
+ end
10
+
11
+ def alias_writer(new, old)
12
+ alias_method "#{new}=", "#{old}="
13
+ end
14
+ 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