yukonisuru 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (96) hide show
  1. checksums.yaml +7 -0
  2. data/.coveralls.yml +1 -0
  3. data/.gitignore +14 -0
  4. data/.rspec +4 -0
  5. data/.travis.yml +13 -0
  6. data/Gemfile +4 -0
  7. data/LICENSE.txt +22 -0
  8. data/README.md +1101 -0
  9. data/Rakefile +1 -0
  10. data/config/locales/en.yml +113 -0
  11. data/lib/yukonisuru.rb +93 -0
  12. data/lib/yukonisuru/matchers/ensure_valid_alpha_format_of.rb +26 -0
  13. data/lib/yukonisuru/matchers/ensure_valid_alpha_numeric_format_of.rb +26 -0
  14. data/lib/yukonisuru/matchers/ensure_valid_base64_format_of.rb +26 -0
  15. data/lib/yukonisuru/matchers/ensure_valid_boolean_format_of.rb +26 -0
  16. data/lib/yukonisuru/matchers/ensure_valid_credit_card_format_of.rb +26 -0
  17. data/lib/yukonisuru/matchers/ensure_valid_currency_format_of.rb +26 -0
  18. data/lib/yukonisuru/matchers/ensure_valid_cusip_format_of.rb +26 -0
  19. data/lib/yukonisuru/matchers/ensure_valid_email_format_of.rb +26 -0
  20. data/lib/yukonisuru/matchers/ensure_valid_equality_matcher_of.rb +40 -0
  21. data/lib/yukonisuru/matchers/ensure_valid_gtin_format_of.rb +26 -0
  22. data/lib/yukonisuru/matchers/ensure_valid_hex_format_of.rb +26 -0
  23. data/lib/yukonisuru/matchers/ensure_valid_imei_format_of.rb +26 -0
  24. data/lib/yukonisuru/matchers/ensure_valid_ip_format_of.rb +26 -0
  25. data/lib/yukonisuru/matchers/ensure_valid_isbn_format_of.rb +26 -0
  26. data/lib/yukonisuru/matchers/ensure_valid_isin_format_of.rb +26 -0
  27. data/lib/yukonisuru/matchers/ensure_valid_latitude_format_of.rb +26 -0
  28. data/lib/yukonisuru/matchers/ensure_valid_longitude_format_of.rb +26 -0
  29. data/lib/yukonisuru/matchers/ensure_valid_mac_address_format_of.rb +26 -0
  30. data/lib/yukonisuru/matchers/ensure_valid_name_format_of.rb +26 -0
  31. data/lib/yukonisuru/matchers/ensure_valid_password_format_of.rb +26 -0
  32. data/lib/yukonisuru/matchers/ensure_valid_phone_format_of.rb +26 -0
  33. data/lib/yukonisuru/matchers/ensure_valid_sedol_format_of.rb +26 -0
  34. data/lib/yukonisuru/matchers/ensure_valid_slug_format_of.rb +26 -0
  35. data/lib/yukonisuru/matchers/ensure_valid_ssn_format_of.rb +26 -0
  36. data/lib/yukonisuru/matchers/ensure_valid_url_format_of.rb +26 -0
  37. data/lib/yukonisuru/matchers/ensure_valid_username_format_of.rb +26 -0
  38. data/lib/yukonisuru/matchers/ensure_valid_uuid_format_of.rb +26 -0
  39. data/lib/yukonisuru/validators/alpha_numeric_validator.rb +31 -0
  40. data/lib/yukonisuru/validators/alpha_validator.rb +31 -0
  41. data/lib/yukonisuru/validators/base64_validator.rb +9 -0
  42. data/lib/yukonisuru/validators/boolean_validator.rb +9 -0
  43. data/lib/yukonisuru/validators/credit_card_validator.rb +133 -0
  44. data/lib/yukonisuru/validators/currency_validator.rb +23 -0
  45. data/lib/yukonisuru/validators/cusip_validator.rb +33 -0
  46. data/lib/yukonisuru/validators/email_validator.rb +25 -0
  47. data/lib/yukonisuru/validators/equality_validator.rb +27 -0
  48. data/lib/yukonisuru/validators/gtin_validator.rb +59 -0
  49. data/lib/yukonisuru/validators/hex_validator.rb +9 -0
  50. data/lib/yukonisuru/validators/imei_validator.rb +37 -0
  51. data/lib/yukonisuru/validators/ip_validator.rb +9 -0
  52. data/lib/yukonisuru/validators/isbn_validator.rb +24 -0
  53. data/lib/yukonisuru/validators/isin_validator.rb +38 -0
  54. data/lib/yukonisuru/validators/latitude_validator.rb +9 -0
  55. data/lib/yukonisuru/validators/longitude_validator.rb +9 -0
  56. data/lib/yukonisuru/validators/mac_address_validator.rb +24 -0
  57. data/lib/yukonisuru/validators/name_validator.rb +9 -0
  58. data/lib/yukonisuru/validators/password_validator.rb +23 -0
  59. data/lib/yukonisuru/validators/phone_validator.rb +9 -0
  60. data/lib/yukonisuru/validators/sedol_validator.rb +32 -0
  61. data/lib/yukonisuru/validators/slug_validator.rb +9 -0
  62. data/lib/yukonisuru/validators/ssn_validator.rb +9 -0
  63. data/lib/yukonisuru/validators/url_validator.rb +36 -0
  64. data/lib/yukonisuru/validators/username_validator.rb +9 -0
  65. data/lib/yukonisuru/validators/uuid_validator.rb +28 -0
  66. data/lib/yukonisuru/version.rb +3 -0
  67. data/spec/lib/alpha_numeric_validator_spec.rb +91 -0
  68. data/spec/lib/alpha_validator_spec.rb +182 -0
  69. data/spec/lib/base64_validator_spec.rb +33 -0
  70. data/spec/lib/boolean_validator_spec.rb +35 -0
  71. data/spec/lib/credit_card_validator_spec.rb +686 -0
  72. data/spec/lib/currency_validator_spec.rb +63 -0
  73. data/spec/lib/cusip_validator_spec.rb +27 -0
  74. data/spec/lib/email_validator_spec.rb +109 -0
  75. data/spec/lib/equality_validator_spec.rb +334 -0
  76. data/spec/lib/gtin_validator_spec.rb +101 -0
  77. data/spec/lib/hex_validator_spec.rb +73 -0
  78. data/spec/lib/imei_validator_spec.rb +41 -0
  79. data/spec/lib/ip_validator_spec.rb +33 -0
  80. data/spec/lib/isbn_validator_spec.rb +41 -0
  81. data/spec/lib/isin_validator_spec.rb +35 -0
  82. data/spec/lib/latitude_validator_spec.rb +31 -0
  83. data/spec/lib/longitude_validator_spec.rb +31 -0
  84. data/spec/lib/mac_address_validator_spec.rb +54 -0
  85. data/spec/lib/name_validator_spec.rb +39 -0
  86. data/spec/lib/password_validator_spec.rb +87 -0
  87. data/spec/lib/phone_validator_spec.rb +42 -0
  88. data/spec/lib/sedol_validator_spec.rb +31 -0
  89. data/spec/lib/slug_validator_spec.rb +41 -0
  90. data/spec/lib/ssn_validator_spec.rb +36 -0
  91. data/spec/lib/url_validator_spec.rb +109 -0
  92. data/spec/lib/username_validator_spec.rb +37 -0
  93. data/spec/lib/uuid_validator_spec.rb +157 -0
  94. data/spec/spec_helper.rb +12 -0
  95. data/yukonisuru.gemspec +29 -0
  96. metadata +264 -0
@@ -0,0 +1,26 @@
1
+ RSpec::Matchers.define :ensure_valid_uuid_format_of do |attribute|
2
+ match do |model|
3
+ model.send("#{attribute}=", "1234uuid")
4
+ model.valid?
5
+
6
+ if model.errors.has_key?(attribute)
7
+ model.errors[attribute].include?(I18n.t('yukonisuru.errors.messages.uuid'))
8
+ end
9
+ end
10
+
11
+ failure_message do |model|
12
+ I18n.t(
13
+ 'yukonisuru.matchers.ensure_valid_uuid_format_of.failure_message_for_should',
14
+ attr: attribute.inspect,
15
+ model: model.class.name
16
+ )
17
+ end
18
+
19
+ failure_message_when_negated do |model|
20
+ I18n.t(
21
+ 'yukonisuru.matchers.ensure_valid_uuid_format_of.failure_message_for_should_not',
22
+ attr: attribute.inspect,
23
+ model: model.class.name
24
+ )
25
+ end
26
+ end
@@ -0,0 +1,31 @@
1
+ class AlphaNumericValidator < ActiveModel::EachValidator
2
+
3
+ def validate_each(record, attribute, value)
4
+ unless valid?(value, options)
5
+ record.errors[attribute] << (options[:message] || I18n.t('yukonisuru.errors.messages.alpha_numeric'))
6
+ end
7
+ end
8
+
9
+ private
10
+
11
+ def valid_format?(value, options)
12
+ case options[:case]
13
+ when :lower
14
+ options[:strict] ? (value =~ /^[a-z0-9]+$/) : (value =~ /^[a-z0-9 ]+$/)
15
+ when :upper
16
+ options[:strict] ? (value =~ /^[A-Z0-9]+$/) : (value =~ /^[A-Z0-9 ]+$/)
17
+ else
18
+ options[:strict] ? (value =~ /^[A-Za-z0-9]+$/i) : (value =~ /^[A-Za-z0-9 ]+$/i)
19
+ end
20
+ end
21
+
22
+ def valid_length?(value)
23
+ value != ' '
24
+ end
25
+
26
+ def valid?(value, options)
27
+ valid_format?(value, options) &&
28
+ valid_length?(value)
29
+ end
30
+
31
+ end
@@ -0,0 +1,31 @@
1
+ class AlphaValidator < ActiveModel::EachValidator
2
+
3
+ def validate_each(record, attribute, value)
4
+ unless valid?(value, options)
5
+ record.errors[attribute] << (options[:message] || I18n.t('yukonisuru.errors.messages.alpha'))
6
+ end
7
+ end
8
+
9
+ private
10
+
11
+ def valid_format?(value, options)
12
+ case options[:case]
13
+ when :lower
14
+ options[:strict] ? (value =~ /^[a-z]+$/) : (value =~ /^[a-z ]+$/)
15
+ when :upper
16
+ options[:strict] ? (value =~ /^[A-Z]+$/) : (value =~ /^[A-Z ]+$/)
17
+ else
18
+ options[:strict] ? (value =~ /^[A-Za-z]+$/i) : (value =~ /^[A-Za-z ]+$/i)
19
+ end
20
+ end
21
+
22
+ def valid_length?(value)
23
+ value != ' '
24
+ end
25
+
26
+ def valid?(value, options)
27
+ valid_format?(value, options) &&
28
+ valid_length?(value)
29
+ end
30
+
31
+ end
@@ -0,0 +1,9 @@
1
+ class Base64Validator < ActiveModel::EachValidator
2
+
3
+ def validate_each(record, attribute, value)
4
+ unless value =~ /^(?:[A-Za-z0-9+$]{4})*(?:[A-Za-z0-9+$]{2}==|[A-Za-z0-9+$]{3}=|[A-Za-z0-9+$]{4})$/
5
+ record.errors[attribute] << (options[:message] || I18n.t('yukonisuru.errors.messages.base64'))
6
+ end
7
+ end
8
+
9
+ end
@@ -0,0 +1,9 @@
1
+ class BooleanValidator < ActiveModel::EachValidator
2
+
3
+ def validate_each(record, attribute, value)
4
+ unless [true, false, 1, 0].include?(value)
5
+ record.errors[attribute] << (options[:message] || I18n.t('yukonisuru.errors.messages.boolean'))
6
+ end
7
+ end
8
+
9
+ end
@@ -0,0 +1,133 @@
1
+ class CreditCardValidator < ActiveModel::EachValidator
2
+
3
+ def validate_each(record, attribute, value)
4
+ unless valid?(value, options)
5
+ record.errors[attribute] << (options[:message] || I18n.t('yukonisuru.errors.messages.credit_card'))
6
+ end
7
+ end
8
+
9
+ private
10
+
11
+ DEFAULT_LENGTHS = {
12
+ american_express: [15],
13
+ diners_club: [14, 16],
14
+ discover: [16],
15
+ jcb: [16],
16
+ laser: [16, 17, 18, 19],
17
+ maestro: [12, 13, 14, 15, 16, 17, 18, 19],
18
+ mastercard: [16],
19
+ solo: [16, 18, 19],
20
+ unionpay: [16, 17, 18, 19],
21
+ visa: [16]
22
+ }
23
+
24
+ DEFAULT_PREFIXES = {
25
+ american_express: ['34', '37'],
26
+ diners_club: ['300', '301', '302', '303', '304', '305', '36', '54', '55'],
27
+ discover: [
28
+ '6011', '622126', '622127', '622128', '622129', '62213',
29
+ '62214', '62215', '62216', '62217', '62218', '62219',
30
+ '6222', '6223', '6224', '6225', '6226', '6227', '6228',
31
+ '62290', '62291', '622920', '622921', '622922', '622923',
32
+ '622924', '622925', '644', '645', '646', '647', '648',
33
+ '649', '65'
34
+ ],
35
+ jcb: ['3528', '3529', '353', '354', '355', '356', '357', '358'],
36
+ laser: ['6304', '6706', '6771', '6709'],
37
+ maestro: [
38
+ '5018', '5020', '5038', '6304', '6759', '6761', '6762',
39
+ '6763', '6764', '6765', '6766'
40
+ ],
41
+ mastercard: ['51', '52', '53', '54', '55'],
42
+ solo: ['6334', '6767'],
43
+ unionpay: [
44
+ '622126', '622127', '622128', '622129', '62213', '62214',
45
+ '62215', '62216', '62217', '62218', '62219', '6222', '6223',
46
+ '6224', '6225', '6226', '6227', '6228', '62290', '62291',
47
+ '622920', '622921', '622922', '622923', '622924', '622925'
48
+ ],
49
+ visa: ['4']
50
+ }
51
+
52
+ def valid_format?(value, options)
53
+ if options[:strict]
54
+ value =~ /^[0-9]+$/
55
+ else
56
+ value =~ /^[0-9 -.]+$/
57
+ end
58
+ end
59
+
60
+ def valid_length?(value, options)
61
+ value_size_range = DEFAULT_LENGTHS.values.flatten.uniq.sort
62
+ value_size = value.size
63
+
64
+ case options[:card]
65
+ when :american_express, :amex
66
+ DEFAULT_LENGTHS[:american_express].include?(value_size)
67
+ when :diners_club
68
+ DEFAULT_LENGTHS[:diners_club].include?(value_size)
69
+ when :discover
70
+ DEFAULT_LENGTHS[:discover].include?(value_size)
71
+ when :jcb
72
+ DEFAULT_LENGTHS[:jcb].include?(value_size)
73
+ when :laser
74
+ DEFAULT_LENGTHS[:laser].include?(value_size)
75
+ when :maestro
76
+ DEFAULT_LENGTHS[:maestro].include?(value_size)
77
+ when :mastercard
78
+ DEFAULT_LENGTHS[:mastercard].include?(value_size)
79
+ when :solo
80
+ DEFAULT_LENGTHS[:solo].include?(value_size)
81
+ when :unionpay
82
+ DEFAULT_LENGTHS[:unionpay].include?(value_size)
83
+ when :visa
84
+ DEFAULT_LENGTHS[:visa].include?(value_size)
85
+ else
86
+ value_size.between?(value_size_range.first, value_size_range.last)
87
+ end
88
+ end
89
+
90
+ def valid_prefix?(value, options)
91
+ value_size = value.size
92
+
93
+ case options[:card]
94
+ when :american_express, :amex
95
+ DEFAULT_PREFIXES[:american_express].any? { |prefix| value.start_with?(prefix) }
96
+ when :diners_club
97
+ DEFAULT_PREFIXES[:diners_club].any? { |prefix| value.start_with?(prefix) }
98
+ when :discover
99
+ DEFAULT_PREFIXES[:discover].any? { |prefix| value.start_with?(prefix) }
100
+ when :jcb
101
+ DEFAULT_PREFIXES[:jcb].any? { |prefix| value.start_with?(prefix) }
102
+ when :laser
103
+ DEFAULT_PREFIXES[:laser].any? { |prefix| value.start_with?(prefix) }
104
+ when :maestro
105
+ DEFAULT_PREFIXES[:maestro].any? { |prefix| value.start_with?(prefix) }
106
+ when :mastercard
107
+ DEFAULT_PREFIXES[:mastercard].any? { |prefix| value.start_with?(prefix) }
108
+ when :solo
109
+ DEFAULT_PREFIXES[:solo].any? { |prefix| value.start_with?(prefix) }
110
+ when :unionpay
111
+ DEFAULT_PREFIXES[:unionpay].any? { |prefix| value.start_with?(prefix) }
112
+ when :visa
113
+ DEFAULT_PREFIXES[:visa].any? { |prefix| value.start_with?(prefix) }
114
+ else
115
+ responses = []
116
+ DEFAULT_LENGTHS.each do |key, values|
117
+ if values.include?(value_size)
118
+ responses << DEFAULT_PREFIXES[key].any? { |prefix| value.start_with?(prefix) }
119
+ end
120
+ end
121
+ responses.include?(true)
122
+ end
123
+ end
124
+
125
+ def valid?(value, options)
126
+ striped_value = value.to_s.gsub(/\D/, '')
127
+
128
+ valid_format?(value, options) &&
129
+ valid_length?(striped_value, options) &&
130
+ valid_prefix?(striped_value, options)
131
+ end
132
+
133
+ end
@@ -0,0 +1,23 @@
1
+ class CurrencyValidator < ActiveModel::EachValidator
2
+
3
+ def validate_each(record, attribute, value)
4
+ unless valid?(value, options)
5
+ record.errors[attribute] << (options[:message] || I18n.t('yukonisuru.errors.messages.currency'))
6
+ end
7
+ end
8
+
9
+ private
10
+
11
+ def valid_format?(value, options)
12
+ if options[:strict]
13
+ value =~ /^\d+(\.\d{2})$/
14
+ else
15
+ value =~ /^\d*+(\.\d{1,2})$/
16
+ end
17
+ end
18
+
19
+ def valid?(value, options)
20
+ valid_format?(value, options)
21
+ end
22
+
23
+ end
@@ -0,0 +1,33 @@
1
+ class CusipValidator < ActiveModel::EachValidator
2
+
3
+ def validate_each(record, attribute, value)
4
+ unless valid?(value.to_s)
5
+ record.errors[attribute] << (options[:message] || I18n.t('yukonisuru.errors.messages.cusip'))
6
+ end
7
+ end
8
+
9
+ private
10
+
11
+ def valid_format?(value)
12
+ value =~ /^[0-9A-Z]{9}$/
13
+ end
14
+
15
+ def valid_checksum?(value)
16
+ digits = value.split('').map { |i| i.match(/[A-Z]/) ? (i.ord - 55) : i.to_i }
17
+ even_values = digits.values_at(* digits.each_index.select { |i| i.even? })
18
+ odd_values = digits.values_at(* digits.each_index.select { |i| i.odd? })
19
+ values = odd_values.map { |i| i * 2 }.zip(even_values).flatten
20
+
21
+ values = values.inject(0) do |sum, i|
22
+ sum += (i / 10) + i % 10
23
+ end
24
+
25
+ ((10 - values) % 10) % 10
26
+ end
27
+
28
+ def valid?(value)
29
+ valid_format?(value) &&
30
+ valid_checksum?(value)
31
+ end
32
+
33
+ end
@@ -0,0 +1,25 @@
1
+ class EmailValidator < ActiveModel::EachValidator
2
+
3
+ def validate_each(record, attribute, value)
4
+ unless valid?(value, options)
5
+ record.errors[attribute] << (options[:message] || I18n.t('yukonisuru.errors.messages.email'))
6
+ end
7
+ end
8
+
9
+ private
10
+
11
+ def valid_format?(value)
12
+ value =~ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i
13
+ end
14
+
15
+ def valid_domain?(value, options)
16
+ value_downcased = value.downcase
17
+ options.empty? || options.any? { |domain| value_downcased.end_with?(".#{domain.downcase}") }
18
+ end
19
+
20
+ def valid?(email, options)
21
+ valid_format?(email) &&
22
+ valid_domain?(email, [*(options[:domain])])
23
+ end
24
+
25
+ end
@@ -0,0 +1,27 @@
1
+ class EqualityValidator < ActiveModel::EachValidator
2
+
3
+ def validate_each(record, attribute, value)
4
+ to_value = options[:to]
5
+ operator = OPERATORS.fetch(options[:operator])
6
+
7
+ if (options[:operator] == :less_than) || (options[:operator] == :greater_than)
8
+ raise Exception if to_value.nil? || operator.nil?
9
+ end
10
+
11
+ unless value.send(operator, record.send(to_value.to_sym))
12
+ record.errors[attribute] << (options[:message] || I18n.t('yukonisuru.errors.messages.equality', attr: to_value, operator: operator))
13
+ end
14
+ end
15
+
16
+ private
17
+
18
+ OPERATORS = {
19
+ less_than: :<,
20
+ less_than_or_equal_to: :<=,
21
+ greater_than: :>,
22
+ greater_than_or_equal_to: :>=,
23
+ equal_to: :==,
24
+ not_equal_to: :!=
25
+ }
26
+
27
+ end
@@ -0,0 +1,59 @@
1
+ class GtinValidator < ActiveModel::EachValidator
2
+
3
+ def validate_each(record, attribute, value)
4
+ unless valid?(value, options)
5
+ record.errors[attribute] << (options[:message] || I18n.t('yukonisuru.errors.messages.gtin'))
6
+ end
7
+ end
8
+
9
+ private
10
+
11
+ def includes_key?(value, options)
12
+ value.any? { |key| options.keys.include?(key) }
13
+ end
14
+
15
+ def valid_format?(value, options)
16
+ if options[:strict]
17
+ value =~ /^[0-9]+$/
18
+ else
19
+ value =~ /^[0-9 ]+$/
20
+ end
21
+ end
22
+
23
+ def valid_length?(value, options)
24
+ value_size = value.size
25
+
26
+ case options[:format]
27
+ when :ean_8, :gtin_8, :ucc_8
28
+ value_size == 8
29
+ when :gtin_12, :upc, :upc_a
30
+ value_size == 12
31
+ when :ean, :ean_13, :gtin_13, :ucc_13
32
+ value_size == 13
33
+ when :gtin_14, :ucc_14
34
+ value_size == 14
35
+ else
36
+ [8, 12, 13, 14].include?(value_size)
37
+ end
38
+ end
39
+
40
+ def valid_checksum?(value)
41
+ reversed_values = value.reverse
42
+ odd = even = 0
43
+
44
+ (1..(reversed_values.length - 1)).each do |i|
45
+ i.even? ? (even += reversed_values[i].chr.to_i) : (odd += reversed_values[i].chr.to_i)
46
+ end
47
+
48
+ reversed_values.first.chr.to_i == (0 == (odd * 3 + even) % 10 ? 0 : 10 - (odd * 3 + even) % 10)
49
+ end
50
+
51
+ def valid?(value, options)
52
+ striped_value = value.to_s.gsub(/\D/, '')
53
+
54
+ valid_format?(value, options) &&
55
+ valid_length?(striped_value, options) &&
56
+ valid_checksum?(striped_value)
57
+ end
58
+
59
+ end
@@ -0,0 +1,9 @@
1
+ class HexValidator < ActiveModel::EachValidator
2
+
3
+ def validate_each(record, attribute, value)
4
+ unless value =~ /^#?([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/
5
+ record.errors[attribute] << (options[:message] || I18n.t('yukonisuru.errors.messages.hex'))
6
+ end
7
+ end
8
+
9
+ end
@@ -0,0 +1,37 @@
1
+ class ImeiValidator < ActiveModel::EachValidator
2
+
3
+ def validate_each(record, attribute, value)
4
+ unless valid?(value.to_s)
5
+ record.errors[attribute] << (options[:message] || I18n.t('yukonisuru.errors.messages.imei'))
6
+ end
7
+ end
8
+
9
+ private
10
+
11
+ def valid_format?(value)
12
+ value =~ /\A[\d\.\:\-\s]+\z/i
13
+ end
14
+
15
+ def valid_luhn?(value)
16
+ numbers = value.gsub(/\D/, '').reverse
17
+
18
+ sum, i = 0, 0
19
+
20
+ numbers.each_char do |ch|
21
+ n = ch.to_i
22
+ n *= 2 if i.odd?
23
+ n = 1 + (n - 10) if n >= 10
24
+
25
+ sum += n
26
+ i += 1
27
+ end
28
+
29
+ (sum % 10).zero?
30
+ end
31
+
32
+ def valid?(value)
33
+ valid_format?(value) &&
34
+ valid_luhn?(value)
35
+ end
36
+
37
+ end