active_storage_validations 1.0.4 → 1.1.4

Sign up to get free protection for your applications and to get access to all the features.
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