active_storage_validations 1.1.3 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +133 -69
  3. data/config/locales/da.yml +33 -0
  4. data/config/locales/de.yml +5 -0
  5. data/config/locales/en.yml +5 -0
  6. data/config/locales/es.yml +5 -0
  7. data/config/locales/fr.yml +5 -0
  8. data/config/locales/it.yml +5 -0
  9. data/config/locales/ja.yml +5 -0
  10. data/config/locales/nl.yml +5 -0
  11. data/config/locales/pl.yml +5 -0
  12. data/config/locales/pt-BR.yml +5 -0
  13. data/config/locales/ru.yml +5 -0
  14. data/config/locales/sv.yml +10 -1
  15. data/config/locales/tr.yml +5 -0
  16. data/config/locales/uk.yml +5 -0
  17. data/config/locales/vi.yml +5 -0
  18. data/config/locales/zh-CN.yml +5 -0
  19. data/lib/active_storage_validations/aspect_ratio_validator.rb +47 -22
  20. data/lib/active_storage_validations/attached_validator.rb +12 -3
  21. data/lib/active_storage_validations/base_size_validator.rb +66 -0
  22. data/lib/active_storage_validations/concerns/errorable.rb +38 -0
  23. data/lib/active_storage_validations/concerns/symbolizable.rb +8 -6
  24. data/lib/active_storage_validations/content_type_validator.rb +41 -6
  25. data/lib/active_storage_validations/dimension_validator.rb +15 -15
  26. data/lib/active_storage_validations/limit_validator.rb +44 -7
  27. data/lib/active_storage_validations/matchers/aspect_ratio_validator_matcher.rb +119 -0
  28. data/lib/active_storage_validations/matchers/attached_validator_matcher.rb +25 -36
  29. data/lib/active_storage_validations/matchers/base_size_validator_matcher.rb +134 -0
  30. data/lib/active_storage_validations/matchers/concerns/active_storageable.rb +17 -0
  31. data/lib/active_storage_validations/matchers/concerns/allow_blankable.rb +26 -0
  32. data/lib/active_storage_validations/matchers/concerns/attachable.rb +48 -0
  33. data/lib/active_storage_validations/matchers/concerns/contextable.rb +47 -0
  34. data/lib/active_storage_validations/matchers/concerns/messageable.rb +26 -0
  35. data/lib/active_storage_validations/matchers/concerns/rspecable.rb +25 -0
  36. data/lib/active_storage_validations/matchers/concerns/validatable.rb +11 -10
  37. data/lib/active_storage_validations/matchers/content_type_validator_matcher.rb +44 -27
  38. data/lib/active_storage_validations/matchers/dimension_validator_matcher.rb +67 -59
  39. data/lib/active_storage_validations/matchers/processable_image_validator_matcher.rb +78 -0
  40. data/lib/active_storage_validations/matchers/size_validator_matcher.rb +8 -126
  41. data/lib/active_storage_validations/matchers/total_size_validator_matcher.rb +40 -0
  42. data/lib/active_storage_validations/matchers.rb +3 -0
  43. data/lib/active_storage_validations/metadata.rb +60 -28
  44. data/lib/active_storage_validations/processable_image_validator.rb +14 -5
  45. data/lib/active_storage_validations/size_validator.rb +7 -51
  46. data/lib/active_storage_validations/total_size_validator.rb +49 -0
  47. data/lib/active_storage_validations/version.rb +1 -1
  48. data/lib/active_storage_validations.rb +3 -2
  49. metadata +38 -39
  50. data/lib/active_storage_validations/error_handler.rb +0 -21
@@ -1,21 +1,31 @@
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 AspectRatioValidator < ActiveModel::EachValidator # :nodoc
8
9
  include OptionProcUnfolding
9
- include ErrorHandler
10
+ include Errorable
10
11
  include Symbolizable
11
12
 
12
13
  AVAILABLE_CHECKS = %i[with].freeze
13
- 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
14
25
 
15
26
  def check_validity!
16
- unless AVAILABLE_CHECKS.any? { |argument| options.key?(argument) }
17
- raise ArgumentError, 'You must pass :with to the validator'
18
- end
27
+ ensure_at_least_one_validator_option
28
+ ensure_aspect_ratio_validity
19
29
  end
20
30
 
21
31
  if Rails.gem_version >= Gem::Version.new('6.0.0')
@@ -29,7 +39,7 @@ module ActiveStorageValidations
29
39
 
30
40
  files.each do |file|
31
41
  metadata = Metadata.new(file).metadata
32
- next if is_valid?(record, attribute, metadata)
42
+ next if is_valid?(record, attribute, file, metadata)
33
43
  break
34
44
  end
35
45
  end
@@ -45,19 +55,17 @@ module ActiveStorageValidations
45
55
  file.analyze; file.reload unless file.analyzed?
46
56
  metadata = file.metadata
47
57
 
48
- next if is_valid?(record, attribute, metadata)
58
+ next if is_valid?(record, attribute, file, metadata)
49
59
  break
50
60
  end
51
61
  end
52
62
  end
53
63
 
54
-
55
64
  private
56
65
 
57
-
58
- def is_valid?(record, attribute, metadata)
66
+ def is_valid?(record, attribute, file, metadata)
59
67
  flat_options = unfold_procs(record, self.options, AVAILABLE_CHECKS)
60
- errors_options = initialize_error_options(options)
68
+ errors_options = initialize_error_options(options, file)
61
69
 
62
70
  if metadata[:width].to_i <= 0 || metadata[:height].to_i <= 0
63
71
  errors_options[:aspect_ratio] = flat_options[:with]
@@ -82,21 +90,38 @@ module ActiveStorageValidations
82
90
  errors_options[:aspect_ratio] = flat_options[:with]
83
91
  add_error(record, attribute, :aspect_ratio_not_landscape, **errors_options)
84
92
 
93
+ when ASPECT_RATIO_REGEX
94
+ flat_options[:with] =~ ASPECT_RATIO_REGEX
95
+ x = $1.to_i
96
+ y = $2.to_i
97
+
98
+ return true if x > 0 && y > 0 && (x.to_f / y).round(PRECISION) == (metadata[:width].to_f / metadata[:height]).round(PRECISION)
99
+
100
+ errors_options[:aspect_ratio] = "#{x}:#{y}"
101
+ add_error(record, attribute, :aspect_ratio_is_not, **errors_options)
85
102
  else
86
- if flat_options[:with] =~ /is_(\d*)_(\d*)/
87
- x = $1.to_i
88
- y = $2.to_i
103
+ errors_options[:aspect_ratio] = flat_options[:with]
104
+ add_error(record, attribute, :aspect_ratio_unknown, **errors_options)
105
+ return false
106
+ end
107
+ end
108
+
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
113
+ end
89
114
 
90
- return true if (x.to_f / y).round(PRECISION) == (metadata[:width].to_f / metadata[:height]).round(PRECISION)
115
+ def ensure_aspect_ratio_validity
116
+ return true if options[:with]&.is_a?(Proc)
91
117
 
92
- errors_options[:aspect_ratio] = "#{x}x#{y}"
93
- add_error(record, attribute, :aspect_ratio_is_not, **errors_options)
94
- else
95
- errors_options[:aspect_ratio] = flat_options[:with]
96
- add_error(record, attribute, :aspect_ratio_unknown, **errors_options)
97
- end
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
98
124
  end
99
- false
100
125
  end
101
126
  end
102
127
  end
@@ -1,19 +1,28 @@
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 AttachedValidator < ActiveModel::EachValidator # :nodoc:
7
- include ErrorHandler
8
+ include Errorable
8
9
  include Symbolizable
9
10
 
10
11
  ERROR_TYPES = %i[blank].freeze
11
12
 
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 the #{self.class.name.split('::').last.underscore}"
17
+ end
18
+ end
19
+ end
20
+
12
21
  def validate_each(record, attribute, _value)
13
- return if record.send(attribute).attached?
22
+ return if record.send(attribute).attached? &&
23
+ !Array.wrap(record.send(attribute)).all?(&:marked_for_destruction?)
14
24
 
15
25
  errors_options = initialize_error_options(options)
16
-
17
26
  add_error(record, attribute, ERROR_TYPES.first, **errors_options)
18
27
  end
19
28
  end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'concerns/errorable.rb'
4
+ require_relative 'concerns/symbolizable.rb'
5
+
6
+ module ActiveStorageValidations
7
+ class BaseSizeValidator < ActiveModel::EachValidator # :nodoc:
8
+ include OptionProcUnfolding
9
+ include Errorable
10
+ include Symbolizable
11
+
12
+ delegate :number_to_human_size, to: ActiveSupport::NumberHelper
13
+
14
+ AVAILABLE_CHECKS = %i[
15
+ less_than
16
+ less_than_or_equal_to
17
+ greater_than
18
+ greater_than_or_equal_to
19
+ between
20
+ ].freeze
21
+
22
+ def initialize(*args)
23
+ if self.class == BaseSizeValidator
24
+ raise NotImplementedError, 'BaseSizeValidator is an abstract class and cannot be instantiated directly.'
25
+ end
26
+ super
27
+ end
28
+
29
+ def check_validity!
30
+ unless AVAILABLE_CHECKS.one? { |argument| options.key?(argument) }
31
+ raise ArgumentError, 'You must pass either :less_than(_or_equal_to), :greater_than(_or_equal_to), or :between to the validator'
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def is_valid?(size, flat_options)
38
+ return false if size < 0
39
+
40
+ if flat_options[:between].present?
41
+ flat_options[:between].include?(size)
42
+ elsif flat_options[:less_than].present?
43
+ size < flat_options[:less_than]
44
+ elsif flat_options[:less_than_or_equal_to].present?
45
+ size <= flat_options[:less_than_or_equal_to]
46
+ elsif flat_options[:greater_than].present?
47
+ size > flat_options[:greater_than]
48
+ elsif flat_options[:greater_than_or_equal_to].present?
49
+ size >= flat_options[:greater_than_or_equal_to]
50
+ end
51
+ end
52
+
53
+ def populate_error_options(errors_options, flat_options)
54
+ errors_options[:min_size] = number_to_human_size(min_size(flat_options))
55
+ errors_options[:max_size] = number_to_human_size(max_size(flat_options))
56
+ end
57
+
58
+ def min_size(flat_options)
59
+ flat_options[:between]&.min || flat_options[:greater_than] || flat_options[:greater_than_or_equal_to]
60
+ end
61
+
62
+ def max_size(flat_options)
63
+ flat_options[:between]&.max || flat_options[:less_than] || flat_options[:less_than_or_equal_to]
64
+ end
65
+ end
66
+ 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) unless self.class.to_sym == :total_size)
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, ActiveStorage::Attachment then file.blob&.filename&.to_s
34
+ when Hash then file[:filename]
35
+ end
36
+ end
37
+ end
38
+ end
@@ -1,10 +1,12 @@
1
- module Symbolizable
2
- extend ActiveSupport::Concern
1
+ module ActiveStorageValidations
2
+ module Symbolizable
3
+ extend ActiveSupport::Concern
3
4
 
4
- class_methods do
5
- def to_sym
6
- validator_class = self.name.split("::").last
7
- validator_class.sub(/Validator/, '').underscore.to_sym
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
8
10
  end
9
11
  end
10
12
  end
@@ -1,30 +1,35 @@
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 ContentTypeValidator < ActiveModel::EachValidator # :nodoc:
7
8
  include OptionProcUnfolding
8
- include ErrorHandler
9
+ include Errorable
9
10
  include Symbolizable
10
11
 
11
12
  AVAILABLE_CHECKS = %i[with in].freeze
12
13
  ERROR_TYPES = %i[content_type_invalid].freeze
13
14
 
15
+ def check_validity!
16
+ ensure_exactly_one_validator_option
17
+ ensure_content_types_validity
18
+ end
19
+
14
20
  def validate_each(record, attribute, _value)
15
21
  return true unless record.send(attribute).attached?
16
22
 
17
23
  types = authorized_types(record)
18
24
  return true if types.empty?
19
-
20
- files = Array.wrap(record.send(attribute))
21
25
 
22
- errors_options = initialize_error_options(options)
23
- errors_options[:authorized_types] = types_to_human_format(types)
26
+ files = Array.wrap(record.send(attribute))
24
27
 
25
28
  files.each do |file|
26
29
  next if is_valid?(file, types)
27
30
 
31
+ errors_options = initialize_error_options(options, file)
32
+ errors_options[:authorized_types] = types_to_human_format(types)
28
33
  errors_options[:content_type] = content_type(file)
29
34
  add_error(record, attribute, ERROR_TYPES.first, **errors_options)
30
35
  break
@@ -44,7 +49,7 @@ module ActiveStorageValidations
44
49
 
45
50
  def types_to_human_format(types)
46
51
  types
47
- .map { |type| type.to_s.split('/').last.upcase }
52
+ .map { |type| type.is_a?(Regexp) ? type.source : type.to_s.split('/').last.upcase }
48
53
  .join(', ')
49
54
  end
50
55
 
@@ -58,5 +63,35 @@ module ActiveStorageValidations
58
63
  type == file_type || (type.is_a?(Regexp) && type.match?(file_type.to_s))
59
64
  end
60
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
61
96
  end
62
97
  end
@@ -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,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'concerns/active_storageable.rb'
4
+ require_relative 'concerns/allow_blankable.rb'
5
+ require_relative 'concerns/attachable.rb'
6
+ require_relative 'concerns/contextable.rb'
7
+ require_relative 'concerns/messageable.rb'
8
+ require_relative 'concerns/rspecable.rb'
9
+ require_relative 'concerns/validatable.rb'
10
+
11
+ module ActiveStorageValidations
12
+ module Matchers
13
+ def validate_aspect_ratio_of(attribute_name)
14
+ AspectRatioValidatorMatcher.new(attribute_name)
15
+ end
16
+
17
+ class AspectRatioValidatorMatcher
18
+ include ActiveStorageable
19
+ include AllowBlankable
20
+ include Attachable
21
+ include Contextable
22
+ include Messageable
23
+ include Rspecable
24
+ include Validatable
25
+
26
+ def initialize(attribute_name)
27
+ initialize_allow_blankable
28
+ initialize_contextable
29
+ initialize_messageable
30
+ initialize_rspecable
31
+ @attribute_name = attribute_name
32
+ @allowed_aspect_ratios = @rejected_aspect_ratios = []
33
+ end
34
+
35
+ def description
36
+ "validate the aspect ratios allowed on :#{@attribute_name}."
37
+ end
38
+
39
+ def failure_message
40
+ "is expected to validate aspect ratio of :#{@attribute_name}"
41
+ end
42
+
43
+ def allowing(*aspect_ratios)
44
+ @allowed_aspect_ratios = aspect_ratios.flatten
45
+ self
46
+ end
47
+
48
+ def rejecting(*aspect_ratios)
49
+ @rejected_aspect_ratios = aspect_ratios.flatten
50
+ self
51
+ end
52
+
53
+ def matches?(subject)
54
+ @subject = subject.is_a?(Class) ? subject.new : subject
55
+
56
+ is_a_valid_active_storage_attribute? &&
57
+ is_context_valid? &&
58
+ is_allowing_blank? &&
59
+ is_custom_message_valid? &&
60
+ all_allowed_aspect_ratios_allowed? &&
61
+ all_rejected_aspect_ratios_rejected?
62
+ end
63
+
64
+ protected
65
+
66
+ def all_allowed_aspect_ratios_allowed?
67
+ @allowed_aspect_ratios_not_allowed ||= @allowed_aspect_ratios.reject { |aspect_ratio| aspect_ratio_allowed?(aspect_ratio) }
68
+ @allowed_aspect_ratios_not_allowed.empty?
69
+ end
70
+
71
+ def all_rejected_aspect_ratios_rejected?
72
+ @rejected_aspect_ratios_not_rejected ||= @rejected_aspect_ratios.select { |aspect_ratio| aspect_ratio_allowed?(aspect_ratio) }
73
+ @rejected_aspect_ratios_not_rejected.empty?
74
+ end
75
+
76
+ def aspect_ratio_allowed?(aspect_ratio)
77
+ width, height = valid_width_and_height_for(aspect_ratio)
78
+
79
+ mock_dimensions_for(attach_file, width, height) do
80
+ validate
81
+ detach_file
82
+ is_valid?
83
+ end
84
+ end
85
+
86
+ def is_custom_message_valid?
87
+ return true unless @custom_message
88
+
89
+ mock_dimensions_for(attach_file, -1, -1) do
90
+ validate
91
+ detach_file
92
+ has_an_error_message_which_is_custom_message?
93
+ end
94
+ end
95
+
96
+ def mock_dimensions_for(attachment, width, height)
97
+ Matchers.mock_metadata(attachment, width, height) do
98
+ yield
99
+ end
100
+ end
101
+
102
+ def valid_width_and_height_for(aspect_ratio)
103
+ case aspect_ratio
104
+ when :square then [100, 100]
105
+ when :portrait then [100, 200]
106
+ when :landscape then [200, 100]
107
+ when validator_class::ASPECT_RATIO_REGEX
108
+ aspect_ratio =~ validator_class::ASPECT_RATIO_REGEX
109
+ x = Regexp.last_match(1).to_i
110
+ y = Regexp.last_match(2).to_i
111
+
112
+ [100 * x, 100 * y]
113
+ else
114
+ [-1, -1]
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end