lite-validators 1.0.0

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 (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