active_storage_validations 1.0.4 → 1.1.4

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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +126 -48
  3. data/config/locales/de.yml +6 -1
  4. data/config/locales/en.yml +5 -1
  5. data/config/locales/es.yml +6 -1
  6. data/config/locales/fr.yml +6 -1
  7. data/config/locales/it.yml +6 -1
  8. data/config/locales/ja.yml +6 -1
  9. data/config/locales/nl.yml +6 -1
  10. data/config/locales/pl.yml +6 -1
  11. data/config/locales/pt-BR.yml +6 -1
  12. data/config/locales/ru.yml +7 -2
  13. data/config/locales/sv.yml +23 -0
  14. data/config/locales/tr.yml +6 -1
  15. data/config/locales/uk.yml +6 -1
  16. data/config/locales/vi.yml +6 -1
  17. data/config/locales/zh-CN.yml +6 -1
  18. data/lib/active_storage_validations/aspect_ratio_validator.rb +57 -27
  19. data/lib/active_storage_validations/attached_validator.rb +20 -5
  20. data/lib/active_storage_validations/concerns/errorable.rb +38 -0
  21. data/lib/active_storage_validations/concerns/symbolizable.rb +12 -0
  22. data/lib/active_storage_validations/content_type_validator.rb +47 -7
  23. data/lib/active_storage_validations/dimension_validator.rb +61 -30
  24. data/lib/active_storage_validations/limit_validator.rb +49 -5
  25. data/lib/active_storage_validations/matchers/aspect_ratio_validator_matcher.rb +128 -0
  26. data/lib/active_storage_validations/matchers/attached_validator_matcher.rb +54 -23
  27. data/lib/active_storage_validations/matchers/concerns/active_storageable.rb +17 -0
  28. data/lib/active_storage_validations/matchers/concerns/allow_blankable.rb +26 -0
  29. data/lib/active_storage_validations/matchers/concerns/contextable.rb +35 -0
  30. data/lib/active_storage_validations/matchers/concerns/messageable.rb +26 -0
  31. data/lib/active_storage_validations/matchers/concerns/rspecable.rb +25 -0
  32. data/lib/active_storage_validations/matchers/concerns/validatable.rb +48 -0
  33. data/lib/active_storage_validations/matchers/content_type_validator_matcher.rb +75 -32
  34. data/lib/active_storage_validations/matchers/dimension_validator_matcher.rb +99 -52
  35. data/lib/active_storage_validations/matchers/size_validator_matcher.rb +96 -31
  36. data/lib/active_storage_validations/matchers.rb +1 -0
  37. data/lib/active_storage_validations/metadata.rb +42 -28
  38. data/lib/active_storage_validations/processable_image_validator.rb +16 -10
  39. data/lib/active_storage_validations/size_validator.rb +32 -9
  40. data/lib/active_storage_validations/version.rb +1 -1
  41. data/lib/active_storage_validations.rb +3 -0
  42. metadata +29 -4
@@ -1,17 +1,31 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'concerns/errorable.rb'
4
+ require_relative 'concerns/symbolizable.rb'
3
5
  require_relative 'metadata.rb'
4
6
 
5
7
  module ActiveStorageValidations
6
8
  class AspectRatioValidator < ActiveModel::EachValidator # :nodoc
7
9
  include OptionProcUnfolding
10
+ include Errorable
11
+ include Symbolizable
8
12
 
9
13
  AVAILABLE_CHECKS = %i[with].freeze
10
- PRECISION = 3
14
+ NAMED_ASPECT_RATIOS = %i[square portrait landscape].freeze
15
+ ASPECT_RATIO_REGEX = /is_([1-9]\d*)_([1-9]\d*)/.freeze
16
+ ERROR_TYPES = %i[
17
+ image_metadata_missing
18
+ aspect_ratio_not_square
19
+ aspect_ratio_not_portrait
20
+ aspect_ratio_not_landscape
21
+ aspect_ratio_is_not
22
+ aspect_ratio_unknown
23
+ ].freeze
24
+ PRECISION = 3.freeze
11
25
 
12
26
  def check_validity!
13
- return true if AVAILABLE_CHECKS.any? { |argument| options.key?(argument) }
14
- raise ArgumentError, 'You must pass :with to the validator'
27
+ ensure_at_least_one_validator_option
28
+ ensure_aspect_ratio_validity
15
29
  end
16
30
 
17
31
  if Rails.gem_version >= Gem::Version.new('6.0.0')
@@ -25,7 +39,7 @@ module ActiveStorageValidations
25
39
 
26
40
  files.each do |file|
27
41
  metadata = Metadata.new(file).metadata
28
- next if is_valid?(record, attribute, metadata)
42
+ next if is_valid?(record, attribute, file, metadata)
29
43
  break
30
44
  end
31
45
  end
@@ -41,57 +55,73 @@ module ActiveStorageValidations
41
55
  file.analyze; file.reload unless file.analyzed?
42
56
  metadata = file.metadata
43
57
 
44
- next if is_valid?(record, attribute, metadata)
58
+ next if is_valid?(record, attribute, file, metadata)
45
59
  break
46
60
  end
47
61
  end
48
62
  end
49
63
 
50
-
51
64
  private
52
65
 
53
-
54
- def is_valid?(record, attribute, metadata)
66
+ def is_valid?(record, attribute, file, metadata)
55
67
  flat_options = unfold_procs(record, self.options, AVAILABLE_CHECKS)
68
+ errors_options = initialize_error_options(options, file)
69
+
56
70
  if metadata[:width].to_i <= 0 || metadata[:height].to_i <= 0
57
- add_error(record, attribute, :image_metadata_missing, flat_options[:with])
71
+ errors_options[:aspect_ratio] = flat_options[:with]
72
+
73
+ add_error(record, attribute, :image_metadata_missing, **errors_options)
58
74
  return false
59
75
  end
60
76
 
61
77
  case flat_options[:with]
62
78
  when :square
63
79
  return true if metadata[:width] == metadata[:height]
64
- add_error(record, attribute, :aspect_ratio_not_square, flat_options[:with])
80
+ errors_options[:aspect_ratio] = flat_options[:with]
81
+ add_error(record, attribute, :aspect_ratio_not_square, **errors_options)
65
82
 
66
83
  when :portrait
67
84
  return true if metadata[:height] > metadata[:width]
68
- add_error(record, attribute, :aspect_ratio_not_portrait, flat_options[:with])
85
+ errors_options[:aspect_ratio] = flat_options[:with]
86
+ add_error(record, attribute, :aspect_ratio_not_portrait, **errors_options)
69
87
 
70
88
  when :landscape
71
89
  return true if metadata[:width] > metadata[:height]
72
- add_error(record, attribute, :aspect_ratio_not_landscape, flat_options[:with])
90
+ errors_options[:aspect_ratio] = flat_options[:with]
91
+ add_error(record, attribute, :aspect_ratio_not_landscape, **errors_options)
73
92
 
74
- else
75
- if flat_options[:with] =~ /is_(\d*)_(\d*)/
76
- x = $1.to_i
77
- y = $2.to_i
93
+ when ASPECT_RATIO_REGEX
94
+ flat_options[:with] =~ ASPECT_RATIO_REGEX
95
+ x = $1.to_i
96
+ y = $2.to_i
78
97
 
79
- return true if (x.to_f / y).round(PRECISION) == (metadata[:width].to_f / metadata[:height]).round(PRECISION)
98
+ return true if x > 0 && y > 0 && (x.to_f / y).round(PRECISION) == (metadata[:width].to_f / metadata[:height]).round(PRECISION)
80
99
 
81
- add_error(record, attribute, :aspect_ratio_is_not, "#{x}x#{y}")
82
- else
83
- add_error(record, attribute, :aspect_ratio_unknown, flat_options[:with])
84
- end
100
+ errors_options[:aspect_ratio] = "#{x}:#{y}"
101
+ add_error(record, attribute, :aspect_ratio_is_not, **errors_options)
102
+ else
103
+ errors_options[:aspect_ratio] = flat_options[:with]
104
+ add_error(record, attribute, :aspect_ratio_unknown, **errors_options)
105
+ return false
85
106
  end
86
- false
87
107
  end
88
108
 
89
-
90
- def add_error(record, attribute, default_message, interpolate)
91
- message = options[:message].presence || default_message
92
- return if record.errors.added?(attribute, message)
93
- record.errors.add(attribute, message, aspect_ratio: interpolate)
109
+ def ensure_at_least_one_validator_option
110
+ unless AVAILABLE_CHECKS.any? { |argument| options.key?(argument) }
111
+ raise ArgumentError, 'You must pass :with to the validator'
112
+ end
94
113
  end
95
114
 
115
+ def ensure_aspect_ratio_validity
116
+ return true if options[:with]&.is_a?(Proc)
117
+
118
+ unless NAMED_ASPECT_RATIOS.include?(options[:with]) || options[:with] =~ ASPECT_RATIO_REGEX
119
+ raise ArgumentError, <<~ERROR_MESSAGE
120
+ You must pass a valid aspect ratio to the validator
121
+ It should either be a named aspect ratio (#{NAMED_ASPECT_RATIOS.join(', ')})
122
+ Or an aspect ratio like 'is_16_9' (matching /#{ASPECT_RATIO_REGEX.source}/)
123
+ ERROR_MESSAGE
124
+ end
125
+ end
96
126
  end
97
127
  end
@@ -1,14 +1,29 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'concerns/errorable.rb'
4
+ require_relative 'concerns/symbolizable.rb'
5
+
3
6
  module ActiveStorageValidations
4
7
  class AttachedValidator < ActiveModel::EachValidator # :nodoc:
5
- def validate_each(record, attribute, _value)
6
- return if record.send(attribute).attached?
8
+ include Errorable
9
+ include Symbolizable
10
+
11
+ ERROR_TYPES = %i[blank].freeze
7
12
 
8
- errors_options = {}
9
- errors_options[:message] = options[:message] if options[:message].present?
13
+ def check_validity!
14
+ %i(allow_nil allow_blank).each do |not_authorized_option|
15
+ if options.include?(not_authorized_option)
16
+ raise ArgumentError, "You cannot pass the :#{not_authorized_option} option to this validator"
17
+ end
18
+ end
19
+ end
20
+
21
+ def validate_each(record, attribute, _value)
22
+ return if record.send(attribute).attached? &&
23
+ !Array.wrap(record.send(attribute)).all? { |file| file.marked_for_destruction? }
10
24
 
11
- record.errors.add(attribute, :blank, **errors_options)
25
+ errors_options = initialize_error_options(options)
26
+ add_error(record, attribute, ERROR_TYPES.first, **errors_options)
12
27
  end
13
28
  end
14
29
  end
@@ -0,0 +1,38 @@
1
+ module ActiveStorageValidations
2
+ module Errorable
3
+ extend ActiveSupport::Concern
4
+
5
+ def initialize_error_options(options, file = nil)
6
+ not_explicitly_written_options = %i(with in)
7
+ curated_options = options.except(*not_explicitly_written_options)
8
+
9
+ active_storage_validations_options = {
10
+ validator_type: self.class.to_sym,
11
+ custom_message: (options[:message] if options[:message].present?),
12
+ filename: get_filename(file)
13
+ }.compact
14
+
15
+ curated_options.merge(active_storage_validations_options)
16
+ end
17
+
18
+ def add_error(record, attribute, error_type, **errors_options)
19
+ type = errors_options[:custom_message].presence || error_type
20
+ return if record.errors.added?(attribute, type)
21
+
22
+ # You can read https://api.rubyonrails.org/classes/ActiveModel/Errors.html#method-i-add
23
+ # to better understand how Rails model errors work
24
+ record.errors.add(attribute, type, **errors_options)
25
+ end
26
+
27
+ private
28
+
29
+ def get_filename(file)
30
+ return nil unless file
31
+
32
+ case file
33
+ when ActiveStorage::Attached then file.blob.filename.to_s
34
+ when Hash then file[:filename]
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,12 @@
1
+ module ActiveStorageValidations
2
+ module Symbolizable
3
+ extend ActiveSupport::Concern
4
+
5
+ class_methods do
6
+ def to_sym
7
+ validator_class = self.name.split("::").last
8
+ validator_class.sub(/Validator/, '').underscore.to_sym
9
+ end
10
+ end
11
+ end
12
+ end
@@ -1,27 +1,37 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'concerns/errorable.rb'
4
+ require_relative 'concerns/symbolizable.rb'
5
+
3
6
  module ActiveStorageValidations
4
7
  class ContentTypeValidator < ActiveModel::EachValidator # :nodoc:
5
8
  include OptionProcUnfolding
9
+ include Errorable
10
+ include Symbolizable
6
11
 
7
12
  AVAILABLE_CHECKS = %i[with in].freeze
8
-
13
+ ERROR_TYPES = %i[content_type_invalid].freeze
14
+
15
+ def check_validity!
16
+ ensure_exactly_one_validator_option
17
+ ensure_content_types_validity
18
+ end
19
+
9
20
  def validate_each(record, attribute, _value)
10
21
  return true unless record.send(attribute).attached?
11
22
 
12
23
  types = authorized_types(record)
13
24
  return true if types.empty?
14
-
15
- files = Array.wrap(record.send(attribute))
16
25
 
17
- errors_options = { authorized_types: types_to_human_format(types) }
18
- errors_options[:message] = options[:message] if options[:message].present?
26
+ files = Array.wrap(record.send(attribute))
19
27
 
20
28
  files.each do |file|
21
29
  next if is_valid?(file, types)
22
30
 
31
+ errors_options = initialize_error_options(options, file)
32
+ errors_options[:authorized_types] = types_to_human_format(types)
23
33
  errors_options[:content_type] = content_type(file)
24
- record.errors.add(attribute, :content_type_invalid, **errors_options)
34
+ add_error(record, attribute, ERROR_TYPES.first, **errors_options)
25
35
  break
26
36
  end
27
37
  end
@@ -39,7 +49,7 @@ module ActiveStorageValidations
39
49
 
40
50
  def types_to_human_format(types)
41
51
  types
42
- .map { |type| type.to_s.split('/').last.upcase }
52
+ .map { |type| type.is_a?(Regexp) ? type.source : type.to_s.split('/').last.upcase }
43
53
  .join(', ')
44
54
  end
45
55
 
@@ -53,5 +63,35 @@ module ActiveStorageValidations
53
63
  type == file_type || (type.is_a?(Regexp) && type.match?(file_type.to_s))
54
64
  end
55
65
  end
66
+
67
+ def ensure_exactly_one_validator_option
68
+ unless AVAILABLE_CHECKS.one? { |argument| options.key?(argument) }
69
+ raise ArgumentError, 'You must pass either :with or :in to the validator'
70
+ end
71
+ end
72
+
73
+ def ensure_content_types_validity
74
+ return true if options[:with]&.is_a?(Proc) || options[:in]&.is_a?(Proc)
75
+
76
+ ([options[:with]] || options[:in]).each do |content_type|
77
+ raise ArgumentError, invalid_content_type_message(content_type) if invalid_content_type?(content_type)
78
+ end
79
+ end
80
+
81
+ def invalid_content_type_message(content_type)
82
+ <<~ERROR_MESSAGE
83
+ You must pass valid content types to the validator
84
+ '#{content_type}' is not find in Marcel::EXTENSIONS mimes
85
+ ERROR_MESSAGE
86
+ end
87
+
88
+ def invalid_content_type?(content_type)
89
+ case content_type
90
+ when String, Symbol
91
+ Marcel::MimeType.for(declared_type: content_type.to_s, extension: content_type.to_s) == 'application/octet-stream'
92
+ when Regexp
93
+ false # We always validate regexes
94
+ end
95
+ end
56
96
  end
57
97
  end
@@ -1,12 +1,29 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'concerns/errorable.rb'
4
+ require_relative 'concerns/symbolizable.rb'
3
5
  require_relative 'metadata.rb'
4
6
 
5
7
  module ActiveStorageValidations
6
8
  class DimensionValidator < ActiveModel::EachValidator # :nodoc
7
9
  include OptionProcUnfolding
10
+ include Errorable
11
+ include Symbolizable
8
12
 
9
13
  AVAILABLE_CHECKS = %i[width height min max].freeze
14
+ ERROR_TYPES = %i[
15
+ image_metadata_missing
16
+ dimension_min_inclusion
17
+ dimension_max_inclusion
18
+ dimension_width_inclusion
19
+ dimension_height_inclusion
20
+ dimension_width_greater_than_or_equal_to
21
+ dimension_height_greater_than_or_equal_to
22
+ dimension_width_less_than_or_equal_to
23
+ dimension_height_less_than_or_equal_to
24
+ dimension_width_equal_to
25
+ dimension_height_equal_to
26
+ ].freeze
10
27
 
11
28
  def process_options(record)
12
29
  flat_options = unfold_procs(record, self.options, AVAILABLE_CHECKS)
@@ -30,10 +47,10 @@ module ActiveStorageValidations
30
47
  flat_options
31
48
  end
32
49
 
33
-
34
50
  def check_validity!
35
- return true if AVAILABLE_CHECKS.any? { |argument| options.key?(argument) }
36
- raise ArgumentError, 'You must pass either :width, :height, :min or :max to the validator'
51
+ unless AVAILABLE_CHECKS.any? { |argument| options.key?(argument) }
52
+ raise ArgumentError, 'You must pass either :width, :height, :min or :max to the validator'
53
+ end
37
54
  end
38
55
 
39
56
 
@@ -47,7 +64,7 @@ module ActiveStorageValidations
47
64
  files = Array.wrap(changes.is_a?(ActiveStorage::Attached::Changes::CreateMany) ? changes.attachables : changes.attachable)
48
65
  files.each do |file|
49
66
  metadata = Metadata.new(file).metadata
50
- next if is_valid?(record, attribute, metadata)
67
+ next if is_valid?(record, attribute, file, metadata)
51
68
  break
52
69
  end
53
70
  end
@@ -61,60 +78,81 @@ module ActiveStorageValidations
61
78
  # Analyze file first if not analyzed to get all required metadata.
62
79
  file.analyze; file.reload unless file.analyzed?
63
80
  metadata = file.metadata rescue {}
64
- next if is_valid?(record, attribute, metadata)
81
+ next if is_valid?(record, attribute, file, metadata)
65
82
  break
66
83
  end
67
84
  end
68
85
  end
69
86
 
70
87
 
71
- def is_valid?(record, attribute, file_metadata)
88
+ def is_valid?(record, attribute, file, metadata)
72
89
  flat_options = process_options(record)
90
+ errors_options = initialize_error_options(options, file)
91
+
73
92
  # Validation fails unless file metadata contains valid width and height.
74
- if file_metadata[:width].to_i <= 0 || file_metadata[:height].to_i <= 0
75
- add_error(record, attribute, :image_metadata_missing)
93
+ if metadata[:width].to_i <= 0 || metadata[:height].to_i <= 0
94
+ add_error(record, attribute, :image_metadata_missing, **errors_options)
76
95
  return false
77
96
  end
78
97
 
79
98
  # Validation based on checks :min and :max (:min, :max has higher priority to :width, :height).
80
99
  if flat_options[:min] || flat_options[:max]
81
100
  if flat_options[:min] && (
82
- (flat_options[:width][:min] && file_metadata[:width] < flat_options[:width][:min]) ||
83
- (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])
84
103
  )
85
- add_error(record, attribute, :dimension_min_inclusion, width: flat_options[:width][:min], height: flat_options[:height][:min])
104
+ errors_options[:width] = flat_options[:width][:min]
105
+ errors_options[:height] = flat_options[:height][:min]
106
+
107
+ add_error(record, attribute, :dimension_min_inclusion, **errors_options)
86
108
  return false
87
109
  end
88
110
  if flat_options[:max] && (
89
- (flat_options[:width][:max] && file_metadata[:width] > flat_options[:width][:max]) ||
90
- (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])
91
113
  )
92
- add_error(record, attribute, :dimension_max_inclusion, width: flat_options[:width][:max], height: flat_options[:height][:max])
114
+ errors_options[:width] = flat_options[:width][:max]
115
+ errors_options[:height] = flat_options[:height][:max]
116
+
117
+ add_error(record, attribute, :dimension_max_inclusion, **errors_options)
93
118
  return false
94
119
  end
95
120
 
96
121
  # Validation based on checks :width and :height.
97
122
  else
98
123
  width_or_height_invalid = false
124
+
99
125
  [:width, :height].each do |length|
100
126
  next unless flat_options[length]
101
127
  if flat_options[length].is_a?(Hash)
102
- if flat_options[length][:in] && (file_metadata[length] < flat_options[length][:min] || file_metadata[length] > flat_options[length][:max])
103
- add_error(record, attribute, :"dimension_#{length}_inclusion", min: flat_options[length][:min], max: flat_options[length][:max])
128
+ if flat_options[length][:in] && (metadata[length] < flat_options[length][:min] || metadata[length] > flat_options[length][:max])
129
+ error_type = :"dimension_#{length}_inclusion"
130
+ errors_options[:min] = flat_options[length][:min]
131
+ errors_options[:max] = flat_options[length][:max]
132
+
133
+ add_error(record, attribute, error_type, **errors_options)
104
134
  width_or_height_invalid = true
105
135
  else
106
- if flat_options[length][:min] && file_metadata[length] < flat_options[length][:min]
107
- add_error(record, attribute, :"dimension_#{length}_greater_than_or_equal_to", length: flat_options[length][:min])
136
+ if flat_options[length][:min] && metadata[length] < flat_options[length][:min]
137
+ error_type = :"dimension_#{length}_greater_than_or_equal_to"
138
+ errors_options[:length] = flat_options[length][:min]
139
+
140
+ add_error(record, attribute, error_type, **errors_options)
108
141
  width_or_height_invalid = true
109
- end
110
- if flat_options[length][:max] && file_metadata[length] > flat_options[length][:max]
111
- add_error(record, attribute, :"dimension_#{length}_less_than_or_equal_to", length: flat_options[length][:max])
142
+ elsif flat_options[length][:max] && metadata[length] > flat_options[length][:max]
143
+ error_type = :"dimension_#{length}_less_than_or_equal_to"
144
+ errors_options[:length] = flat_options[length][:max]
145
+
146
+ add_error(record, attribute, error_type, **errors_options)
112
147
  width_or_height_invalid = true
113
148
  end
114
149
  end
115
150
  else
116
- if file_metadata[length] != flat_options[length]
117
- add_error(record, attribute, :"dimension_#{length}_equal_to", length: flat_options[length])
151
+ if metadata[length] != flat_options[length]
152
+ error_type = :"dimension_#{length}_equal_to"
153
+ errors_options[:length] = flat_options[length]
154
+
155
+ add_error(record, attribute, error_type, **errors_options)
118
156
  width_or_height_invalid = true
119
157
  end
120
158
  end
@@ -125,12 +163,5 @@ module ActiveStorageValidations
125
163
 
126
164
  true # valid file
127
165
  end
128
-
129
- def add_error(record, attribute, default_message, **attrs)
130
- message = options[:message].presence || default_message
131
- return if record.errors.added?(attribute, message)
132
- record.errors.add(attribute, message, **attrs)
133
- end
134
-
135
166
  end
136
167
  end
@@ -1,25 +1,38 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'concerns/errorable.rb'
4
+ require_relative 'concerns/symbolizable.rb'
5
+
3
6
  module ActiveStorageValidations
4
7
  class LimitValidator < ActiveModel::EachValidator # :nodoc:
5
8
  include OptionProcUnfolding
9
+ include Errorable
10
+ include Symbolizable
6
11
 
7
12
  AVAILABLE_CHECKS = %i[max min].freeze
13
+ ERROR_TYPES = %i[
14
+ limit_out_of_range
15
+ ].freeze
8
16
 
9
17
  def check_validity!
10
- return true if AVAILABLE_CHECKS.any? { |argument| options.key?(argument) }
11
- raise ArgumentError, 'You must pass either :max or :min to the validator'
18
+ ensure_at_least_one_validator_option
19
+ ensure_arguments_validity
12
20
  end
13
21
 
14
22
  def validate_each(record, attribute, _)
15
- files = Array.wrap(record.send(attribute)).compact.uniq
23
+ files = Array.wrap(record.send(attribute)).reject { |file| file.blank? }.compact.uniq
16
24
  flat_options = unfold_procs(record, self.options, AVAILABLE_CHECKS)
17
- errors_options = { min: flat_options[:min], max: flat_options[:max] }
18
25
 
19
26
  return true if files_count_valid?(files.count, flat_options)
20
- record.errors.add(attribute, options[:message].presence || :limit_out_of_range, **errors_options)
27
+
28
+ errors_options = initialize_error_options(options)
29
+ errors_options[:min] = flat_options[:min]
30
+ errors_options[:max] = flat_options[:max]
31
+ add_error(record, attribute, ERROR_TYPES.first, **errors_options)
21
32
  end
22
33
 
34
+ private
35
+
23
36
  def files_count_valid?(count, flat_options)
24
37
  if flat_options[:max].present? && flat_options[:min].present?
25
38
  count >= flat_options[:min] && count <= flat_options[:max]
@@ -29,5 +42,36 @@ module ActiveStorageValidations
29
42
  count >= flat_options[:min]
30
43
  end
31
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
32
76
  end
33
77
  end