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
@@ -0,0 +1,42 @@
1
+ # Describes a regular expression pattern for email addresses, based on RFC2822 and RFC3696.
2
+ # Adapted from Alex Dunae's validates_email_format_of plugin v1.2.2, available under MIT License at http://code.dunae.ca/validates_email_format_of.html
3
+ #
4
+ # ==Options
5
+ # * :with_mx_record [boolean, default false] - whether to verify the email's domain using a dns lookup for an mx record. This requires the Unix `dig` command. In Debian this is part of the dnsutils package. NOTE: RFC2821 states that when no MX records are listed, an A record may be used instead. This means that a missing MX record may not mean an invalid email address! Use at your own risk.
6
+ #
7
+ # == Example
8
+ # field_is_an_email :with_mx_record => true
9
+ class Predicates::Email < Predicates::Base
10
+ attr_accessor :with_mx_record
11
+
12
+ def validate(value, record)
13
+ # local part max is 64 chars, domain part max is 255 chars
14
+ domain, local = value.reverse.split('@', 2)
15
+
16
+ valid = value.match(EmailAddressPattern)
17
+ valid &&= !(value.match /\.\./)
18
+ valid &&= (domain.length <= 255)
19
+ valid &&= (local.length <= 64)
20
+
21
+ if valid and self.with_mx_record
22
+ mx_record = `dig #{value.split('@').last} mx +noall +short`
23
+ valid &&= (!mx_record.empty?)
24
+ end
25
+
26
+ valid
27
+ end
28
+
29
+ def error_message
30
+ @error_message || :email
31
+ end
32
+
33
+ EmailAddressPattern = begin
34
+ local_part_special_chars = Regexp.escape('!#$%&\'*-/=?+-^_`{|}~')
35
+ local_part_unquoted = '(([[:alnum:]' + local_part_special_chars + ']+[\.\+]+))*[[:alnum:]' + local_part_special_chars + '+]+'
36
+ local_part_quoted = '\"(([[:alnum:]' + local_part_special_chars + '\.\+]*|(\\\\[\x00-\xFF]))*)\"'
37
+ Regexp.new(
38
+ '^((' + local_part_unquoted + ')|(' + local_part_quoted + ')+)@(((\w+\-+)|(\w+\.))*\w{1,63}\.[a-z]{2,6}$)',
39
+ Regexp::EXTENDED | Regexp::IGNORECASE, "n"
40
+ )
41
+ end
42
+ end
@@ -0,0 +1,23 @@
1
+ # A basic pattern where the values are enumerated.
2
+ #
3
+ # ==Options
4
+ # * :options - a complete collection of values that the attribute may contain
5
+ #
6
+ # ==Example
7
+ # field_is_enumerated :options => ['allowed_value_one', 'allowed_value_two']
8
+ class Predicates::Enumerated < Predicates::Base
9
+ attr_accessor :options
10
+
11
+ def initialize(attr, options = {})
12
+ options[:or_empty] ||= false
13
+ super(attr, options)
14
+ end
15
+
16
+ def error_message
17
+ @error_message || :inclusion
18
+ end
19
+
20
+ def validate(value, record)
21
+ self.options.include? value
22
+ end
23
+ end
@@ -0,0 +1,24 @@
1
+ # Defines a field as a hex color. These are hexadecimal strings from 3 to 6 characters long. All hex colors will be saved in the database with a preceding pound sign (e.g. #123456). Also, #123456 is actually a pretty cool color. You should try it.
2
+ class Predicates::HexColor < Predicates::Pattern
3
+ # a fixed regexp
4
+ def like
5
+ @like ||= /\A#[0-9a-fA-F]{6}\Z/
6
+ end
7
+
8
+ def error_message
9
+ @error_message ||= :hex
10
+ end
11
+
12
+ def normalize(value)
13
+ return value if value.blank?
14
+
15
+ # ensure leading pound sign
16
+ value = "##{value}" unless value[0].chr == '#'
17
+ # expand from three characters to six characters
18
+ if value =~ /\A#[0-9a-fA-F]{3}\Z/
19
+ value = "##{value[1].chr * 2}#{value[2].chr * 2}#{value[3].chr * 2}"
20
+ end
21
+
22
+ value
23
+ end
24
+ end
@@ -0,0 +1,71 @@
1
+ # Lets you declare a min/max/exact length for something. This works for arrays and strings both.
2
+ #
3
+ # ==Options
4
+ # * :above [integer] - when the attribute has a minimum
5
+ # * :below [integer] - when the attribute has a maximum
6
+ # * :range [range] - when the attribute has a minimum and a maximum
7
+ # * :exactly [integer] - when the attribute must be exactly some length
8
+ #
9
+ # ==Examples
10
+ # field_has_length :exactly => 3
11
+ # field_has_length :above => 5
12
+ # field_has_length :range => 4..8
13
+ class Predicates::Length < Predicates::Base
14
+ # when the length has just a min
15
+ attr_accessor :above
16
+
17
+ # when the length has just a max
18
+ attr_accessor :below
19
+
20
+ # when the length has both a max and a min
21
+ attr_accessor :range
22
+
23
+ # when the length must be exact
24
+ attr_accessor :exactly
25
+
26
+ def error_message
27
+ @error_message || range_description
28
+ end
29
+
30
+ def error_binds
31
+ self.range ?
32
+ {:min => self.range.first, :max => self.range.last} :
33
+ {:min => self.above, :max => self.below, :count => self.exactly}
34
+ end
35
+
36
+ def validate(value, record)
37
+ l = tokenize(value).length
38
+ if self.exactly
39
+ l == self.exactly
40
+ elsif self.range
41
+ self.range.include? l
42
+ elsif self.above
43
+ l > self.above
44
+ elsif self.below
45
+ l < self.below
46
+ else
47
+ true
48
+ end
49
+ end
50
+
51
+ def normalize(v)
52
+ (v and v.is_a? String) ? v.gsub("\r\n", "\n") : v
53
+ end
54
+
55
+ protected
56
+
57
+ def tokenize(value)
58
+ case value
59
+ when Array, Hash then value
60
+ else value.to_s.mb_chars
61
+ end
62
+ end
63
+
64
+ def range_description
65
+ return :inexact_length if self.exactly
66
+ return :wrong_length if self.range
67
+ return :too_short if self.above
68
+ return :too_long if self.below
69
+ raise 'undetermined range'
70
+ end
71
+ end
@@ -0,0 +1,104 @@
1
+ # Describes an attribute as a number. You may specify boundaries for the number (min, max, or both), and also specify whether it must be an integer.
2
+ #
3
+ # ==Options
4
+ # * :integer [boolean, default: false] - if the number is an integer (or float/decimal)
5
+ # * :above [integer, float] - when the number has a minimum
6
+ # * :below [integer, float] - when the number has a maximum
7
+ # * :range [range] - when the number has a minimum and a maximum
8
+ # * :inclusive [boolean, default: false] - if your maximum or minimum is also an allowed value. Does not work with :range.
9
+ # * :at_least [integer, float] - an easy way to say :above and :inclusive
10
+ # * :no_more_than [integer, float] - an easy way to say :below and :inclusive
11
+ #
12
+ # ==Examples
13
+ # field_is_a_number :integer => true
14
+ # field_is_a_number :range => 1..5, :integer => true
15
+ # field_is_a_number :above => 4.5
16
+ # field_is_a_number :below => 4.5, :inclusive => true
17
+ class Predicates::Number < Predicates::Base
18
+ # whether to require an integer value
19
+ attr_accessor :integer
20
+
21
+ # when the number has a minimum value, but no maximum
22
+ attr_accessor :above
23
+
24
+ # when the number has a maximum value, but no minimum
25
+ attr_accessor :below
26
+
27
+ # when the number has both a maximum and a minimum value
28
+ attr_accessor :range
29
+
30
+ # meant to be used with :above and :below, when you want the endpoint to be inclusive.
31
+ # with the :range option you can just specify inclusion using the standard Ruby range syntax.
32
+ attr_accessor :inclusive
33
+
34
+ def at_least=(val)
35
+ self.above = val
36
+ self.inclusive = true
37
+ end
38
+
39
+ def no_more_than=(val)
40
+ self.below = val
41
+ self.inclusive = true
42
+ end
43
+
44
+ def error_message
45
+ @error_message || range_description
46
+ end
47
+
48
+ def error_binds
49
+ self.range ?
50
+ {:min => self.range.first, :max => self.range.last} :
51
+ {:min => self.above, :max => self.below}
52
+ end
53
+
54
+ def validate(value, record)
55
+ # check data type
56
+ valid = if self.integer
57
+ # if it must be an integer, do a regexp check for digits only
58
+ value.to_s.match(/\A[+-]?[0-9]+\Z/) unless value.is_a? Hash or value.is_a? Array
59
+ else
60
+ # if it can also be a float or decimal, then try a conversion
61
+ begin
62
+ Kernel.Float(value)
63
+ true
64
+ rescue ArgumentError, TypeError
65
+ false
66
+ end
67
+ end
68
+
69
+ # if it's the right data type, then also check boundaries
70
+ valid &&= if self.range
71
+ self.range.include? value
72
+ elsif self.above
73
+ operator = self.inclusive ? :>= : :>
74
+ value.send operator, self.above
75
+ elsif self.below
76
+ operator = self.inclusive ? :<= : :<
77
+ value.send operator, self.below
78
+ else
79
+ true
80
+ end
81
+
82
+ valid
83
+ end
84
+
85
+ protected
86
+
87
+ def range_description
88
+ # if it has two endpoints
89
+ if self.range
90
+ return :between
91
+ end
92
+ # if it only has one inclusive endpoint
93
+ if inclusive
94
+ return :greater_than_or_equal_to if self.above
95
+ return :less_than_or_equal_to if self.below
96
+ # if it only has one exclusive endpoint
97
+ else
98
+ return :greater_than if self.above
99
+ return :less_than if self.below
100
+ end
101
+ # if it has no endpoints
102
+ :not_a_number
103
+ end
104
+ end
@@ -0,0 +1,22 @@
1
+ # Provides a generic pattern predicate, which is extended and used by other pattern-based validations.
2
+ #
3
+ # WARNING: If you define a pattern, you probably want to use \A and \Z instead of ^ and $. The latter anchors are per-line, which means that multi-line strings can sneak stuff past your regular expression. For example:
4
+ #
5
+ # @predicate.like = /^hello world$/
6
+ # assert @predicate.validate("malicious\nhello world\ntext", nil), 'must match only one line'
7
+ # @predicate.like = /\Ahello world\Z/
8
+ # assert !@predicate.validate("malicious\nhello world\ntext", nil), 'must match entire string'
9
+ #
10
+ # ==Options
11
+ # * :like - a regular expression matching pattern
12
+ #
13
+ # == Example
14
+ # field_has_a_pattern :like => /\Aim in ur [a-z]+, [a-z]+ ur [a-z]+\Z/
15
+ class Predicates::Pattern < Predicates::Base
16
+ attr_accessor :like
17
+
18
+ def validate(value, record)
19
+ value = value.to_s if [Symbol, Fixnum].include?(value.class)
20
+ value.match(self.like)
21
+ end
22
+ end
@@ -0,0 +1,62 @@
1
+ # Defines a field as a phone number. Currently it assumes the phone number fits the North American Numbering Plan. Future support of other plans will be implemented by calling code, e.g. +44 (UK) or +33 (FR). These will act as triggers for localized phone number validation.
2
+ #
3
+ # ==Options
4
+ # * implied_country_code [integer, default: 1 (North America)]
5
+ #
6
+ # ==Example
7
+ # field_is_a_phone_number
8
+ class Predicates::PhoneNumber < Predicates::Base
9
+ attr_accessor :implied_country_code
10
+
11
+ def initialize(attr, options = {})
12
+ options[:implied_country_code] ||= 1
13
+ super(attr, options)
14
+ end
15
+
16
+ def error_message
17
+ @error_message ||= :phone
18
+ end
19
+
20
+ def validate(value, record)
21
+ case value
22
+ when Patterns::NANP
23
+ match = $~
24
+ valid = !match.nil?
25
+ valid &&= (match[2] != '555' or match[3][0..1] != '01') # 555-01xx are reserved (http://en.wikipedia.org/wiki/555_telephone_number)
26
+
27
+ else
28
+ valid = false
29
+ end
30
+
31
+ valid
32
+ end
33
+
34
+ # check country code, then format differently for each country
35
+ def to_human(value)
36
+ case value
37
+ when Patterns::NANP
38
+ m = $~
39
+ "(#{m[1]}) #{m[2]}-#{m[3]}"
40
+
41
+ else
42
+ value
43
+ end
44
+ end
45
+
46
+ # strip out all non-numeric characters except a leading +
47
+ def normalize(value)
48
+ return value if value.blank?
49
+
50
+ value = "+#{value}" if value.to_s[0..0] == '1' # north american bias
51
+ value = "+#{implied_country_code}#{value}" unless value.to_s[0..0] == '+'
52
+
53
+ leading_plus = (value[0..0] == '+')
54
+ value.gsub!(/[^0-9]/, '')
55
+ value = "+#{value}" if leading_plus
56
+ value
57
+ end
58
+
59
+ class Patterns
60
+ NANP = /\A\+1([2-9][0-8][0-9])([2-9][0-9]{2})([0-9]{4})\Z/
61
+ end
62
+ end
@@ -0,0 +1,22 @@
1
+ # Marks an attribute as being required. This is really just a shortcut for using the or_empty? setting.
2
+ #
3
+ # Example:
4
+ # class Comment < ActiveRecord::Base
5
+ # has_one :owner
6
+ # subject_is_required
7
+ # owner_is_required
8
+ # end
9
+ class Predicates::Required < Predicates::Base
10
+ # this permanently sets :or_empty to false
11
+ def allow_empty?
12
+ false
13
+ end
14
+
15
+ def error_message
16
+ @error_message || :required
17
+ end
18
+
19
+ def validate(value, record)
20
+ !value.blank?
21
+ end
22
+ end
@@ -0,0 +1,17 @@
1
+ # Requires that one field must be the same as another field. Useful when combined with virtual attributes to describe things like password or email confirmation.
2
+ #
3
+ # ==Options
4
+ #
5
+ # ==Example
6
+ # field_is_same_as :method => :other_field
7
+ class Predicates::SameAs < Predicates::Base
8
+ attr_accessor :method
9
+
10
+ def error_message
11
+ @error_message ||= :same_as
12
+ end
13
+
14
+ def validate(value, record)
15
+ value == record.send(self.method)
16
+ end
17
+ end
@@ -0,0 +1,2 @@
1
+ # Size is just an alias for Length
2
+ class Predicates::Size < Predicates::Length; end
@@ -0,0 +1,43 @@
1
+ # Describes a field as a Time field. Currently this means that it has both a time and a date.
2
+ #
3
+ # ==Options
4
+ # * :before [time] - specifies that the value must predate the given time (as object or string)
5
+ # * :after [time] - specifies that the value must postdate the given time (as object or string)
6
+ # * :distance [range] - specifies a range of seconds that provide an minimum and maximum time value to be calculated from the current time.
7
+ class Predicates::Time < Predicates::Base
8
+ # specifies a time that must postdate any valid value
9
+ def before=(val)
10
+ @before = val.is_a?(Time) ? val : Time.parse(val)
11
+ end
12
+ attr_reader :before
13
+
14
+ # specifies a time that must predate any valid value
15
+ def after=(val)
16
+ @after = val.is_a?(Time) ? val : Time.parse(val)
17
+ end
18
+ attr_reader :after
19
+
20
+ # specifies a range of seconds where the lower boundary
21
+ # is the minimum time value and the upper boundary is the
22
+ # maximum time value, both interpreted from the current
23
+ # time.
24
+ attr_accessor :distance
25
+
26
+ def error_message
27
+ @error_message || :time
28
+ end
29
+
30
+ def validate(value, record)
31
+ valid = value.is_a? Time
32
+ valid &&= (value < self.before) if self.before
33
+ valid &&= (value > self.after) if self.after
34
+
35
+ if self.distance
36
+ now = Time.zone.now
37
+ valid &&= (value >= now + self.distance.begin)
38
+ valid &&= (value <= now + self.distance.end)
39
+ end
40
+
41
+ valid
42
+ end
43
+ end
@@ -0,0 +1,71 @@
1
+ # Describes an attribute as being unique, possibly within a certain scope.
2
+ #
3
+ # ==Options
4
+ # * :scope [array] - a list of other fields that define the context for uniqueness. it's like defining a multi-column uniqueness constraint.
5
+ # * :case_sensitive [boolean, default false] - whether case matters for uniqueness.
6
+ #
7
+ # ==Examples
8
+ # field_is_unique :case_sensitive => true
9
+ # field_is_unique :scope => [:other_field, :another_other_field]
10
+ class Predicates::Unique < Predicates::Base
11
+ attr_accessor :case_sensitive
12
+ attr_accessor :scope
13
+
14
+ def initialize(attribute, options = {})
15
+ defaults = {
16
+ :scope => [],
17
+ :case_sensitive => false
18
+ }
19
+ super attribute, defaults.merge(options)
20
+ end
21
+
22
+ def error_message
23
+ @error_message || :taken
24
+ end
25
+
26
+ def validate(value, record)
27
+ klass = record.class
28
+
29
+ # merge all the scope fields with this one. they must all be unique together.
30
+ # no special treatment -- case sensitivity applies to all or none.
31
+ values = [scope].flatten.collect{ |attr| [attr, record.send(attr)] }
32
+ values << [@attribute, value]
33
+
34
+ sql = values.map do |(attr, attr_value)|
35
+ comparison_for(attr, attr_value, klass)
36
+ end
37
+
38
+ unless record.new_record?
39
+ sql << klass.send(:sanitize_sql, ["#{klass.quoted_table_name}.#{klass.primary_key} <> ?", record.id])
40
+ end
41
+
42
+ !klass.where(sql.join(" AND ")).exists?
43
+ end
44
+
45
+ protected
46
+
47
+ def comparison_for(field, value, klass)
48
+ quoted_field = "#{klass.quoted_table_name}.#{klass.connection.quote_column_name(field)}"
49
+
50
+ if klass.columns_hash[field.to_s].text?
51
+ if case_sensitive
52
+ # case sensitive text comparison in any database
53
+ klass.send(:sanitize_sql, ["#{quoted_field} #{klass.connection.case_sensitive_equality_operator} ?", value])
54
+ elsif mysql?(klass.connection)
55
+ # case INsensitive text comparison in mysql - yes this is a database specific optimization. i'm always open to better ways. :)
56
+ klass.send(:sanitize_sql, ["#{quoted_field} = ?", value])
57
+ else
58
+ # case INsensitive text comparison in most databases
59
+ klass.send(:sanitize_sql, ["LOWER(#{quoted_field}) = ?", value.to_s.downcase])
60
+ end
61
+ else
62
+ # non-text comparison
63
+ klass.send(:sanitize_sql, {field => value})
64
+ end
65
+ end
66
+
67
+ def mysql?(connection)
68
+ (defined?(ActiveRecord::ConnectionAdapters::MysqlAdapter) and connection.is_a?(ActiveRecord::ConnectionAdapters::MysqlAdapter)) or
69
+ (defined?(ActiveRecord::ConnectionAdapters::Mysql2Adapter) and connection.is_a?(ActiveRecord::ConnectionAdapters::Mysql2Adapter))
70
+ end
71
+ end
@@ -0,0 +1,62 @@
1
+ require 'uri'
2
+
3
+ # Defines a field as a URL.
4
+ #
5
+ # ==Options
6
+ # :domains [array, default nil] - a whitelist of allowed domains (e.g. ['com', 'net', 'org']). set to nil to allow all domains.
7
+ # :schemes [array, default ['http', 'https']] - a whitelist of allowed schemes. set to nil to allow all schemes.
8
+ # :ports [array, default nil] - a whitelist of allowed ports. set to nil to allow all ports.
9
+ # :allow_ip_address [boolean, default true] - whether to allow ip addresses instead to domain names.
10
+ # :implied_scheme [string, symbol, default 'http'] - what scheme to assume if non is present.
11
+ #
12
+ # ==Examples
13
+ # # if you need an ftp url
14
+ # field_is_an_url :schemes => ['ftp']
15
+ #
16
+ # # if you want to require https
17
+ # field_is_an_url :schemes => ['https'], :implied_scheme => 'https', :ports => [443]
18
+ class Predicates::Url < Predicates::Base
19
+ attr_accessor :domains
20
+ attr_accessor :allow_ip_address
21
+ attr_accessor :schemes
22
+ attr_accessor :ports
23
+ attr_accessor :implied_scheme
24
+
25
+ def initialize(attr, options = {})
26
+ defaults = {
27
+ :allow_ip_address => true,
28
+ :schemes => ['http', 'https'],
29
+ :implied_scheme => 'http'
30
+ }
31
+
32
+ super attr, defaults.merge(options)
33
+ end
34
+
35
+ def error_message
36
+ @error_message || :url
37
+ end
38
+
39
+ def validate(value, record)
40
+ url = URI.parse(value)
41
+ tld = (url.host && url.host.match(/\..+\Z/)) ? url.host.split('.').last : nil
42
+
43
+ valid = true
44
+ valid &&= (!tld.blank?)
45
+ valid &&= (!self.schemes or self.schemes.include? url.scheme)
46
+ valid &&= (!self.domains or self.domains.include? tld)
47
+ valid &&= (!self.ports or self.ports.include? url.port)
48
+ valid &&= (self.allow_ip_address or not url.host =~ /^([0-9]{1,3}\.){3}[0-9]{1,3}$/)
49
+
50
+ valid
51
+ rescue URI::InvalidURIError, URI::InvalidComponentError
52
+ false
53
+ end
54
+
55
+ def normalize(v)
56
+ url = URI.parse(v)
57
+ url = URI.parse("#{self.implied_scheme}://#{v}") if self.implied_scheme and not (url.scheme and url.host)
58
+ url.to_s
59
+ rescue URI::InvalidURIError, URI::InvalidComponentError
60
+ v
61
+ end
62
+ end
@@ -0,0 +1,87 @@
1
+ # Requires that an attribute be in a list of valid USA states.
2
+ # Machine format is the abbreviation, human format is the whole word.
3
+ # List acquired from: http://www.usps.com/ncsc/lookups/abbr_state.txt
4
+ #
5
+ # ==Options
6
+ # * :with_territories [boolean, default false] - whether to allow territories
7
+ class Predicates::UsaState < Predicates::Aliased
8
+ # a boolean for whether to allow united states territories. default is false.
9
+ attr_writer :with_territories
10
+ def with_territories?
11
+ @with_territories ? true : false
12
+ end
13
+
14
+ def options
15
+ @options ||= STATES.merge(with_territories? ? TERRITORIES : {})
16
+ end
17
+ undef_method :options=
18
+
19
+ def error_message
20
+ @error_message || with_territories? ? :us_state_or_territory : :us_state
21
+ end
22
+
23
+ TERRITORIES = {
24
+ 'American Samoa' => 'AS',
25
+ 'Federated States of Micronesia' => 'FM',
26
+ 'Guam' => 'GU',
27
+ 'Marshall Islands' => 'MH',
28
+ 'Northern Mariana Islands' => 'MP',
29
+ 'Palau' => 'PW',
30
+ 'Puerto Rico' => 'PR',
31
+ 'Virgin Islands' => 'VI'
32
+ }
33
+
34
+ STATES = {
35
+ 'Alabama' => 'AL',
36
+ 'Alaska' => 'AK',
37
+ 'Arizona' => 'AZ',
38
+ 'Arkansas' => 'AR',
39
+ 'California' => 'CA',
40
+ 'Colorado' => 'CO',
41
+ 'Connecticut' => 'CT',
42
+ 'Delaware' => 'DE',
43
+ 'District of Columbia' => 'DC',
44
+ 'Florida' => 'FL',
45
+ 'Georgia' => 'GA',
46
+ 'Hawaii' => 'HI',
47
+ 'Idaho' => 'ID',
48
+ 'Illinois' => 'IL',
49
+ 'Indiana' => 'IN',
50
+ 'Iowa' => 'IA',
51
+ 'Kansas' => 'KS',
52
+ 'Kentucky' => 'KY',
53
+ 'Louisiana' => 'LA',
54
+ 'Maine' => 'ME',
55
+ 'Maryland' => 'MD',
56
+ 'Massachusetts' => 'MA',
57
+ 'Michigan' => 'MI',
58
+ 'Minnesota' => 'MN',
59
+ 'Mississippi' => 'MS',
60
+ 'Missouri' => 'MO',
61
+ 'Montana' => 'MT',
62
+ 'Nebraska' => 'NE',
63
+ 'Nevaga' => 'NV',
64
+ 'New Hampshire' => 'NH',
65
+ 'New Jersey' => 'NJ',
66
+ 'New Mexico' => 'NM',
67
+ 'New York' => 'NY',
68
+ 'North Carolina' => 'NC',
69
+ 'North Dakota' => 'ND',
70
+ 'Ohio' => 'OH',
71
+ 'Oklahoma' => 'OK',
72
+ 'Oregon' => 'OR',
73
+ 'Pennsylvania' => 'PA',
74
+ 'Rhode Island' => 'RI',
75
+ 'South Carolina' => 'SC',
76
+ 'South Dakota' => 'SD',
77
+ 'Tennessee' => 'TN',
78
+ 'Texas' => 'TX',
79
+ 'Utah' => 'UT',
80
+ 'Vermont' => 'VT',
81
+ 'Virginia' => 'VA',
82
+ 'Washington' => 'WA',
83
+ 'West Virginia' => 'WV',
84
+ 'Wisconsin' => 'WI',
85
+ 'Wyoming' => 'WY'
86
+ }
87
+ end
@@ -0,0 +1,25 @@
1
+ # A United States zip code.
2
+ # Only validates format, does not attempt any verification for *actual* zip codes.
3
+ # Validates via regular expression.
4
+ #
5
+ # ==Options
6
+ # * :extended [:allowed, :required, or false (default)] - whether to allow (or require!) the extended (+4) zip code format
7
+ class Predicates::UsaZipCode < Predicates::Pattern
8
+ attr_accessor :extended
9
+
10
+ def like
11
+ return @like unless @like.nil?
12
+ pattern = '[0-9]{5}'
13
+ if extended
14
+ pattern += '(-[0-9]{4})'
15
+ pattern += '?' unless extended == :required
16
+ end
17
+
18
+ @like = /\A#{pattern}\Z/
19
+ end
20
+ undef_method :like=
21
+
22
+ def error_message
23
+ @error_message || :us_zip_code
24
+ end
25
+ end