lite-validators 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (84) hide show
  1. checksums.yaml +7 -0
  2. data/.fasterer.yml +19 -0
  3. data/.gitignore +11 -0
  4. data/.rspec +4 -0
  5. data/.rubocop.yml +31 -0
  6. data/.travis.yml +24 -0
  7. data/CHANGELOG.md +11 -0
  8. data/CODE_OF_CONDUCT.md +74 -0
  9. data/Gemfile +6 -0
  10. data/Gemfile.lock +128 -0
  11. data/LICENSE.txt +21 -0
  12. data/README.md +88 -0
  13. data/Rakefile +8 -0
  14. data/_config.yml +1 -0
  15. data/bin/console +15 -0
  16. data/bin/setup +8 -0
  17. data/config/locales/en.yml +23 -0
  18. data/docs/ALPHA.md +29 -0
  19. data/docs/ALPHA_NUMERIC.md +29 -0
  20. data/docs/BASE64.md +18 -0
  21. data/docs/BOOLEAN.md +19 -0
  22. data/docs/COMPARE.md +30 -0
  23. data/docs/COORDINATE.md +27 -0
  24. data/docs/CREDIT_CARD.md +29 -0
  25. data/docs/CSV.md +44 -0
  26. data/docs/CURRENCY.md +18 -0
  27. data/docs/CUSIP.md +20 -0
  28. data/docs/EMAIL.md +26 -0
  29. data/docs/FILE_CONTENT_TYPE.md +26 -0
  30. data/docs/FILE_EXTENSION.md +25 -0
  31. data/docs/FILE_SIZE.md +38 -0
  32. data/docs/HEX.md +18 -0
  33. data/docs/IMEI.md +20 -0
  34. data/docs/IP_ADDRESS.md +26 -0
  35. data/docs/ISBN.md +18 -0
  36. data/docs/ISIN.md +20 -0
  37. data/docs/MAC_ADDRESS.md +21 -0
  38. data/docs/NAME.md +18 -0
  39. data/docs/PASSWORD.md +26 -0
  40. data/docs/PHONE_NUMBER.md +18 -0
  41. data/docs/SEDOL.md +20 -0
  42. data/docs/SLUG.md +18 -0
  43. data/docs/SSN.md +18 -0
  44. data/docs/TIME_ZONE.md +17 -0
  45. data/docs/TYPE.md +28 -0
  46. data/docs/URL.md +28 -0
  47. data/docs/USERNAME.md +18 -0
  48. data/docs/UUID.md +28 -0
  49. data/lib/lite/validators/alpha_numeric_validator.rb +11 -0
  50. data/lib/lite/validators/alpha_validator.rb +34 -0
  51. data/lib/lite/validators/base64_validator.rb +9 -0
  52. data/lib/lite/validators/base_validator.rb +54 -0
  53. data/lib/lite/validators/boolean_validator.rb +15 -0
  54. data/lib/lite/validators/compare_validator.rb +41 -0
  55. data/lib/lite/validators/coordinate_validator.rb +37 -0
  56. data/lib/lite/validators/credit_card_validator.rb +144 -0
  57. data/lib/lite/validators/csv_validator.rb +73 -0
  58. data/lib/lite/validators/currency_validator.rb +7 -0
  59. data/lib/lite/validators/cusip_validator.rb +27 -0
  60. data/lib/lite/validators/email_validator.rb +19 -0
  61. data/lib/lite/validators/file_content_type_validator.rb +38 -0
  62. data/lib/lite/validators/file_extension_validator.rb +41 -0
  63. data/lib/lite/validators/file_size_validator.rb +75 -0
  64. data/lib/lite/validators/hex_validator.rb +7 -0
  65. data/lib/lite/validators/imei_validator.rb +27 -0
  66. data/lib/lite/validators/ip_address_validator.rb +39 -0
  67. data/lib/lite/validators/isbn_validator.rb +16 -0
  68. data/lib/lite/validators/isin_validator.rb +36 -0
  69. data/lib/lite/validators/mac_address_validator.rb +18 -0
  70. data/lib/lite/validators/name_validator.rb +7 -0
  71. data/lib/lite/validators/password_validator.rb +29 -0
  72. data/lib/lite/validators/phone_number_validator.rb +7 -0
  73. data/lib/lite/validators/sedol_validator.rb +23 -0
  74. data/lib/lite/validators/slug_validator.rb +7 -0
  75. data/lib/lite/validators/ssn_validator.rb +7 -0
  76. data/lib/lite/validators/time_zone_validator.rb +13 -0
  77. data/lib/lite/validators/type_validator.rb +41 -0
  78. data/lib/lite/validators/url_validator.rb +61 -0
  79. data/lib/lite/validators/username_validator.rb +7 -0
  80. data/lib/lite/validators/uuid_validator.rb +37 -0
  81. data/lib/lite/validators/version.rb +9 -0
  82. data/lib/lite/validators.rb +12 -0
  83. data/lite-validators.gemspec +51 -0
  84. metadata +265 -0
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ class BaseValidator < ActiveModel::EachValidator
4
+
5
+ def validate_each(record, attribute, value)
6
+ assign_attr_readers(record, attribute, value)
7
+ return if valid?
8
+
9
+ record.errors.add(attribute, *error_message)
10
+ end
11
+
12
+ private
13
+
14
+ attr_reader :record, :attribute, :value
15
+
16
+ def assign_attr_readers(record, attribute, value)
17
+ @record = record
18
+ @attribute = attribute
19
+ @value = value
20
+ end
21
+
22
+ # rubocop:disable Metrics/LineLength
23
+ def assert_valid_option!(name, collection, option: nil)
24
+ option ||= send(name)
25
+
26
+ [*option].each do |option_value|
27
+ next if collection.include?(option_value)
28
+
29
+ raise ArgumentError, "Unknown #{name}: #{option_value.inspect}. Valid options are: #{collection.map(&:inspect).join(', ')}"
30
+ end
31
+ end
32
+ # rubocop:enable Metrics/LineLength
33
+
34
+ def error_message
35
+ [options[:message] || :invalid]
36
+ end
37
+
38
+ def valid?
39
+ valid_length? && valid_attr?
40
+ end
41
+
42
+ def valid_attr?
43
+ valid_regexp?
44
+ end
45
+
46
+ def valid_length?
47
+ !value.to_s.strip.empty?
48
+ end
49
+
50
+ def valid_regexp?
51
+ value.to_s =~ self.class::REGEXP
52
+ end
53
+
54
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ class BooleanValidator < BaseValidator
4
+
5
+ VALUES ||= %w[
6
+ 1 0 t f true false
7
+ ].freeze
8
+
9
+ private
10
+
11
+ def valid_attr?
12
+ VALUES.include?(value.to_s.downcase)
13
+ end
14
+
15
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CompareValidator < BaseValidator
4
+
5
+ CHECKS ||= {
6
+ less_than: :<,
7
+ less_than_or_equal_to: :<=,
8
+ greater_than: :>,
9
+ greater_than_or_equal_to: :>=,
10
+ equal_to: :==,
11
+ not_equal_to: :!=
12
+ }.freeze
13
+
14
+ def validate_each(record, attribute, value)
15
+ assert_valid_to!
16
+ assert_valid_check!
17
+ super
18
+ end
19
+
20
+ private
21
+
22
+ def assert_valid_check!
23
+ assert_valid_option!(:check, CHECKS.keys)
24
+ end
25
+
26
+ def assert_valid_to!
27
+ return if options.key?(:to)
28
+
29
+ raise ArgumentError, 'Missing ":to" attribute for comparison.'
30
+ end
31
+
32
+ def check
33
+ options[:check] || :equal_to
34
+ end
35
+
36
+ def valid_attr?
37
+ other = record.send(options[:to])
38
+ value.send(CHECKS[check], other)
39
+ end
40
+
41
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CoordinateValidator < BaseValidator
4
+
5
+ BOUNDARIES ||= {
6
+ latitude: 90.0,
7
+ longitude: 180.0
8
+ }.freeze
9
+
10
+ def validate_each(record, attribute, value)
11
+ assert_valid_boundary!
12
+ super
13
+ end
14
+
15
+ private
16
+
17
+ def assert_valid_boundary!
18
+ assert_valid_option!(:boundary, BOUNDARIES.keys.push(:pair))
19
+ end
20
+
21
+ def boundary
22
+ options[:boundary] || :pair
23
+ end
24
+
25
+ def valid_attr?
26
+ case boundary
27
+ when :latitude then valid_boundary?(:latitude)
28
+ when :longitude then valid_boundary?(:longitude)
29
+ else valid_boundary?(:latitude, value.first) && valid_boundary?(:longitude, value.last)
30
+ end
31
+ end
32
+
33
+ def valid_boundary?(key, coordinate = nil)
34
+ (coordinate || value).to_f.abs <= BOUNDARIES[key]
35
+ end
36
+
37
+ end
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreditCardValidator < BaseValidator
4
+
5
+ # NOTE: https://en.wikipedia.org/wiki/Payment_card_value#Issuer_identification_value_.28IIN.29
6
+
7
+ PROVIDERS ||= {
8
+ american_express: {
9
+ sizes: [15],
10
+ prefixes: [34, 37]
11
+ },
12
+ dankort: {
13
+ sizes: [16],
14
+ prefixes: [4571, 5019]
15
+ },
16
+ diners_club: {
17
+ sizes: (14..19),
18
+ prefixes: [36, (38..39), (54..55), (300..305), 3095]
19
+ },
20
+ discover: {
21
+ sizes: (16..19),
22
+ prefixes: [(64..65), 6011, (622_126..622_925), (624_000..626_999), (628_200..628_899)]
23
+ },
24
+ interpayment: {
25
+ sizes: (16..19),
26
+ prefixes: [636]
27
+ },
28
+ rupay: {
29
+ sizes: [16],
30
+ prefixes: [60, (6521..6522)]
31
+ },
32
+ jcb: {
33
+ sizes: (16..19),
34
+ prefixes: [(3528..3589)]
35
+ },
36
+ maestro: {
37
+ sizes: (12..19),
38
+ prefixes: [50, (56..69)]
39
+ },
40
+ maestro_uk: {
41
+ sizes: (12..19),
42
+ prefixes: [6759, 676_770, 676_774]
43
+ },
44
+ mastercard: {
45
+ sizes: [16],
46
+ prefixes: [(51..55), (2221..2720)]
47
+ },
48
+ mir: {
49
+ sizes: [16],
50
+ prefixes: [(2200..2204)]
51
+ },
52
+ nps_pridnestrovie: {
53
+ sizes: [16],
54
+ prefixes: [(6_054_740..6_054_744)]
55
+ },
56
+ troy: {
57
+ sizes: [16],
58
+ prefixes: [(979_200..979_289)]
59
+ },
60
+ uatp: {
61
+ sizes: [16],
62
+ prefixes: [1]
63
+ },
64
+ unionpay: {
65
+ sizes: (16..19),
66
+ prefixes: [62]
67
+ },
68
+ verve: {
69
+ sizes: [16, 19],
70
+ prefixes: [(506_099..506_198), (650_002..650_027)]
71
+ },
72
+ visa: {
73
+ sizes: [16],
74
+ prefixes: [4]
75
+ }
76
+ }.freeze
77
+
78
+ def validate_each(record, attribute, value)
79
+ assert_valid_provider!
80
+ super
81
+ end
82
+
83
+ private
84
+
85
+ def assert_valid_provider!
86
+ assert_valid_option!(:provider, PROVIDERS.keys.push(:all))
87
+ end
88
+
89
+ def checksum(value)
90
+ values = digits(value).reverse.map.with_index { |n, i| i.even? ? n * 2 : n }
91
+ total = values.reverse.inject(0) { |a, b| a + digits(b).inject(:+) }
92
+ checksum = 10 - (total % 10)
93
+ checksum == 10 ? 0 : checksum
94
+ end
95
+
96
+ def digits(value)
97
+ value.to_s.chars.map(&:to_i)
98
+ end
99
+
100
+ def encompasses?(subject, value)
101
+ case subject
102
+ when Array then subject.include?(value)
103
+ when Range then subject.cover?(value)
104
+ else subject == value
105
+ end
106
+ end
107
+
108
+ def prefix(subject, value)
109
+ nums = case subject
110
+ when Array, Range then subject.first
111
+ else subject
112
+ end
113
+
114
+ value.to_s[0..(nums.to_s.size - 1)].to_i
115
+ end
116
+
117
+ def provider
118
+ options[:provider] || :all
119
+ end
120
+
121
+ def valid_attr?
122
+ valid_size?(value) && valid_prefix?(value) && valid_checksum?
123
+ end
124
+
125
+ def valid_checksum?
126
+ nums = digits(value)
127
+ nums.pop == checksum(nums.join)
128
+ end
129
+
130
+ def valid_prefix?(value)
131
+ prefixes = PROVIDERS.dig(provider, :prefixes) || PROVIDERS.flat_map { |_, h| h[:prefixes] }
132
+
133
+ prefixes.any? do |subject|
134
+ iin = prefix(subject, value)
135
+ encompasses?(subject, iin)
136
+ end
137
+ end
138
+
139
+ def valid_size?(value)
140
+ sizes = PROVIDERS.dig(provider, :sizes) || (12..19)
141
+ encompasses?(sizes, value.to_s.size)
142
+ end
143
+
144
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'csv'
4
+
5
+ class CsvValidator < FileSizeValidator
6
+
7
+ DIMENSIONS ||= %i[
8
+ columns rows
9
+ ].freeze
10
+
11
+ def validate_each(record, attribute, value)
12
+ assert_valid_dimensions!
13
+ assert_valid_checks!
14
+ assign_attr_readers(record, attribute, csv_dimensions(value.path))
15
+ valid?
16
+ rescue CSV::MalformedCSVError
17
+ record.errors.add(attribute, *error_message)
18
+ end
19
+
20
+ private
21
+
22
+ # rubocop:disable Metrics/LineLength, Style/GuardClause
23
+ def assert_valid_dimensions!
24
+ if dimensions.empty?
25
+ raise ArgumentError, "Missing atleast one dimension of #{DIMENSIONS.map(&:inspect).join(', ')} attribute for comparison."
26
+ else
27
+ assert_valid_option!(:dimensions, DIMENSIONS)
28
+ end
29
+ end
30
+ # rubocop:enable Metrics/LineLength, Style/GuardClause
31
+
32
+ def csv_dimensions(path)
33
+ dimension = { columns: 0, rows: 0 }
34
+
35
+ CSV.foreach(path) do |row|
36
+ dimension[:columns] = row.size if dimension[:rows].zero?
37
+ dimension[:rows] += 1
38
+ end
39
+
40
+ dimension
41
+ end
42
+
43
+ def checks
44
+ dimension_options = options.slice(:columns, :rows).values
45
+ dimension_options.flat_map(&:keys) & CHECKS.keys
46
+ end
47
+
48
+ def dimensions
49
+ options.keys
50
+ end
51
+
52
+ def error_message_for(dimension, check, check_value)
53
+ options[:message] || I18n.t(
54
+ "errors.messages.csv.#{check}",
55
+ error_options(check_value).merge(dimension: dimension)
56
+ )
57
+ end
58
+
59
+ def valid_attr?
60
+ options.slice(*DIMENSIONS).each do |dimension, dimension_value|
61
+ dimension_value.each do |check, check_value|
62
+ validate_check(dimension, check, check_value)
63
+ end
64
+ end
65
+ end
66
+
67
+ def validate_check(dimension, check, check_value)
68
+ return if valid_size?(value[dimension], check, check_value)
69
+
70
+ record.errors.add(attribute, error_message_for(dimension, check, check_value))
71
+ end
72
+
73
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CurrencyValidator < BaseValidator
4
+
5
+ REGEXP ||= /^\d*+(\.\d{1,2})$/.freeze
6
+
7
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CusipValidator < BaseValidator
4
+
5
+ REGEXP ||= /^[0-9A-Z]{9}$/.freeze
6
+
7
+ private
8
+
9
+ def valid_attr?
10
+ valid_regexp? && valid_checksum?
11
+ end
12
+
13
+ # rubocop:disable Metrics/AbcSize
14
+ def valid_checksum?
15
+ digits = value.chars.map { |chr| /[A-Z]/.match?(chr) ? (chr.ord - 55) : chr.to_i }
16
+
17
+ even_values = digits.values_at(*digits.each_index.select(&:even?))
18
+ odd_values = digits.values_at(*digits.each_index.select(&:odd?))
19
+
20
+ values = odd_values.map { |int| int * 2 }.zip(even_values).flatten
21
+ values = values.inject(0) { |sum, int| sum + (int / 10) + int % 10 }
22
+
23
+ ((10 - values) % 10) % 10
24
+ end
25
+ # rubocop:enable Metrics/AbcSize
26
+
27
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ class EmailValidator < BaseValidator
4
+
5
+ REGEXP ||= /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i.freeze
6
+
7
+ private
8
+
9
+ def valid_attr?
10
+ valid_regexp? && valid_domain?
11
+ end
12
+
13
+ def valid_domain?
14
+ return true unless options.key?(:domain)
15
+
16
+ [options[:domain]].flatten.any? { |domain| value.downcase.end_with?(".#{domain.downcase}") }
17
+ end
18
+
19
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ class FileContentTypeValidator < BaseValidator
4
+
5
+ def validate_each(record, attribute, value)
6
+ assert_valid_include_or_exclude!
7
+ assign_attr_readers(record, attribute, value)
8
+ valid?
9
+ end
10
+
11
+ private
12
+
13
+ def assert_valid_include_or_exclude!
14
+ return if options.key?(:include) || options.key?(:exclude)
15
+
16
+ raise ArgumentError, 'Missing ":include" or ":exclude" attribute for comparison.'
17
+ end
18
+
19
+ def valid_attr?
20
+ options.slice(:include, :exclude).each do |option, option_value|
21
+ validate_check(option, option_value)
22
+ end
23
+ end
24
+
25
+ # rubocop:disable Style/CaseEquality
26
+ def valid_content_type?(option, option_value)
27
+ check = [*option_value].any? { |type| type === value.content_type }
28
+ option == :exclude ? !check : check
29
+ end
30
+ # rubocop:enable Style/CaseEquality
31
+
32
+ def validate_check(option, option_value)
33
+ return if valid_content_type?(option, option_value)
34
+
35
+ record.errors.add(attribute, *error_message)
36
+ end
37
+
38
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ class FileExtensionValidator < BaseValidator
4
+
5
+ def validate_each(record, attribute, value)
6
+ assert_valid_include_or_exclude!
7
+ assign_attr_readers(record, attribute, value)
8
+ valid?
9
+ end
10
+
11
+ private
12
+
13
+ def assert_valid_include_or_exclude!
14
+ return if options.key?(:include) || options.key?(:exclude)
15
+
16
+ raise ArgumentError, 'Missing ":include" or ":exclude" attribute for comparison.'
17
+ end
18
+
19
+ def check
20
+ options.key?(:exclude) ? :exclude : :include
21
+ end
22
+
23
+ def include_or_exclude
24
+ values = options[:include] || options[:exclude]
25
+ [*values]
26
+ end
27
+
28
+ # rubocop:disable Metrics/AbcSize
29
+ def valid_attr?
30
+ extension = File.extname(value.original_filename).tr('.', '')
31
+ return false if extension.empty?
32
+
33
+ included = include_or_exclude.any? { |ext| ext.to_s == extension }
34
+ included = !included if check == :exclude
35
+ return if included
36
+
37
+ record.errors.add(attribute, *error_message)
38
+ end
39
+ # rubocop:enable Metrics/AbcSize
40
+
41
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ class FileSizeValidator < BaseValidator
4
+
5
+ CHECKS ||= {
6
+ in: :===,
7
+ less_than: :<,
8
+ less_than_or_equal_to: :<=,
9
+ greater_than: :>,
10
+ greater_than_or_equal_to: :>=,
11
+ equal_to: :==,
12
+ not_equal_to: :!=
13
+ }.freeze
14
+
15
+ def validate_each(record, attribute, value)
16
+ assert_valid_checks!
17
+ assign_attr_readers(record, attribute, value)
18
+ valid?
19
+ end
20
+
21
+ private
22
+
23
+ # rubocop:disable Metrics/LineLength, Style/GuardClause
24
+ def assert_valid_checks!
25
+ if checks.empty?
26
+ raise ArgumentError, "Missing atleast one check of #{CHECKS.keys.map(&:inspect).join(', ')} attribute for comparison."
27
+ else
28
+ assert_valid_option!(:checks, CHECKS.keys)
29
+ end
30
+ end
31
+ # rubocop:enable Metrics/LineLength, Style/GuardClause
32
+
33
+ def checks
34
+ options.keys & CHECKS.keys
35
+ end
36
+
37
+ def error_message_for(option, option_value)
38
+ options[:message] || I18n.t(
39
+ "errors.messages.file_size.#{option}",
40
+ error_options(option_value)
41
+ )
42
+ end
43
+
44
+ def error_options(option_value)
45
+ if option_value.is_a?(Range)
46
+ { min: option_value.min, max: option_value.max }
47
+ else
48
+ { count: option_value }
49
+ end
50
+ end
51
+
52
+ def valid_attr?
53
+ options.slice(*CHECKS.keys).each do |option, option_value|
54
+ validate_check(option, option_value)
55
+ end
56
+ end
57
+
58
+ def valid_size?(size, option, option_value)
59
+ return false if size.nil?
60
+
61
+ if option_value.is_a?(Range)
62
+ option_value.send(CHECKS[option], size)
63
+ else
64
+ size.send(CHECKS[option], option_value)
65
+ end
66
+ end
67
+
68
+ def validate_check(option, option_value)
69
+ size = value.respond_to?(:byte_size) ? value.byte_size : value.size
70
+ return if valid_size?(size, option, option_value)
71
+
72
+ record.errors.add(attribute, error_message_for(option, option_value))
73
+ end
74
+
75
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class HexValidator < BaseValidator
4
+
5
+ REGEXP ||= /^#?([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/.freeze
6
+
7
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ImeiValidator < BaseValidator
4
+
5
+ REGEXP ||= /\A[\d\.\:\-\s]+\z/i.freeze
6
+
7
+ private
8
+
9
+ def valid_attr?
10
+ valid_regexp? && valid_checksum?
11
+ end
12
+
13
+ def valid_checksum?
14
+ number = value.to_s.gsub(/\D/, '').reverse
15
+
16
+ total = 0
17
+ number.chars.each_with_index do |chr, idx|
18
+ result = chr.to_i
19
+ result *= 2 if idx.odd?
20
+ result = (1 + (result - 10)) if result >= 10
21
+ total += result
22
+ end
23
+
24
+ (total % 10).zero?
25
+ end
26
+
27
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'resolv'
4
+
5
+ class IpAddressValidator < BaseValidator
6
+
7
+ REGEXP ||= {
8
+ ipv4: Resolv::IPv4::Regex,
9
+ ipv6: Resolv::IPv6::Regex
10
+ }.freeze
11
+
12
+ def validate_each(record, attribute, value)
13
+ assert_valid_protocol!
14
+ super
15
+ end
16
+
17
+ private
18
+
19
+ def assert_valid_protocol!
20
+ assert_valid_option!(:protocol, REGEXP.keys.push(:any))
21
+ end
22
+
23
+ def protocol
24
+ options[:protocol] || :any
25
+ end
26
+
27
+ def valid_attr?
28
+ case protocol
29
+ when :ipv4 then valid_regexp?(:ipv4)
30
+ when :ipv6 then valid_regexp?(:ipv6)
31
+ else valid_regexp?(:ipv4) || valid_regexp?(:ipv6)
32
+ end
33
+ end
34
+
35
+ def valid_regexp?(key)
36
+ value.to_s =~ REGEXP[key]
37
+ end
38
+
39
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ class IsbnValidator < BaseValidator
4
+
5
+ CHARACTERS ||= %w[
6
+ 0 1 2 3 4 5 6 7 8 9 0 x
7
+ ].freeze
8
+
9
+ private
10
+
11
+ def valid_attr?
12
+ values = value.to_s.gsub(/-| /, '').downcase.chars
13
+ [10, 13].include?(values.size) && values.all? { |chr| CHARACTERS.include?(chr) }
14
+ end
15
+
16
+ end