active_storage_validations 1.3.5 → 2.0.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.
- checksums.yaml +4 -4
- data/README.md +620 -279
- data/config/locales/da.yml +50 -29
- data/config/locales/de.yml +50 -29
- data/config/locales/en.yml +50 -29
- data/config/locales/es.yml +50 -29
- data/config/locales/fr.yml +50 -29
- data/config/locales/it.yml +50 -29
- data/config/locales/ja.yml +50 -29
- data/config/locales/nl.yml +50 -29
- data/config/locales/pl.yml +50 -29
- data/config/locales/pt-BR.yml +50 -29
- data/config/locales/ru.yml +50 -29
- data/config/locales/sv.yml +50 -29
- data/config/locales/tr.yml +50 -29
- data/config/locales/uk.yml +50 -29
- data/config/locales/vi.yml +50 -29
- data/config/locales/zh-CN.yml +50 -29
- data/lib/active_storage_validations/analyzer/audio_analyzer.rb +58 -0
- data/lib/active_storage_validations/analyzer/content_type_analyzer.rb +60 -0
- data/lib/active_storage_validations/analyzer/image_analyzer/image_magick.rb +12 -11
- data/lib/active_storage_validations/analyzer/image_analyzer/vips.rb +12 -12
- data/lib/active_storage_validations/analyzer/image_analyzer.rb +18 -46
- data/lib/active_storage_validations/analyzer/null_analyzer.rb +2 -2
- data/lib/active_storage_validations/analyzer/shared/asv_ff_probable.rb +61 -0
- data/lib/active_storage_validations/analyzer/video_analyzer.rb +130 -0
- data/lib/active_storage_validations/analyzer.rb +54 -1
- data/lib/active_storage_validations/aspect_ratio_validator.rb +154 -120
- data/lib/active_storage_validations/{base_size_validator.rb → base_comparison_validator.rb} +18 -16
- data/lib/active_storage_validations/content_type_validator.rb +51 -17
- data/lib/active_storage_validations/dimension_validator.rb +20 -19
- data/lib/active_storage_validations/duration_validator.rb +55 -0
- data/lib/active_storage_validations/extensors/asv_blob_metadatable.rb +24 -0
- data/lib/active_storage_validations/{marcel_extensor.rb → extensors/asv_marcelable.rb} +5 -0
- data/lib/active_storage_validations/limit_validator.rb +14 -2
- data/lib/active_storage_validations/matchers/aspect_ratio_validator_matcher.rb +1 -1
- data/lib/active_storage_validations/matchers/{base_size_validator_matcher.rb → base_comparison_validator_matcher.rb} +31 -25
- data/lib/active_storage_validations/matchers/content_type_validator_matcher.rb +7 -3
- data/lib/active_storage_validations/matchers/dimension_validator_matcher.rb +1 -1
- data/lib/active_storage_validations/matchers/duration_validator_matcher.rb +39 -0
- data/lib/active_storage_validations/matchers/{processable_image_validator_matcher.rb → processable_file_validator_matcher.rb} +5 -5
- data/lib/active_storage_validations/matchers/size_validator_matcher.rb +18 -2
- data/lib/active_storage_validations/matchers/total_size_validator_matcher.rb +18 -2
- data/lib/active_storage_validations/matchers.rb +4 -3
- data/lib/active_storage_validations/{processable_image_validator.rb → processable_file_validator.rb} +4 -3
- data/lib/active_storage_validations/railtie.rb +5 -0
- data/lib/active_storage_validations/shared/asv_active_storageable.rb +2 -2
- data/lib/active_storage_validations/shared/asv_analyzable.rb +38 -3
- data/lib/active_storage_validations/shared/asv_attachable.rb +36 -15
- data/lib/active_storage_validations/size_validator.rb +11 -3
- data/lib/active_storage_validations/total_size_validator.rb +9 -3
- data/lib/active_storage_validations/version.rb +1 -1
- data/lib/active_storage_validations.rb +7 -3
- metadata +14 -8
- data/lib/active_storage_validations/content_type_spoof_detector.rb +0 -96
@@ -6,14 +6,12 @@ require_relative 'shared/asv_optionable'
|
|
6
6
|
require_relative 'shared/asv_symbolizable'
|
7
7
|
|
8
8
|
module ActiveStorageValidations
|
9
|
-
class
|
9
|
+
class BaseComparisonValidator < ActiveModel::EachValidator # :nodoc:
|
10
10
|
include ASVActiveStorageable
|
11
11
|
include ASVErrorable
|
12
12
|
include ASVOptionable
|
13
13
|
include ASVSymbolizable
|
14
14
|
|
15
|
-
delegate :number_to_human_size, to: ActiveSupport::NumberHelper
|
16
|
-
|
17
15
|
AVAILABLE_CHECKS = %i[
|
18
16
|
less_than
|
19
17
|
less_than_or_equal_to
|
@@ -23,8 +21,8 @@ module ActiveStorageValidations
|
|
23
21
|
].freeze
|
24
22
|
|
25
23
|
def initialize(*args)
|
26
|
-
if self.class ==
|
27
|
-
raise NotImplementedError, '
|
24
|
+
if self.class == BaseComparisonValidator
|
25
|
+
raise NotImplementedError, 'BaseComparisonValidator is an abstract class and cannot be instantiated directly.'
|
28
26
|
end
|
29
27
|
super
|
30
28
|
end
|
@@ -37,32 +35,36 @@ module ActiveStorageValidations
|
|
37
35
|
|
38
36
|
private
|
39
37
|
|
40
|
-
def is_valid?(
|
41
|
-
return false if
|
38
|
+
def is_valid?(value, flat_options)
|
39
|
+
return false if value < 0
|
42
40
|
|
43
41
|
if flat_options[:between].present?
|
44
|
-
flat_options[:between].include?(
|
42
|
+
flat_options[:between].include?(value)
|
45
43
|
elsif flat_options[:less_than].present?
|
46
|
-
|
44
|
+
value < flat_options[:less_than]
|
47
45
|
elsif flat_options[:less_than_or_equal_to].present?
|
48
|
-
|
46
|
+
value <= flat_options[:less_than_or_equal_to]
|
49
47
|
elsif flat_options[:greater_than].present?
|
50
|
-
|
48
|
+
value > flat_options[:greater_than]
|
51
49
|
elsif flat_options[:greater_than_or_equal_to].present?
|
52
|
-
|
50
|
+
value >= flat_options[:greater_than_or_equal_to]
|
53
51
|
end
|
54
52
|
end
|
55
53
|
|
56
54
|
def populate_error_options(errors_options, flat_options)
|
57
|
-
errors_options[:
|
58
|
-
errors_options[:
|
55
|
+
errors_options[:min] = format_bound_value(min(flat_options))
|
56
|
+
errors_options[:max] = format_bound_value(max(flat_options))
|
57
|
+
end
|
58
|
+
|
59
|
+
def format_bound_value
|
60
|
+
raise NotImplementedError
|
59
61
|
end
|
60
62
|
|
61
|
-
def
|
63
|
+
def min(flat_options)
|
62
64
|
flat_options[:between]&.min || flat_options[:greater_than] || flat_options[:greater_than_or_equal_to]
|
63
65
|
end
|
64
66
|
|
65
|
-
def
|
67
|
+
def max(flat_options)
|
66
68
|
flat_options[:between]&.max || flat_options[:less_than] || flat_options[:less_than_or_equal_to]
|
67
69
|
end
|
68
70
|
end
|
@@ -6,7 +6,7 @@ require_relative 'shared/asv_attachable'
|
|
6
6
|
require_relative 'shared/asv_errorable'
|
7
7
|
require_relative 'shared/asv_optionable'
|
8
8
|
require_relative 'shared/asv_symbolizable'
|
9
|
-
require_relative '
|
9
|
+
require_relative 'analyzer/content_type_analyzer'
|
10
10
|
|
11
11
|
module ActiveStorageValidations
|
12
12
|
class ContentTypeValidator < ActiveModel::EachValidator # :nodoc:
|
@@ -20,8 +20,9 @@ module ActiveStorageValidations
|
|
20
20
|
AVAILABLE_CHECKS = %i[with in].freeze
|
21
21
|
ERROR_TYPES = %i[
|
22
22
|
content_type_invalid
|
23
|
-
|
23
|
+
content_type_spoofed
|
24
24
|
].freeze
|
25
|
+
METADATA_KEYS = %i[content_type].freeze
|
25
26
|
|
26
27
|
def check_validity!
|
27
28
|
ensure_exactly_one_validator_option
|
@@ -34,9 +35,9 @@ module ActiveStorageValidations
|
|
34
35
|
@authorized_content_types = authorized_content_types_from_options(record)
|
35
36
|
return if @authorized_content_types.empty?
|
36
37
|
|
37
|
-
|
38
|
-
set_attachable_cached_values(
|
39
|
-
is_valid?(record, attribute, attachable)
|
38
|
+
attachables_and_blobs(record, attribute).each do |attachable, blob|
|
39
|
+
set_attachable_cached_values(blob)
|
40
|
+
is_valid?(record, attribute, attachable, blob)
|
40
41
|
end
|
41
42
|
end
|
42
43
|
|
@@ -53,16 +54,16 @@ module ActiveStorageValidations
|
|
53
54
|
end
|
54
55
|
end
|
55
56
|
|
56
|
-
def set_attachable_cached_values(
|
57
|
-
@attachable_content_type =
|
58
|
-
@attachable_filename =
|
57
|
+
def set_attachable_cached_values(blob)
|
58
|
+
@attachable_content_type = blob.content_type
|
59
|
+
@attachable_filename = blob.filename.to_s
|
59
60
|
end
|
60
61
|
|
61
62
|
# Check if the provided content_type is authorized and not spoofed against
|
62
63
|
# the file io.
|
63
|
-
def is_valid?(record, attribute, attachable)
|
64
|
+
def is_valid?(record, attribute, attachable, blob)
|
64
65
|
authorized_content_type?(record, attribute, attachable) &&
|
65
|
-
not_spoofing_content_type?(record, attribute, attachable)
|
66
|
+
not_spoofing_content_type?(record, attribute, attachable, blob)
|
66
67
|
end
|
67
68
|
|
68
69
|
# Dead code that we keep here for some time, maybe we will find a solution
|
@@ -100,11 +101,19 @@ module ActiveStorageValidations
|
|
100
101
|
false
|
101
102
|
end
|
102
103
|
|
103
|
-
def
|
104
|
+
def marcel_attachable_content_type(attachable)
|
105
|
+
Marcel::MimeType.for(declared_type: @attachable_content_type, name: @attachable_filename)
|
106
|
+
end
|
107
|
+
|
108
|
+
def not_spoofing_content_type?(record, attribute, attachable, blob)
|
104
109
|
return true unless enable_spoofing_protection?
|
105
110
|
|
106
|
-
|
107
|
-
|
111
|
+
@detected_content_type = metadata_for(blob, attachable, METADATA_KEYS)&.fetch(:content_type, nil)
|
112
|
+
|
113
|
+
if attachable_content_type_vs_detected_content_type_mismatch?
|
114
|
+
errors_options = initialize_and_populate_error_options(options, attachable)
|
115
|
+
errors_options[:detected_content_type] = @detected_content_type
|
116
|
+
errors_options[:detected_human_content_type] = content_type_to_human_format(@detected_content_type)
|
108
117
|
add_error(record, attribute, ERROR_TYPES.second, **errors_options)
|
109
118
|
false
|
110
119
|
else
|
@@ -112,19 +121,40 @@ module ActiveStorageValidations
|
|
112
121
|
end
|
113
122
|
end
|
114
123
|
|
115
|
-
def
|
116
|
-
|
124
|
+
def disable_spoofing_protection?
|
125
|
+
!enable_spoofing_protection?
|
117
126
|
end
|
118
127
|
|
119
128
|
def enable_spoofing_protection?
|
120
129
|
options[:spoofing_protection] == true
|
121
130
|
end
|
122
131
|
|
132
|
+
def attachable_content_type_vs_detected_content_type_mismatch?
|
133
|
+
@attachable_content_type.present? &&
|
134
|
+
!attachable_content_type_intersects_detected_content_type?
|
135
|
+
end
|
136
|
+
|
137
|
+
def attachable_content_type_intersects_detected_content_type?
|
138
|
+
# Ruby intersects? method is only available from 3.1
|
139
|
+
enlarged_content_type(content_type_without_parameters(@attachable_content_type)).any? do |item|
|
140
|
+
enlarged_content_type(content_type_without_parameters(@detected_content_type)).include?(item)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def enlarged_content_type(content_type)
|
145
|
+
[content_type, *parent_content_types(content_type)].compact.uniq
|
146
|
+
end
|
147
|
+
|
148
|
+
def parent_content_types(content_type)
|
149
|
+
Marcel::TYPE_PARENTS[content_type] || []
|
150
|
+
end
|
151
|
+
|
123
152
|
def initialize_and_populate_error_options(options, attachable)
|
124
153
|
errors_options = initialize_error_options(options, attachable)
|
125
154
|
errors_options[:content_type] = @attachable_content_type
|
126
155
|
errors_options[:human_content_type] = content_type_to_human_format(@attachable_content_type)
|
127
|
-
errors_options[:
|
156
|
+
errors_options[:authorized_human_content_types] = content_type_to_human_format(@authorized_content_types)
|
157
|
+
errors_options[:count] = @authorized_content_types.size
|
128
158
|
errors_options
|
129
159
|
end
|
130
160
|
|
@@ -152,7 +182,7 @@ module ActiveStorageValidations
|
|
152
182
|
def ensure_content_types_validity
|
153
183
|
return true if options[:with]&.is_a?(Proc) || options[:in]&.is_a?(Proc)
|
154
184
|
|
155
|
-
(
|
185
|
+
(Array(options[:with]) + Array(options[:in])).each do |content_type|
|
156
186
|
raise ArgumentError, invalid_content_type_option_message(content_type) if invalid_option?(content_type)
|
157
187
|
end
|
158
188
|
end
|
@@ -181,6 +211,10 @@ module ActiveStorageValidations
|
|
181
211
|
end
|
182
212
|
|
183
213
|
def invalid_content_type?(content_type)
|
214
|
+
if content_type == 'image/jpg'
|
215
|
+
raise ArgumentError, "'image/jpg' is not a valid content type, you should use 'image/jpeg' instead"
|
216
|
+
end
|
217
|
+
|
184
218
|
Marcel::TYPE_EXTS[content_type.to_s] == nil
|
185
219
|
end
|
186
220
|
|
@@ -18,18 +18,19 @@ module ActiveStorageValidations
|
|
18
18
|
|
19
19
|
AVAILABLE_CHECKS = %i[width height min max].freeze
|
20
20
|
ERROR_TYPES = %i[
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
21
|
+
dimension_min_not_included_in
|
22
|
+
dimension_max_not_included_in
|
23
|
+
dimension_width_not_included_in
|
24
|
+
dimension_height_not_included_in
|
25
|
+
dimension_width_not_greater_than_or_equal_to
|
26
|
+
dimension_height_not_greater_than_or_equal_to
|
27
|
+
dimension_width_not_less_than_or_equal_to
|
28
|
+
dimension_height_not_less_than_or_equal_to
|
29
|
+
dimension_width_not_equal_to
|
30
|
+
dimension_height_not_equal_to
|
31
|
+
media_metadata_missing
|
32
32
|
].freeze
|
33
|
+
METADATA_KEYS = %i[width height].freeze
|
33
34
|
|
34
35
|
def check_validity!
|
35
36
|
unless AVAILABLE_CHECKS.any? { |argument| options.key?(argument) }
|
@@ -40,7 +41,7 @@ module ActiveStorageValidations
|
|
40
41
|
def validate_each(record, attribute, _value)
|
41
42
|
return if no_attachments?(record, attribute)
|
42
43
|
|
43
|
-
validate_changed_files_from_metadata(record, attribute)
|
44
|
+
validate_changed_files_from_metadata(record, attribute, METADATA_KEYS)
|
44
45
|
end
|
45
46
|
|
46
47
|
private
|
@@ -51,7 +52,7 @@ module ActiveStorageValidations
|
|
51
52
|
|
52
53
|
# Validation fails unless file metadata contains valid width and height.
|
53
54
|
if metadata[:width].to_i <= 0 || metadata[:height].to_i <= 0
|
54
|
-
add_error(record, attribute, :
|
55
|
+
add_error(record, attribute, :media_metadata_missing, **errors_options)
|
55
56
|
return false
|
56
57
|
end
|
57
58
|
|
@@ -64,7 +65,7 @@ module ActiveStorageValidations
|
|
64
65
|
errors_options[:width] = flat_options[:width][:min]
|
65
66
|
errors_options[:height] = flat_options[:height][:min]
|
66
67
|
|
67
|
-
add_error(record, attribute, :
|
68
|
+
add_error(record, attribute, :dimension_min_not_included_in, **errors_options)
|
68
69
|
return false
|
69
70
|
end
|
70
71
|
if flat_options[:max] && (
|
@@ -74,7 +75,7 @@ module ActiveStorageValidations
|
|
74
75
|
errors_options[:width] = flat_options[:width][:max]
|
75
76
|
errors_options[:height] = flat_options[:height][:max]
|
76
77
|
|
77
|
-
add_error(record, attribute, :
|
78
|
+
add_error(record, attribute, :dimension_max_not_included_in, **errors_options)
|
78
79
|
return false
|
79
80
|
end
|
80
81
|
|
@@ -86,7 +87,7 @@ module ActiveStorageValidations
|
|
86
87
|
next unless flat_options[length]
|
87
88
|
if flat_options[length].is_a?(Hash)
|
88
89
|
if flat_options[length][:in] && (metadata[length] < flat_options[length][:min] || metadata[length] > flat_options[length][:max])
|
89
|
-
error_type = :"dimension_#{length}
|
90
|
+
error_type = :"dimension_#{length}_not_included_in"
|
90
91
|
errors_options[:min] = flat_options[length][:min]
|
91
92
|
errors_options[:max] = flat_options[length][:max]
|
92
93
|
|
@@ -94,13 +95,13 @@ module ActiveStorageValidations
|
|
94
95
|
width_or_height_invalid = true
|
95
96
|
else
|
96
97
|
if flat_options[length][:min] && metadata[length] < flat_options[length][:min]
|
97
|
-
error_type = :"dimension_#{length}
|
98
|
+
error_type = :"dimension_#{length}_not_greater_than_or_equal_to"
|
98
99
|
errors_options[:length] = flat_options[length][:min]
|
99
100
|
|
100
101
|
add_error(record, attribute, error_type, **errors_options)
|
101
102
|
width_or_height_invalid = true
|
102
103
|
elsif flat_options[length][:max] && metadata[length] > flat_options[length][:max]
|
103
|
-
error_type = :"dimension_#{length}
|
104
|
+
error_type = :"dimension_#{length}_not_less_than_or_equal_to"
|
104
105
|
errors_options[:length] = flat_options[length][:max]
|
105
106
|
|
106
107
|
add_error(record, attribute, error_type, **errors_options)
|
@@ -109,7 +110,7 @@ module ActiveStorageValidations
|
|
109
110
|
end
|
110
111
|
else
|
111
112
|
if metadata[length] != flat_options[length]
|
112
|
-
error_type = :"dimension_#{length}
|
113
|
+
error_type = :"dimension_#{length}_not_equal_to"
|
113
114
|
errors_options[:length] = flat_options[length]
|
114
115
|
|
115
116
|
add_error(record, attribute, error_type, **errors_options)
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'base_comparison_validator'
|
4
|
+
|
5
|
+
module ActiveStorageValidations
|
6
|
+
class DurationValidator < BaseComparisonValidator
|
7
|
+
include ASVAnalyzable
|
8
|
+
include ASVAttachable
|
9
|
+
|
10
|
+
ERROR_TYPES = %i[
|
11
|
+
duration_not_less_than
|
12
|
+
duration_not_less_than_or_equal_to
|
13
|
+
duration_not_greater_than
|
14
|
+
duration_not_greater_than_or_equal_to
|
15
|
+
duration_not_between
|
16
|
+
].freeze
|
17
|
+
METADATA_KEYS = %i[duration].freeze
|
18
|
+
|
19
|
+
def validate_each(record, attribute, _value)
|
20
|
+
return if no_attachments?(record, attribute)
|
21
|
+
|
22
|
+
flat_options = set_flat_options(record)
|
23
|
+
|
24
|
+
attachables_and_blobs(record, attribute).each do |attachable, blob|
|
25
|
+
duration = metadata_for(blob, attachable, METADATA_KEYS)&.fetch(:duration, nil)
|
26
|
+
|
27
|
+
if duration.to_i <= 0
|
28
|
+
errors_options = initialize_error_options(options, attachable)
|
29
|
+
add_error(record, attribute, :media_metadata_missing, **errors_options)
|
30
|
+
next
|
31
|
+
end
|
32
|
+
|
33
|
+
next if is_valid?(duration, flat_options)
|
34
|
+
|
35
|
+
errors_options = initialize_error_options(options, attachable)
|
36
|
+
populate_error_options(errors_options, flat_options)
|
37
|
+
errors_options[:duration] = format_bound_value(duration)
|
38
|
+
|
39
|
+
keys = AVAILABLE_CHECKS & flat_options.keys
|
40
|
+
error_type = "duration_not_#{keys.first}".to_sym
|
41
|
+
|
42
|
+
add_error(record, attribute, error_type, **errors_options)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def format_bound_value(value)
|
49
|
+
return nil unless value
|
50
|
+
|
51
|
+
custom_value = value == value.to_i ? value.to_i : value
|
52
|
+
ActiveSupport::Duration.build(custom_value).inspect
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveStorageValidations
|
4
|
+
module ASVBlobMetadatable
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
def active_storage_validations_metadata
|
9
|
+
metadata.dig('custom', 'active_storage_validations') || {}
|
10
|
+
end
|
11
|
+
|
12
|
+
def active_storage_validations_metadata=(value)
|
13
|
+
metadata['custom'] ||= {}
|
14
|
+
metadata['custom']['active_storage_validations'] = value
|
15
|
+
end
|
16
|
+
|
17
|
+
def merge_into_active_storage_validations_metadata(new_data)
|
18
|
+
metadata['custom'] ||= {}
|
19
|
+
metadata['custom']['active_storage_validations'] ||= {}
|
20
|
+
metadata['custom']['active_storage_validations'].merge!(new_data)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -1,7 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "marcel"
|
4
|
+
|
3
5
|
Marcel::MimeType.extend "application/x-rar-compressed", parents: %(application/x-rar)
|
4
6
|
Marcel::MimeType.extend "audio/x-hx-aac-adts", parents: %(audio/x-aac)
|
5
7
|
Marcel::MimeType.extend "audio/x-m4a", parents: %(audio/mp4)
|
6
8
|
Marcel::MimeType.extend "text/xml", parents: %(application/xml) # alias
|
7
9
|
Marcel::MimeType.extend "video/theora", parents: %(video/ogg)
|
10
|
+
|
11
|
+
# Add empty content type
|
12
|
+
Marcel::MimeType.extend "inode/x-empty", extensions: %w(empty)
|
@@ -15,6 +15,8 @@ module ActiveStorageValidations
|
|
15
15
|
AVAILABLE_CHECKS = %i[max min].freeze
|
16
16
|
ERROR_TYPES = %i[
|
17
17
|
limit_out_of_range
|
18
|
+
limit_min_not_reached
|
19
|
+
limit_max_exceeded
|
18
20
|
].freeze
|
19
21
|
|
20
22
|
def check_validity!
|
@@ -25,13 +27,23 @@ module ActiveStorageValidations
|
|
25
27
|
def validate_each(record, attribute, _value)
|
26
28
|
files = attached_files(record, attribute).reject(&:blank?)
|
27
29
|
flat_options = set_flat_options(record)
|
30
|
+
count = files.count
|
28
31
|
|
29
|
-
return if files_count_valid?(
|
32
|
+
return if files_count_valid?(count, flat_options)
|
30
33
|
|
31
34
|
errors_options = initialize_error_options(options)
|
32
35
|
errors_options[:min] = flat_options[:min]
|
33
36
|
errors_options[:max] = flat_options[:max]
|
34
|
-
|
37
|
+
errors_options[:count] = count
|
38
|
+
error_type = if flat_options[:min] && flat_options[:max]
|
39
|
+
:limit_out_of_range
|
40
|
+
elsif flat_options[:min] && count < flat_options[:min]
|
41
|
+
:limit_min_not_reached
|
42
|
+
else
|
43
|
+
:limit_max_exceeded
|
44
|
+
end
|
45
|
+
|
46
|
+
add_error(record, attribute, error_type, **errors_options)
|
35
47
|
end
|
36
48
|
|
37
49
|
private
|
@@ -13,8 +13,8 @@ require_relative 'shared/asv_validatable'
|
|
13
13
|
|
14
14
|
module ActiveStorageValidations
|
15
15
|
module Matchers
|
16
|
-
class
|
17
|
-
#
|
16
|
+
class BaseComparisonValidatorMatcher
|
17
|
+
# BaseComparisonValidatorMatcher is an abstract class and shouldn't be instantiated directly.
|
18
18
|
|
19
19
|
include ASVActiveStorageable
|
20
20
|
include ASVAllowBlankable
|
@@ -33,23 +33,23 @@ module ActiveStorageValidations
|
|
33
33
|
@min = @max = nil
|
34
34
|
end
|
35
35
|
|
36
|
-
def less_than(
|
37
|
-
@max =
|
36
|
+
def less_than(value)
|
37
|
+
@max = value - smallest_measurement
|
38
38
|
self
|
39
39
|
end
|
40
40
|
|
41
|
-
def less_than_or_equal_to(
|
42
|
-
@max =
|
41
|
+
def less_than_or_equal_to(value)
|
42
|
+
@max = value
|
43
43
|
self
|
44
44
|
end
|
45
45
|
|
46
|
-
def greater_than(
|
47
|
-
@min =
|
46
|
+
def greater_than(value)
|
47
|
+
@min = value + smallest_measurement
|
48
48
|
self
|
49
49
|
end
|
50
50
|
|
51
|
-
def greater_than_or_equal_to(
|
52
|
-
@min =
|
51
|
+
def greater_than_or_equal_to(value)
|
52
|
+
@min = value
|
53
53
|
self
|
54
54
|
end
|
55
55
|
|
@@ -78,45 +78,53 @@ module ActiveStorageValidations
|
|
78
78
|
|
79
79
|
message << " but there seem to have issues with the matcher methods you used, since:"
|
80
80
|
@failure_message_artefacts.each do |error_case|
|
81
|
-
message << " validation failed when provided with a #{error_case[:
|
81
|
+
message << " validation failed when provided with a #{error_case[:value]} #{failure_message_unit} test file"
|
82
82
|
end
|
83
83
|
message << " whereas it should have passed"
|
84
84
|
end
|
85
85
|
|
86
|
+
def failure_message_unit
|
87
|
+
raise NotImplementedError
|
88
|
+
end
|
89
|
+
|
86
90
|
def not_lower_than_min?
|
87
|
-
@min.nil? || !
|
91
|
+
@min.nil? || !passes_validation_with_value(@min - 1)
|
88
92
|
end
|
89
93
|
|
90
94
|
def higher_than_min?
|
91
|
-
@min.nil? ||
|
95
|
+
@min.nil? || passes_validation_with_value(@min + 1)
|
92
96
|
end
|
93
97
|
|
94
98
|
def lower_than_max?
|
95
|
-
@max.nil? || @max == Float::INFINITY ||
|
99
|
+
@max.nil? || @max == Float::INFINITY || passes_validation_with_value(@max - 1)
|
96
100
|
end
|
97
101
|
|
98
102
|
def not_higher_than_max?
|
99
|
-
@max.nil? || @max == Float::INFINITY || !
|
103
|
+
@max.nil? || @max == Float::INFINITY || !passes_validation_with_value(@max + 1)
|
100
104
|
end
|
101
105
|
|
102
|
-
def
|
103
|
-
|
106
|
+
def smallest_measurement
|
107
|
+
raise NotImplementedError
|
108
|
+
end
|
109
|
+
|
110
|
+
def passes_validation_with_value(value)
|
111
|
+
mock_value_for(io, value) do
|
104
112
|
attach_file
|
105
113
|
validate
|
106
114
|
detach_file
|
107
|
-
is_valid? || add_failure_message_artefact(
|
115
|
+
is_valid? || add_failure_message_artefact(value)
|
108
116
|
end
|
109
117
|
end
|
110
118
|
|
111
|
-
def add_failure_message_artefact(
|
112
|
-
@failure_message_artefacts << {
|
119
|
+
def add_failure_message_artefact(value)
|
120
|
+
@failure_message_artefacts << { value: value }
|
113
121
|
false
|
114
122
|
end
|
115
123
|
|
116
124
|
def is_custom_message_valid?
|
117
125
|
return true unless @custom_message
|
118
126
|
|
119
|
-
|
127
|
+
mock_value_for(io, -smallest_measurement) do
|
120
128
|
attach_file
|
121
129
|
validate
|
122
130
|
detach_file
|
@@ -124,10 +132,8 @@ module ActiveStorageValidations
|
|
124
132
|
end
|
125
133
|
end
|
126
134
|
|
127
|
-
def
|
128
|
-
|
129
|
-
yield
|
130
|
-
end
|
135
|
+
def mock_value_for(io, size)
|
136
|
+
raise NotImplementedError
|
131
137
|
end
|
132
138
|
end
|
133
139
|
end
|
@@ -46,12 +46,12 @@ module ActiveStorageValidations
|
|
46
46
|
end
|
47
47
|
|
48
48
|
def allowing(*content_types)
|
49
|
-
@allowed_content_types = content_types.flatten
|
49
|
+
@allowed_content_types = content_types.map { |content_type| normalize_content_type(content_type) }.flatten
|
50
50
|
self
|
51
51
|
end
|
52
52
|
|
53
53
|
def rejecting(*content_types)
|
54
|
-
@rejected_content_types = content_types.flatten
|
54
|
+
@rejected_content_types = content_types.map { |content_type| normalize_content_type(content_type) }.flatten
|
55
55
|
self
|
56
56
|
end
|
57
57
|
|
@@ -88,6 +88,10 @@ module ActiveStorageValidations
|
|
88
88
|
end
|
89
89
|
end
|
90
90
|
|
91
|
+
def normalize_content_type(content_type)
|
92
|
+
Marcel::MimeType.for(declared_type: content_type.to_s, extension: content_type.to_s)
|
93
|
+
end
|
94
|
+
|
91
95
|
def all_allowed_content_types_allowed?
|
92
96
|
@allowed_content_types_not_allowed ||= @allowed_content_types.reject { |type| type_allowed?(type) }
|
93
97
|
@allowed_content_types_not_allowed.empty?
|
@@ -135,7 +139,7 @@ module ActiveStorageValidations
|
|
135
139
|
# (ie spoofed file basically), we need to ignore the error related to
|
136
140
|
# content type spoofing in our matcher to pass the tests
|
137
141
|
def validator_errors_for_attribute
|
138
|
-
super.reject { |hash| hash[:error] == :
|
142
|
+
super.reject { |hash| hash[:error] == :content_type_spoofed }
|
139
143
|
end
|
140
144
|
end
|
141
145
|
end
|
@@ -185,7 +185,7 @@ module ActiveStorageValidations
|
|
185
185
|
end
|
186
186
|
|
187
187
|
def mock_dimensions_for(attachment, width, height)
|
188
|
-
Matchers.mock_metadata(attachment, width, height) do
|
188
|
+
Matchers.mock_metadata(attachment, { width: width, height: height }) do
|
189
189
|
yield
|
190
190
|
end
|
191
191
|
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'base_comparison_validator_matcher'
|
4
|
+
|
5
|
+
module ActiveStorageValidations
|
6
|
+
module Matchers
|
7
|
+
def validate_duration_of(attribute_name)
|
8
|
+
DurationValidatorMatcher.new(attribute_name)
|
9
|
+
end
|
10
|
+
|
11
|
+
class DurationValidatorMatcher < BaseComparisonValidatorMatcher
|
12
|
+
def description
|
13
|
+
"validate file duration of :#{@attribute_name}"
|
14
|
+
end
|
15
|
+
|
16
|
+
def failure_message
|
17
|
+
message = ["is expected to validate file duration of :#{@attribute_name}"]
|
18
|
+
build_failure_message(message)
|
19
|
+
message.join("\n")
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def failure_message_unit
|
25
|
+
"seconds"
|
26
|
+
end
|
27
|
+
|
28
|
+
def smallest_measurement
|
29
|
+
1.second
|
30
|
+
end
|
31
|
+
|
32
|
+
def mock_value_for(io, duration)
|
33
|
+
Matchers.mock_metadata(io, { duration: duration }) do
|
34
|
+
yield
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|