active_storage_validations 1.1.3 → 1.2.0

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 (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