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.
- checksums.yaml +4 -4
- data/README.md +126 -48
- data/config/locales/de.yml +6 -1
- data/config/locales/en.yml +5 -1
- data/config/locales/es.yml +6 -1
- data/config/locales/fr.yml +6 -1
- data/config/locales/it.yml +6 -1
- data/config/locales/ja.yml +6 -1
- data/config/locales/nl.yml +6 -1
- data/config/locales/pl.yml +6 -1
- data/config/locales/pt-BR.yml +6 -1
- data/config/locales/ru.yml +7 -2
- data/config/locales/sv.yml +23 -0
- data/config/locales/tr.yml +6 -1
- data/config/locales/uk.yml +6 -1
- data/config/locales/vi.yml +6 -1
- data/config/locales/zh-CN.yml +6 -1
- data/lib/active_storage_validations/aspect_ratio_validator.rb +57 -27
- data/lib/active_storage_validations/attached_validator.rb +20 -5
- data/lib/active_storage_validations/concerns/errorable.rb +38 -0
- data/lib/active_storage_validations/concerns/symbolizable.rb +12 -0
- data/lib/active_storage_validations/content_type_validator.rb +47 -7
- data/lib/active_storage_validations/dimension_validator.rb +61 -30
- data/lib/active_storage_validations/limit_validator.rb +49 -5
- data/lib/active_storage_validations/matchers/aspect_ratio_validator_matcher.rb +128 -0
- data/lib/active_storage_validations/matchers/attached_validator_matcher.rb +54 -23
- data/lib/active_storage_validations/matchers/concerns/active_storageable.rb +17 -0
- data/lib/active_storage_validations/matchers/concerns/allow_blankable.rb +26 -0
- data/lib/active_storage_validations/matchers/concerns/contextable.rb +35 -0
- data/lib/active_storage_validations/matchers/concerns/messageable.rb +26 -0
- data/lib/active_storage_validations/matchers/concerns/rspecable.rb +25 -0
- data/lib/active_storage_validations/matchers/concerns/validatable.rb +48 -0
- data/lib/active_storage_validations/matchers/content_type_validator_matcher.rb +75 -32
- data/lib/active_storage_validations/matchers/dimension_validator_matcher.rb +99 -52
- data/lib/active_storage_validations/matchers/size_validator_matcher.rb +96 -31
- data/lib/active_storage_validations/matchers.rb +1 -0
- data/lib/active_storage_validations/metadata.rb +42 -28
- data/lib/active_storage_validations/processable_image_validator.rb +16 -10
- data/lib/active_storage_validations/size_validator.rb +32 -9
- data/lib/active_storage_validations/version.rb +1 -1
- data/lib/active_storage_validations.rb +3 -0
- 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
|
-
|
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
|
-
|
14
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
90
|
+
errors_options[:aspect_ratio] = flat_options[:with]
|
91
|
+
add_error(record, attribute, :aspect_ratio_not_landscape, **errors_options)
|
73
92
|
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
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
|
-
|
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
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
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
|
-
|
91
|
-
|
92
|
-
|
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
|
-
|
6
|
-
|
8
|
+
include Errorable
|
9
|
+
include Symbolizable
|
10
|
+
|
11
|
+
ERROR_TYPES = %i[blank].freeze
|
7
12
|
|
8
|
-
|
9
|
-
|
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
|
-
|
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
|
@@ -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
|
-
|
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
|
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
|
-
|
36
|
-
|
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,
|
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
|
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] &&
|
83
|
-
(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
|
-
|
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] &&
|
90
|
-
(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
|
-
|
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] && (
|
103
|
-
|
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] &&
|
107
|
-
|
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
|
-
|
110
|
-
|
111
|
-
|
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
|
117
|
-
|
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
|
-
|
11
|
-
|
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
|
-
|
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
|