active_storage_validations 1.1.2 → 1.1.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (28) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +86 -59
  3. data/lib/active_storage_validations/aspect_ratio_validator.rb +47 -22
  4. data/lib/active_storage_validations/attached_validator.rb +12 -3
  5. data/lib/active_storage_validations/concerns/errorable.rb +38 -0
  6. data/lib/active_storage_validations/concerns/symbolizable.rb +8 -6
  7. data/lib/active_storage_validations/content_type_validator.rb +41 -6
  8. data/lib/active_storage_validations/dimension_validator.rb +15 -15
  9. data/lib/active_storage_validations/limit_validator.rb +44 -7
  10. data/lib/active_storage_validations/matchers/aspect_ratio_validator_matcher.rb +128 -0
  11. data/lib/active_storage_validations/matchers/attached_validator_matcher.rb +20 -23
  12. data/lib/active_storage_validations/matchers/concerns/active_storageable.rb +17 -0
  13. data/lib/active_storage_validations/matchers/concerns/allow_blankable.rb +26 -0
  14. data/lib/active_storage_validations/matchers/concerns/contextable.rb +35 -0
  15. data/lib/active_storage_validations/matchers/concerns/messageable.rb +26 -0
  16. data/lib/active_storage_validations/matchers/concerns/rspecable.rb +25 -0
  17. data/lib/active_storage_validations/matchers/concerns/validatable.rb +45 -43
  18. data/lib/active_storage_validations/matchers/content_type_validator_matcher.rb +39 -25
  19. data/lib/active_storage_validations/matchers/dimension_validator_matcher.rb +61 -44
  20. data/lib/active_storage_validations/matchers/size_validator_matcher.rb +41 -24
  21. data/lib/active_storage_validations/matchers.rb +1 -0
  22. data/lib/active_storage_validations/metadata.rb +42 -28
  23. data/lib/active_storage_validations/processable_image_validator.rb +14 -5
  24. data/lib/active_storage_validations/size_validator.rb +7 -6
  25. data/lib/active_storage_validations/version.rb +1 -1
  26. data/lib/active_storage_validations.rb +1 -1
  27. metadata +9 -3
  28. data/lib/active_storage_validations/error_handler.rb +0 -21
@@ -1,12 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'concerns/errorable.rb'
3
4
  require_relative 'concerns/symbolizable.rb'
4
5
  require_relative 'metadata.rb'
5
6
 
6
7
  module ActiveStorageValidations
7
8
  class DimensionValidator < ActiveModel::EachValidator # :nodoc
8
9
  include OptionProcUnfolding
9
- include ErrorHandler
10
+ include Errorable
10
11
  include Symbolizable
11
12
 
12
13
  AVAILABLE_CHECKS = %i[width height min max].freeze
@@ -46,7 +47,6 @@ module ActiveStorageValidations
46
47
  flat_options
47
48
  end
48
49
 
49
-
50
50
  def check_validity!
51
51
  unless AVAILABLE_CHECKS.any? { |argument| options.key?(argument) }
52
52
  raise ArgumentError, 'You must pass either :width, :height, :min or :max to the validator'
@@ -64,7 +64,7 @@ module ActiveStorageValidations
64
64
  files = Array.wrap(changes.is_a?(ActiveStorage::Attached::Changes::CreateMany) ? changes.attachables : changes.attachable)
65
65
  files.each do |file|
66
66
  metadata = Metadata.new(file).metadata
67
- next if is_valid?(record, attribute, metadata)
67
+ next if is_valid?(record, attribute, file, metadata)
68
68
  break
69
69
  end
70
70
  end
@@ -78,19 +78,19 @@ module ActiveStorageValidations
78
78
  # Analyze file first if not analyzed to get all required metadata.
79
79
  file.analyze; file.reload unless file.analyzed?
80
80
  metadata = file.metadata rescue {}
81
- next if is_valid?(record, attribute, metadata)
81
+ next if is_valid?(record, attribute, file, metadata)
82
82
  break
83
83
  end
84
84
  end
85
85
  end
86
86
 
87
87
 
88
- def is_valid?(record, attribute, file_metadata)
88
+ def is_valid?(record, attribute, file, metadata)
89
89
  flat_options = process_options(record)
90
- errors_options = initialize_error_options(options)
90
+ errors_options = initialize_error_options(options, file)
91
91
 
92
92
  # Validation fails unless file metadata contains valid width and height.
93
- if file_metadata[:width].to_i <= 0 || file_metadata[:height].to_i <= 0
93
+ if metadata[:width].to_i <= 0 || metadata[:height].to_i <= 0
94
94
  add_error(record, attribute, :image_metadata_missing, **errors_options)
95
95
  return false
96
96
  end
@@ -98,8 +98,8 @@ module ActiveStorageValidations
98
98
  # Validation based on checks :min and :max (:min, :max has higher priority to :width, :height).
99
99
  if flat_options[:min] || flat_options[:max]
100
100
  if flat_options[:min] && (
101
- (flat_options[:width][:min] && file_metadata[:width] < flat_options[:width][:min]) ||
102
- (flat_options[:height][:min] && file_metadata[:height] < flat_options[:height][:min])
101
+ (flat_options[:width][:min] && metadata[:width] < flat_options[:width][:min]) ||
102
+ (flat_options[:height][:min] && metadata[:height] < flat_options[:height][:min])
103
103
  )
104
104
  errors_options[:width] = flat_options[:width][:min]
105
105
  errors_options[:height] = flat_options[:height][:min]
@@ -108,8 +108,8 @@ module ActiveStorageValidations
108
108
  return false
109
109
  end
110
110
  if flat_options[:max] && (
111
- (flat_options[:width][:max] && file_metadata[:width] > flat_options[:width][:max]) ||
112
- (flat_options[:height][:max] && file_metadata[:height] > flat_options[:height][:max])
111
+ (flat_options[:width][:max] && metadata[:width] > flat_options[:width][:max]) ||
112
+ (flat_options[:height][:max] && metadata[:height] > flat_options[:height][:max])
113
113
  )
114
114
  errors_options[:width] = flat_options[:width][:max]
115
115
  errors_options[:height] = flat_options[:height][:max]
@@ -125,7 +125,7 @@ module ActiveStorageValidations
125
125
  [:width, :height].each do |length|
126
126
  next unless flat_options[length]
127
127
  if flat_options[length].is_a?(Hash)
128
- if flat_options[length][:in] && (file_metadata[length] < flat_options[length][:min] || file_metadata[length] > flat_options[length][:max])
128
+ if flat_options[length][:in] && (metadata[length] < flat_options[length][:min] || metadata[length] > flat_options[length][:max])
129
129
  error_type = :"dimension_#{length}_inclusion"
130
130
  errors_options[:min] = flat_options[length][:min]
131
131
  errors_options[:max] = flat_options[length][:max]
@@ -133,13 +133,13 @@ module ActiveStorageValidations
133
133
  add_error(record, attribute, error_type, **errors_options)
134
134
  width_or_height_invalid = true
135
135
  else
136
- if flat_options[length][:min] && file_metadata[length] < flat_options[length][:min]
136
+ if flat_options[length][:min] && metadata[length] < flat_options[length][:min]
137
137
  error_type = :"dimension_#{length}_greater_than_or_equal_to"
138
138
  errors_options[:length] = flat_options[length][:min]
139
139
 
140
140
  add_error(record, attribute, error_type, **errors_options)
141
141
  width_or_height_invalid = true
142
- elsif flat_options[length][:max] && file_metadata[length] > flat_options[length][:max]
142
+ elsif flat_options[length][:max] && metadata[length] > flat_options[length][:max]
143
143
  error_type = :"dimension_#{length}_less_than_or_equal_to"
144
144
  errors_options[:length] = flat_options[length][:max]
145
145
 
@@ -148,7 +148,7 @@ module ActiveStorageValidations
148
148
  end
149
149
  end
150
150
  else
151
- if file_metadata[length] != flat_options[length]
151
+ if metadata[length] != flat_options[length]
152
152
  error_type = :"dimension_#{length}_equal_to"
153
153
  errors_options[:length] = flat_options[length]
154
154
 
@@ -1,32 +1,38 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'concerns/errorable.rb'
3
4
  require_relative 'concerns/symbolizable.rb'
4
5
 
5
6
  module ActiveStorageValidations
6
7
  class LimitValidator < ActiveModel::EachValidator # :nodoc:
7
8
  include OptionProcUnfolding
8
- include ErrorHandler
9
+ include Errorable
9
10
  include Symbolizable
10
11
 
11
12
  AVAILABLE_CHECKS = %i[max min].freeze
13
+ ERROR_TYPES = %i[
14
+ limit_out_of_range
15
+ ].freeze
12
16
 
13
17
  def check_validity!
14
- unless AVAILABLE_CHECKS.any? { |argument| options.key?(argument) }
15
- raise ArgumentError, 'You must pass either :max or :min to the validator'
16
- end
18
+ ensure_at_least_one_validator_option
19
+ ensure_arguments_validity
17
20
  end
18
21
 
19
22
  def validate_each(record, attribute, _)
20
23
  files = Array.wrap(record.send(attribute)).reject { |file| file.blank? }.compact.uniq
21
24
  flat_options = unfold_procs(record, self.options, AVAILABLE_CHECKS)
25
+
26
+ return true if files_count_valid?(files.count, flat_options)
27
+
22
28
  errors_options = initialize_error_options(options)
23
29
  errors_options[:min] = flat_options[:min]
24
30
  errors_options[:max] = flat_options[:max]
25
-
26
- return true if files_count_valid?(files.count, flat_options)
27
- add_error(record, attribute, :limit_out_of_range, **errors_options)
31
+ add_error(record, attribute, ERROR_TYPES.first, **errors_options)
28
32
  end
29
33
 
34
+ private
35
+
30
36
  def files_count_valid?(count, flat_options)
31
37
  if flat_options[:max].present? && flat_options[:min].present?
32
38
  count >= flat_options[:min] && count <= flat_options[:max]
@@ -36,5 +42,36 @@ module ActiveStorageValidations
36
42
  count >= flat_options[:min]
37
43
  end
38
44
  end
45
+
46
+ def ensure_at_least_one_validator_option
47
+ unless AVAILABLE_CHECKS.any? { |argument| options.key?(argument) }
48
+ raise ArgumentError, 'You must pass either :max or :min to the validator'
49
+ end
50
+ end
51
+
52
+ def ensure_arguments_validity
53
+ return true if min_max_are_proc? || min_or_max_is_proc_and_other_not_present?
54
+
55
+ raise ArgumentError, 'You must pass integers to :min and :max' if min_or_max_defined_and_not_integer?
56
+ raise ArgumentError, 'You must pass a higher value to :max than to :min' if min_higher_than_max?
57
+ end
58
+
59
+ def min_max_are_proc?
60
+ options[:min]&.is_a?(Proc) && options[:max]&.is_a?(Proc)
61
+ end
62
+
63
+ def min_or_max_is_proc_and_other_not_present?
64
+ (options[:min]&.is_a?(Proc) && options[:max].nil?) ||
65
+ (options[:min].nil? && options[:max]&.is_a?(Proc))
66
+ end
67
+
68
+ def min_or_max_defined_and_not_integer?
69
+ (options.key?(:min) && !options[:min].is_a?(Integer)) ||
70
+ (options.key?(:max) && !options[:max].is_a?(Integer))
71
+ end
72
+
73
+ def min_higher_than_max?
74
+ options[:min] > options[:max] if options[:min].is_a?(Integer) && options[:max].is_a?(Integer)
75
+ end
39
76
  end
40
77
  end
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'concerns/active_storageable.rb'
4
+ require_relative 'concerns/allow_blankable.rb'
5
+ require_relative 'concerns/contextable.rb'
6
+ require_relative 'concerns/messageable.rb'
7
+ require_relative 'concerns/rspecable.rb'
8
+ require_relative 'concerns/validatable.rb'
9
+
10
+ module ActiveStorageValidations
11
+ module Matchers
12
+ def validate_aspect_ratio_of(name, expected_aspect_ratio)
13
+ AspectRatioValidatorMatcher.new(name, expected_aspect_ratio)
14
+ end
15
+
16
+ class AspectRatioValidatorMatcher
17
+ include ActiveStorageable
18
+ include AllowBlankable
19
+ include Contextable
20
+ include Messageable
21
+ include Rspecable
22
+ include Validatable
23
+
24
+ def initialize(attribute_name)
25
+ initialize_allow_blankable
26
+ initialize_contextable
27
+ initialize_messageable
28
+ initialize_rspecable
29
+ @attribute_name = attribute_name
30
+ @allowed_aspect_ratios = @rejected_aspect_ratios = []
31
+ end
32
+
33
+ def description
34
+ "validate the aspect ratios allowed on :#{@attribute_name}."
35
+ end
36
+
37
+ def failure_message
38
+ "is expected to validate aspect ratio of :#{@attribute_name}"
39
+ end
40
+
41
+ def allowing(*aspect_ratios)
42
+ @allowed_aspect_ratios = aspect_ratios.flatten
43
+ self
44
+ end
45
+
46
+ def rejecting(*aspect_ratios)
47
+ @rejected_aspect_ratios = aspect_ratios.flatten
48
+ self
49
+ end
50
+
51
+ def matches?(subject)
52
+ @subject = subject.is_a?(Class) ? subject.new : subject
53
+
54
+ is_a_valid_active_storage_attribute? &&
55
+ is_context_valid? &&
56
+ is_allowing_blank? &&
57
+ is_custom_message_valid? &&
58
+ all_allowed_aspect_ratios_allowed? &&
59
+ all_rejected_aspect_ratios_rejected?
60
+ end
61
+
62
+ protected
63
+
64
+ def all_allowed_aspect_ratios_allowed?
65
+ @allowed_aspect_ratios_not_allowed ||= @allowed_aspect_ratios.reject { |aspect_ratio| aspect_ratio_allowed?(aspect_ratio) }
66
+ @allowed_aspect_ratios_not_allowed.empty?
67
+ end
68
+
69
+ def all_rejected_aspect_ratios_rejected?
70
+ @rejected_aspect_ratios_not_rejected ||= @rejected_aspect_ratios.select { |aspect_ratio| aspect_ratio_allowed?(aspect_ratio) }
71
+ @rejected_aspect_ratios_not_rejected.empty?
72
+ end
73
+
74
+ def aspect_ratio_allowed?(aspect_ratio)
75
+ width, height = valid_width_and_height_for(aspect_ratio)
76
+
77
+ mock_dimensions_for(attach_file, width, height) do
78
+ validate
79
+ is_valid?
80
+ end
81
+ end
82
+
83
+ def is_custom_message_valid?
84
+ return true unless @custom_message
85
+
86
+ mock_dimensions_for(attach_file, -1, -1) do
87
+ validate
88
+ has_an_error_message_which_is_custom_message?
89
+ end
90
+ end
91
+
92
+ def attach_file
93
+ @subject.public_send(@attribute_name).attach(dummy_file)
94
+ @subject.public_send(@attribute_name)
95
+ end
96
+
97
+ def dummy_file
98
+ {
99
+ io: Tempfile.new('Hello world!'),
100
+ filename: 'test.png',
101
+ content_type: 'image/png'
102
+ }
103
+ end
104
+
105
+ def mock_dimensions_for(attachment, width, height)
106
+ Matchers.mock_metadata(attachment, width, height) do
107
+ yield
108
+ end
109
+ end
110
+
111
+ def valid_width_and_height_for(aspect_ratio)
112
+ case aspect_ratio
113
+ when :square then [100, 100]
114
+ when :portrait then [100, 200]
115
+ when :landscape then [200, 100]
116
+ when validator_class::ASPECT_RATIO_REGEX
117
+ aspect_ratio =~ validator_class::ASPECT_RATIO_REGEX
118
+ x = Regexp.last_match(1).to_i
119
+ y = Regexp.last_match(2).to_i
120
+
121
+ [100 * x, 100 * y]
122
+ else
123
+ [-1, -1]
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
@@ -1,5 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'concerns/active_storageable.rb'
4
+ require_relative 'concerns/contextable.rb'
5
+ require_relative 'concerns/messageable.rb'
6
+ require_relative 'concerns/rspecable.rb'
3
7
  require_relative 'concerns/validatable.rb'
4
8
 
5
9
  module ActiveStorageValidations
@@ -9,46 +13,39 @@ module ActiveStorageValidations
9
13
  end
10
14
 
11
15
  class AttachedValidatorMatcher
16
+ include ActiveStorageable
17
+ include Contextable
18
+ include Messageable
19
+ include Rspecable
12
20
  include Validatable
13
21
 
14
22
  def initialize(attribute_name)
23
+ initialize_contextable
24
+ initialize_messageable
25
+ initialize_rspecable
15
26
  @attribute_name = attribute_name
16
- @custom_message = nil
17
27
  end
18
28
 
19
29
  def description
20
- "validate #{@attribute_name} must be attached"
30
+ "validate that :#{@attribute_name} must be attached"
21
31
  end
22
32
 
23
- def with_message(message)
24
- @custom_message = message
25
- self
33
+ def failure_message
34
+ "is expected to validate attachment of :#{@attribute_name}"
26
35
  end
27
36
 
28
37
  def matches?(subject)
29
38
  @subject = subject.is_a?(Class) ? subject.new : subject
30
- responds_to_methods &&
31
- is_valid_when_file_attached? &&
32
- is_invalid_when_file_not_attached? &&
33
- validate_custom_message?
34
- end
35
39
 
36
- def failure_message
37
- "is expected to validate attached of #{@attribute_name}"
38
- end
39
-
40
- def failure_message_when_negated
41
- "is expected to not validate attached of #{@attribute_name}"
40
+ is_a_valid_active_storage_attribute? &&
41
+ is_context_valid? &&
42
+ is_custom_message_valid? &&
43
+ is_valid_when_file_attached? &&
44
+ is_invalid_when_file_not_attached?
42
45
  end
43
46
 
44
47
  private
45
48
 
46
- def responds_to_methods
47
- @subject.respond_to?(@attribute_name) &&
48
- @subject.public_send(@attribute_name).respond_to?(:attach) &&
49
- @subject.public_send(@attribute_name).respond_to?(:detach)
50
- end
51
-
52
49
  def is_valid_when_file_attached?
53
50
  attach_dummy_file unless file_attached?
54
51
  validate
@@ -61,7 +58,7 @@ module ActiveStorageValidations
61
58
  !is_valid?
62
59
  end
63
60
 
64
- def validate_custom_message?
61
+ def is_custom_message_valid?
65
62
  return true unless @custom_message
66
63
 
67
64
  detach_file if file_attached?
@@ -0,0 +1,17 @@
1
+ require "active_support/concern"
2
+
3
+ module ActiveStorageValidations
4
+ module Matchers
5
+ module ActiveStorageable
6
+ extend ActiveSupport::Concern
7
+
8
+ private
9
+
10
+ def is_a_valid_active_storage_attribute?
11
+ @subject.respond_to?(@attribute_name) &&
12
+ @subject.public_send(@attribute_name).respond_to?(:attach) &&
13
+ @subject.public_send(@attribute_name).respond_to?(:detach)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,26 @@
1
+ require "active_support/concern"
2
+
3
+ module ActiveStorageValidations
4
+ module Matchers
5
+ module AllowBlankable
6
+ extend ActiveSupport::Concern
7
+
8
+ def initialize_allow_blankable
9
+ @allow_blank = nil
10
+ end
11
+
12
+ def allow_blank
13
+ @allow_blank = true
14
+ self
15
+ end
16
+
17
+ private
18
+
19
+ def is_allowing_blank?
20
+ return true unless @allow_blank
21
+
22
+ validate
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,35 @@
1
+ require "active_support/concern"
2
+
3
+ module ActiveStorageValidations
4
+ module Matchers
5
+ module Contextable
6
+ extend ActiveSupport::Concern
7
+
8
+ def initialize_contextable
9
+ @context = nil
10
+ end
11
+
12
+ def on(context)
13
+ @context = context
14
+ self
15
+ end
16
+
17
+ private
18
+
19
+ def is_context_valid?
20
+ return true if !@context && !(attribute_validator && attribute_validator.options[:on])
21
+
22
+ raise ArgumentError, "This validator matcher needs the #on option to work since its validator has one" if !@context
23
+ raise ArgumentError, "This validator matcher option only allows a symbol or an array" if !(@context.is_a?(Symbol) || @context.is_a?(Array))
24
+
25
+ if @context.is_a?(Array) && attribute_validator.options[:on].is_a?(Array)
26
+ @context.to_set == attribute_validator.options[:on].to_set
27
+ elsif @context.is_a?(Symbol) && attribute_validator.options[:on].is_a?(Symbol)
28
+ @context == attribute_validator.options[:on]
29
+ else
30
+ false
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,26 @@
1
+ require "active_support/concern"
2
+
3
+ module ActiveStorageValidations
4
+ module Matchers
5
+ module Messageable
6
+ extend ActiveSupport::Concern
7
+
8
+ def initialize_messageable
9
+ @custom_message = nil
10
+ end
11
+
12
+ def with_message(custom_message)
13
+ @custom_message = custom_message
14
+ self
15
+ end
16
+
17
+ private
18
+
19
+ def has_an_error_message_which_is_custom_message?
20
+ validator_errors_for_attribute.one? do |error|
21
+ error[:error] == @custom_message
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,25 @@
1
+ require "active_support/concern"
2
+
3
+ module ActiveStorageValidations
4
+ module Matchers
5
+ module Rspecable
6
+ extend ActiveSupport::Concern
7
+
8
+ def initialize_rspecable
9
+ @failure_message_artefacts = []
10
+ end
11
+
12
+ def description
13
+ raise NotImplementedError, "#{self.class} did not define #{__method__}"
14
+ end
15
+
16
+ def failure_message
17
+ raise NotImplementedError, "#{self.class} did not define #{__method__}"
18
+ end
19
+
20
+ def failure_message_when_negated
21
+ failure_message.sub(/is expected to validate/, 'is expected not to validate')
22
+ end
23
+ end
24
+ end
25
+ end
@@ -1,46 +1,48 @@
1
- module Validatable
2
- extend ActiveSupport::Concern
3
-
4
- private
5
-
6
- def validate
7
- @subject.validate
8
- end
9
-
10
- def validator_errors_for_attribute
11
- @subject.errors.details[@attribute_name].select do |error|
12
- error[:validator_type] == validator_class.to_sym
13
- end
14
- end
15
-
16
- def is_valid?
17
- validator_errors_for_attribute.none? do |error|
18
- error[:error].in?(available_errors)
19
- end
20
- end
21
-
22
- def available_errors
23
- [
24
- *validator_class::ERROR_TYPES,
25
- *error_from_custom_message
26
- ].compact
27
- end
28
-
29
- def validator_class
30
- self.class.name.gsub(/::Matchers|Matcher/, '').constantize
31
- end
32
-
33
- def error_from_custom_message
34
- associated_validation = @subject.class.validators_on(@attribute_name).find do |validator|
35
- validator.class == validator_class
36
- end
37
-
38
- associated_validation.options[:message]
39
- end
40
-
41
- def has_an_error_message_which_is_custom_message?
42
- validator_errors_for_attribute.one? do |error|
43
- error[:error] == @custom_message
1
+ require "active_support/concern"
2
+
3
+ module ActiveStorageValidations
4
+ module Matchers
5
+ module Validatable
6
+ extend ActiveSupport::Concern
7
+
8
+ private
9
+
10
+ def validate
11
+ @subject.validate(@context)
12
+ end
13
+
14
+ def validator_errors_for_attribute
15
+ @subject.errors.details[@attribute_name].select do |error|
16
+ error[:validator_type] == validator_class.to_sym
17
+ end
18
+ end
19
+
20
+ def is_valid?
21
+ validator_errors_for_attribute.none? do |error|
22
+ error[:error].in?(available_errors)
23
+ end
24
+ end
25
+
26
+ def available_errors
27
+ [
28
+ *validator_class::ERROR_TYPES,
29
+ *error_from_custom_message
30
+ ].compact
31
+ end
32
+
33
+ def validator_class
34
+ self.class.name.gsub(/::Matchers|Matcher/, '').constantize
35
+ end
36
+
37
+ def attribute_validator
38
+ @subject.class.validators_on(@attribute_name).find do |validator|
39
+ validator.class == validator_class
40
+ end
41
+ end
42
+
43
+ def error_from_custom_message
44
+ attribute_validator.options[:message]
45
+ end
44
46
  end
45
47
  end
46
48
  end