semantic_attributes 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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