active_storage_validations 1.1.1 → 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 +145 -73
  3. data/config/locales/da.yml +33 -0
  4. data/config/locales/de.yml +6 -0
  5. data/config/locales/en.yml +5 -0
  6. data/config/locales/es.yml +6 -0
  7. data/config/locales/fr.yml +6 -0
  8. data/config/locales/it.yml +6 -0
  9. data/config/locales/ja.yml +6 -0
  10. data/config/locales/nl.yml +6 -0
  11. data/config/locales/pl.yml +6 -0
  12. data/config/locales/pt-BR.yml +5 -0
  13. data/config/locales/ru.yml +7 -1
  14. data/config/locales/sv.yml +10 -1
  15. data/config/locales/tr.yml +6 -0
  16. data/config/locales/uk.yml +6 -0
  17. data/config/locales/vi.yml +6 -0
  18. data/config/locales/zh-CN.yml +6 -0
  19. data/lib/active_storage_validations/aspect_ratio_validator.rb +49 -22
  20. data/lib/active_storage_validations/attached_validator.rb +18 -4
  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 +12 -0
  24. data/lib/active_storage_validations/content_type_validator.rb +47 -8
  25. data/lib/active_storage_validations/dimension_validator.rb +30 -15
  26. data/lib/active_storage_validations/limit_validator.rb +48 -8
  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 +46 -33
  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 +54 -0
  37. data/lib/active_storage_validations/matchers/content_type_validator_matcher.rb +76 -52
  38. data/lib/active_storage_validations/matchers/dimension_validator_matcher.rb +93 -55
  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 +9 -88
  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 +16 -5
  45. data/lib/active_storage_validations/size_validator.rb +18 -43
  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 +42 -26
  50. data/lib/active_storage_validations/error_handler.rb +0 -18
@@ -7,6 +7,11 @@ tr:
7
7
  file_size_not_greater_than: "dosya boyutu %{min_size} boyutundan büyük olmalıdır (geçerli boyut %{file_size}'dir)"
8
8
  file_size_not_greater_than_or_equal_to: "dosya boyutu %{min_size} değerinden büyük veya eşit olmalıdır (geçerli boyut %{file_size}'dir)"
9
9
  file_size_not_between: "dosya boyutu %{min_size} ile %{max_size} arasında olmalıdır (geçerli boyut %{file_size}'dir)"
10
+ total_file_size_not_less_than: "toplam dosya boyutu %{max_size} boyutundan küçük olmalıdır (geçerli boyut %{total_file_size}'dir)"
11
+ total_file_size_not_less_than_or_equal_to: "toplam dosya boyutu %{max_size} değerinden küçük veya ona eşit olmalıdır (geçerli boyut %{total_file_size}'dir)"
12
+ total_file_size_not_greater_than: "toplam dosya boyutu %{min_size} boyutundan büyük olmalıdır (geçerli boyut %{total_file_size}'dir)"
13
+ total_file_size_not_greater_than_or_equal_to: "toplam dosya boyutu %{min_size} değerinden büyük veya eşit olmalıdır (geçerli boyut %{total_file_size}'dir)"
14
+ total_file_size_not_between: "toplam dosya boyutu %{min_size} ile %{max_size} arasında olmalıdır (geçerli boyut %{total_file_size}'dir)"
10
15
  limit_out_of_range: "toplam miktar aralık dışında"
11
16
  image_metadata_missing: "geçerli bir imaj değil"
12
17
  dimension_min_inclusion: "%{width} x %{height} piksele eşit ya da büyük olmalı"
@@ -24,3 +29,4 @@ tr:
24
29
  aspect_ratio_not_landscape: "yatay bir imaj olmalı"
25
30
  aspect_ratio_is_not: "%{aspect_ratio} en boy oranına sahip olmalı"
26
31
  aspect_ratio_unknown: "bilinmeyen en boy oranı"
32
+ image_not_processable: "geçerli bir imaj değil"
@@ -7,6 +7,11 @@ uk:
7
7
  file_size_not_greater_than: "розмір файлу має бути більшим ніж %{min_size} (поточний розмір %{file_size})"
8
8
  file_size_not_greater_than_or_equal_to: "розмір файлу має бути більшим або дорівнювати %{min_size} (поточний розмір %{file_size})"
9
9
  file_size_not_between: "розмір файлу має бути від %{min_size} до %{max_size} (поточний розмір %{file_size})"
10
+ total_file_size_not_less_than: "загальний розмір файлу має бути менше %{max_size} (поточний розмір %{total_file_size})"
11
+ total_file_size_not_less_than_or_equal_to: "загальний розмір файлу має бути меншим або дорівнювати %{max_size} (поточний розмір %{total_file_size})"
12
+ total_file_size_not_greater_than: "загальний розмір файлу має бути більшим ніж %{min_size} (поточний розмір %{total_file_size})"
13
+ total_file_size_not_greater_than_or_equal_to: "загальний розмір файлу має бути більшим або дорівнювати %{min_size} (поточний розмір %{total_file_size})"
14
+ total_file_size_not_between: "загальний розмір файлу має бути від %{min_size} до %{max_size} (поточний розмір %{total_file_size})"
10
15
  limit_out_of_range: "кількість файлів більше необхідного"
11
16
  image_metadata_missing: "не є допустимим зображенням"
12
17
  dimension_min_inclusion: "мусить бути більше або дорівнювати %{width} x %{height} пікселям"
@@ -24,3 +29,4 @@ uk:
24
29
  aspect_ratio_not_landscape: "мусить бути пейзажне зображення"
25
30
  aspect_ratio_is_not: "мусить мати співвідношення сторін %{aspect_ratio}"
26
31
  aspect_ratio_unknown: "має невідоме співвідношення сторін"
32
+ image_not_processable: "не є допустимим зображенням"
@@ -7,6 +7,11 @@ vi:
7
7
  file_size_not_greater_than: "kích thước tệp phải lớn hơn %{min_size} (kích thước hiện tại là %{file_size})"
8
8
  file_size_not_greater_than_or_equal_to: "kích thước tệp phải lớn hơn hoặc bằng %{min_size} (kích thước hiện tại là %{file_size})"
9
9
  file_size_not_between: "kích thước tệp phải nằm trong khoảng từ %{min_size} đến %{max_size} (kích thước hiện tại là %{file_size})"
10
+ total_file_size_not_less_than: "tổng kích thước tệp phải nhỏ hơn %{max_size} (kích thước hiện tại là %{total_file_size})"
11
+ total_file_size_not_less_than_or_equal_to: "tổng kích thước tệp phải nhỏ hơn hoặc bằng %{max_size} (kích thước hiện tại là %{total_file_size})"
12
+ total_file_size_not_greater_than: "tổng kích thước tệp phải lớn hơn %{min_size} (kích thước hiện tại là %{total_file_size})"
13
+ total_file_size_not_greater_than_or_equal_to: "tổng kích thước tệp phải lớn hơn hoặc bằng %{min_size} (kích thước hiện tại là %{total_file_size})"
14
+ total_file_size_not_between: "tổng kích thước tệp phải nằm trong khoảng từ %{min_size} đến %{max_size} (kích thước hiện tại là %{total_file_size})"
10
15
  limit_out_of_range: "tổng số tệp vượt giới hạn"
11
16
  image_metadata_missing: "không phải là ảnh"
12
17
  dimension_min_inclusion: "phải lớn hơn hoặc bằng %{width} x %{height} pixel"
@@ -24,3 +29,4 @@ vi:
24
29
  aspect_ratio_not_landscape: "phải là ảnh ngang"
25
30
  aspect_ratio_is_not: "phải có tỉ lệ ảnh %{aspect_ratio}"
26
31
  aspect_ratio_unknown: "tỉ lệ ảnh không xác định"
32
+ image_not_processable: "không phải là ảnh"
@@ -7,6 +7,11 @@ zh-CN:
7
7
  file_size_not_greater_than: "文件大小必须大于 %{min_size}(当前大小为 %{file_size})"
8
8
  file_size_not_greater_than_or_equal_to: "文件大小必须大于或等于 %{min_size}(当前大小为 %{file_size})"
9
9
  file_size_not_between: "文件大小必须介于 %{min_size} 和 %{max_size} 之间(当前大小为 %{file_size})"
10
+ total_file_size_not_less_than: "总文件大小必须小于 %{max_size}(当前大小为 %{total_file_size})"
11
+ total_file_size_not_less_than_or_equal_to: "文件总大小必须小于或等于 %{max_size}(当前大小为 %{total_file_size})"
12
+ total_file_size_not_greater_than: "总文件大小必须大于 %{min_size}(当前大小为 %{total_file_size})"
13
+ total_file_size_not_greater_than_or_equal_to: "文件总大小必须大于或等于 %{min_size}(当前大小为 %{total_file_size})"
14
+ total_file_size_not_between: "总文件大小必须介于 %{min_size} 和 %{max_size} 之间(当前大小为 %{total_file_size})"
10
15
  limit_out_of_range: "文件数超出限定范围"
11
16
  image_metadata_missing: "不是有效的图像"
12
17
  dimension_min_inclusion: "必须大于或等于 %{width} x %{height} 像素"
@@ -24,3 +29,4 @@ zh-CN:
24
29
  aspect_ratio_not_landscape: "必须是横屏图片"
25
30
  aspect_ratio_is_not: "纵横比必须是 %{aspect_ratio}"
26
31
  aspect_ratio_unknown: "未知的纵横比"
32
+ image_not_processable: "不是有效的图像"
@@ -1,19 +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
8
- include ErrorHandler
10
+ include Errorable
11
+ include Symbolizable
9
12
 
10
13
  AVAILABLE_CHECKS = %i[with].freeze
11
- 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
12
25
 
13
26
  def check_validity!
14
- unless AVAILABLE_CHECKS.any? { |argument| options.key?(argument) }
15
- raise ArgumentError, 'You must pass :with to the validator'
16
- end
27
+ ensure_at_least_one_validator_option
28
+ ensure_aspect_ratio_validity
17
29
  end
18
30
 
19
31
  if Rails.gem_version >= Gem::Version.new('6.0.0')
@@ -27,7 +39,7 @@ module ActiveStorageValidations
27
39
 
28
40
  files.each do |file|
29
41
  metadata = Metadata.new(file).metadata
30
- next if is_valid?(record, attribute, metadata)
42
+ next if is_valid?(record, attribute, file, metadata)
31
43
  break
32
44
  end
33
45
  end
@@ -43,19 +55,17 @@ module ActiveStorageValidations
43
55
  file.analyze; file.reload unless file.analyzed?
44
56
  metadata = file.metadata
45
57
 
46
- next if is_valid?(record, attribute, metadata)
58
+ next if is_valid?(record, attribute, file, metadata)
47
59
  break
48
60
  end
49
61
  end
50
62
  end
51
63
 
52
-
53
64
  private
54
65
 
55
-
56
- def is_valid?(record, attribute, metadata)
66
+ def is_valid?(record, attribute, file, metadata)
57
67
  flat_options = unfold_procs(record, self.options, AVAILABLE_CHECKS)
58
- errors_options = initialize_error_options(options)
68
+ errors_options = initialize_error_options(options, file)
59
69
 
60
70
  if metadata[:width].to_i <= 0 || metadata[:height].to_i <= 0
61
71
  errors_options[:aspect_ratio] = flat_options[:with]
@@ -80,21 +90,38 @@ module ActiveStorageValidations
80
90
  errors_options[:aspect_ratio] = flat_options[:with]
81
91
  add_error(record, attribute, :aspect_ratio_not_landscape, **errors_options)
82
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)
83
102
  else
84
- if flat_options[:with] =~ /is_(\d*)_(\d*)/
85
- x = $1.to_i
86
- 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
87
114
 
88
- 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)
89
117
 
90
- errors_options[:aspect_ratio] = "#{x}x#{y}"
91
- add_error(record, attribute, :aspect_ratio_is_not, **errors_options)
92
- else
93
- errors_options[:aspect_ratio] = flat_options[:with]
94
- add_error(record, attribute, :aspect_ratio_unknown, **errors_options)
95
- 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
96
124
  end
97
- false
98
125
  end
99
126
  end
100
127
  end
@@ -1,15 +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
- include ErrorHandler
8
+ include Errorable
9
+ include Symbolizable
10
+
11
+ ERROR_TYPES = %i[blank].freeze
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
6
20
 
7
21
  def validate_each(record, attribute, _value)
8
- return if record.send(attribute).attached?
22
+ return if record.send(attribute).attached? &&
23
+ !Array.wrap(record.send(attribute)).all?(&:marked_for_destruction?)
9
24
 
10
25
  errors_options = initialize_error_options(options)
11
-
12
- add_error(record, attribute, :blank, **errors_options)
26
+ add_error(record, attribute, ERROR_TYPES.first, **errors_options)
13
27
  end
14
28
  end
15
29
  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
@@ -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,28 +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
6
- include ErrorHandler
9
+ include Errorable
10
+ include Symbolizable
7
11
 
8
12
  AVAILABLE_CHECKS = %i[with in].freeze
9
-
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
+
10
20
  def validate_each(record, attribute, _value)
11
21
  return true unless record.send(attribute).attached?
12
22
 
13
23
  types = authorized_types(record)
14
24
  return true if types.empty?
15
-
16
- files = Array.wrap(record.send(attribute))
17
25
 
18
- errors_options = initialize_error_options(options)
19
- errors_options[:authorized_types] = types_to_human_format(types)
26
+ files = Array.wrap(record.send(attribute))
20
27
 
21
28
  files.each do |file|
22
29
  next if is_valid?(file, types)
23
30
 
31
+ errors_options = initialize_error_options(options, file)
32
+ errors_options[:authorized_types] = types_to_human_format(types)
24
33
  errors_options[:content_type] = content_type(file)
25
- add_error(record, attribute, :content_type_invalid, **errors_options)
34
+ add_error(record, attribute, ERROR_TYPES.first, **errors_options)
26
35
  break
27
36
  end
28
37
  end
@@ -40,7 +49,7 @@ module ActiveStorageValidations
40
49
 
41
50
  def types_to_human_format(types)
42
51
  types
43
- .map { |type| type.to_s.split('/').last.upcase }
52
+ .map { |type| type.is_a?(Regexp) ? type.source : type.to_s.split('/').last.upcase }
44
53
  .join(', ')
45
54
  end
46
55
 
@@ -54,5 +63,35 @@ module ActiveStorageValidations
54
63
  type == file_type || (type.is_a?(Regexp) && type.match?(file_type.to_s))
55
64
  end
56
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
57
96
  end
58
97
  end
@@ -1,13 +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
8
- include ErrorHandler
10
+ include Errorable
11
+ include Symbolizable
9
12
 
10
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
11
27
 
12
28
  def process_options(record)
13
29
  flat_options = unfold_procs(record, self.options, AVAILABLE_CHECKS)
@@ -31,7 +47,6 @@ module ActiveStorageValidations
31
47
  flat_options
32
48
  end
33
49
 
34
-
35
50
  def check_validity!
36
51
  unless AVAILABLE_CHECKS.any? { |argument| options.key?(argument) }
37
52
  raise ArgumentError, 'You must pass either :width, :height, :min or :max to the validator'
@@ -49,7 +64,7 @@ module ActiveStorageValidations
49
64
  files = Array.wrap(changes.is_a?(ActiveStorage::Attached::Changes::CreateMany) ? changes.attachables : changes.attachable)
50
65
  files.each do |file|
51
66
  metadata = Metadata.new(file).metadata
52
- next if is_valid?(record, attribute, metadata)
67
+ next if is_valid?(record, attribute, file, metadata)
53
68
  break
54
69
  end
55
70
  end
@@ -63,19 +78,19 @@ module ActiveStorageValidations
63
78
  # Analyze file first if not analyzed to get all required metadata.
64
79
  file.analyze; file.reload unless file.analyzed?
65
80
  metadata = file.metadata rescue {}
66
- next if is_valid?(record, attribute, metadata)
81
+ next if is_valid?(record, attribute, file, metadata)
67
82
  break
68
83
  end
69
84
  end
70
85
  end
71
86
 
72
87
 
73
- def is_valid?(record, attribute, file_metadata)
88
+ def is_valid?(record, attribute, file, metadata)
74
89
  flat_options = process_options(record)
75
- errors_options = initialize_error_options(options)
90
+ errors_options = initialize_error_options(options, file)
76
91
 
77
92
  # Validation fails unless file metadata contains valid width and height.
78
- 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
79
94
  add_error(record, attribute, :image_metadata_missing, **errors_options)
80
95
  return false
81
96
  end
@@ -83,8 +98,8 @@ module ActiveStorageValidations
83
98
  # Validation based on checks :min and :max (:min, :max has higher priority to :width, :height).
84
99
  if flat_options[:min] || flat_options[:max]
85
100
  if flat_options[:min] && (
86
- (flat_options[:width][:min] && file_metadata[:width] < flat_options[:width][:min]) ||
87
- (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])
88
103
  )
89
104
  errors_options[:width] = flat_options[:width][:min]
90
105
  errors_options[:height] = flat_options[:height][:min]
@@ -93,8 +108,8 @@ module ActiveStorageValidations
93
108
  return false
94
109
  end
95
110
  if flat_options[:max] && (
96
- (flat_options[:width][:max] && file_metadata[:width] > flat_options[:width][:max]) ||
97
- (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])
98
113
  )
99
114
  errors_options[:width] = flat_options[:width][:max]
100
115
  errors_options[:height] = flat_options[:height][:max]
@@ -110,7 +125,7 @@ module ActiveStorageValidations
110
125
  [:width, :height].each do |length|
111
126
  next unless flat_options[length]
112
127
  if flat_options[length].is_a?(Hash)
113
- 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])
114
129
  error_type = :"dimension_#{length}_inclusion"
115
130
  errors_options[:min] = flat_options[length][:min]
116
131
  errors_options[:max] = flat_options[length][:max]
@@ -118,13 +133,13 @@ module ActiveStorageValidations
118
133
  add_error(record, attribute, error_type, **errors_options)
119
134
  width_or_height_invalid = true
120
135
  else
121
- if flat_options[length][:min] && file_metadata[length] < flat_options[length][:min]
136
+ if flat_options[length][:min] && metadata[length] < flat_options[length][:min]
122
137
  error_type = :"dimension_#{length}_greater_than_or_equal_to"
123
138
  errors_options[:length] = flat_options[length][:min]
124
139
 
125
140
  add_error(record, attribute, error_type, **errors_options)
126
141
  width_or_height_invalid = true
127
- elsif flat_options[length][:max] && file_metadata[length] > flat_options[length][:max]
142
+ elsif flat_options[length][:max] && metadata[length] > flat_options[length][:max]
128
143
  error_type = :"dimension_#{length}_less_than_or_equal_to"
129
144
  errors_options[:length] = flat_options[length][:max]
130
145
 
@@ -133,7 +148,7 @@ module ActiveStorageValidations
133
148
  end
134
149
  end
135
150
  else
136
- if file_metadata[length] != flat_options[length]
151
+ if metadata[length] != flat_options[length]
137
152
  error_type = :"dimension_#{length}_equal_to"
138
153
  errors_options[:length] = flat_options[length]
139
154
 
@@ -1,29 +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
6
- include ErrorHandler
9
+ include Errorable
10
+ include Symbolizable
7
11
 
8
12
  AVAILABLE_CHECKS = %i[max min].freeze
13
+ ERROR_TYPES = %i[
14
+ limit_out_of_range
15
+ ].freeze
9
16
 
10
17
  def check_validity!
11
- unless AVAILABLE_CHECKS.any? { |argument| options.key?(argument) }
12
- raise ArgumentError, 'You must pass either :max or :min to the validator'
13
- end
18
+ ensure_at_least_one_validator_option
19
+ ensure_arguments_validity
14
20
  end
15
21
 
16
22
  def validate_each(record, attribute, _)
17
- files = Array.wrap(record.send(attribute)).compact.uniq
23
+ files = Array.wrap(record.send(attribute)).reject { |file| file.blank? }.compact.uniq
18
24
  flat_options = unfold_procs(record, self.options, AVAILABLE_CHECKS)
25
+
26
+ return true if files_count_valid?(files.count, flat_options)
27
+
19
28
  errors_options = initialize_error_options(options)
20
29
  errors_options[:min] = flat_options[:min]
21
30
  errors_options[:max] = flat_options[:max]
22
-
23
- return true if files_count_valid?(files.count, flat_options)
24
- add_error(record, attribute, :limit_out_of_range, **errors_options)
31
+ add_error(record, attribute, ERROR_TYPES.first, **errors_options)
25
32
  end
26
33
 
34
+ private
35
+
27
36
  def files_count_valid?(count, flat_options)
28
37
  if flat_options[:max].present? && flat_options[:min].present?
29
38
  count >= flat_options[:min] && count <= flat_options[:max]
@@ -33,5 +42,36 @@ module ActiveStorageValidations
33
42
  count >= flat_options[:min]
34
43
  end
35
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
36
76
  end
37
77
  end