active_storage_validations 0.9.7 → 2.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +737 -229
- data/config/locales/da.yml +53 -0
- data/config/locales/de.yml +50 -19
- data/config/locales/en.yml +50 -19
- data/config/locales/es.yml +50 -19
- data/config/locales/fr.yml +50 -19
- data/config/locales/it.yml +50 -19
- data/config/locales/ja.yml +50 -19
- data/config/locales/nl.yml +50 -19
- data/config/locales/pl.yml +50 -19
- data/config/locales/pt-BR.yml +50 -19
- data/config/locales/ru.yml +50 -19
- data/config/locales/sv.yml +53 -0
- data/config/locales/tr.yml +50 -19
- data/config/locales/uk.yml +50 -19
- data/config/locales/vi.yml +50 -19
- data/config/locales/zh-CN.yml +53 -0
- 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 +47 -0
- data/lib/active_storage_validations/analyzer/image_analyzer/vips.rb +57 -0
- data/lib/active_storage_validations/analyzer/image_analyzer.rb +49 -0
- data/lib/active_storage_validations/analyzer/null_analyzer.rb +18 -0
- 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 +87 -0
- data/lib/active_storage_validations/aspect_ratio_validator.rb +154 -99
- data/lib/active_storage_validations/attached_validator.rb +22 -5
- data/lib/active_storage_validations/base_comparison_validator.rb +71 -0
- data/lib/active_storage_validations/content_type_validator.rb +206 -25
- data/lib/active_storage_validations/dimension_validator.rb +105 -82
- data/lib/active_storage_validations/duration_validator.rb +55 -0
- data/lib/active_storage_validations/extensors/asv_blob_metadatable.rb +49 -0
- data/lib/active_storage_validations/extensors/asv_marcelable.rb +12 -0
- data/lib/active_storage_validations/limit_validator.rb +75 -16
- data/lib/active_storage_validations/matchers/aspect_ratio_validator_matcher.rb +119 -0
- data/lib/active_storage_validations/matchers/attached_validator_matcher.rb +48 -25
- data/lib/active_storage_validations/matchers/base_comparison_validator_matcher.rb +140 -0
- data/lib/active_storage_validations/matchers/content_type_validator_matcher.rb +94 -59
- data/lib/active_storage_validations/matchers/dimension_validator_matcher.rb +97 -55
- data/lib/active_storage_validations/matchers/duration_validator_matcher.rb +39 -0
- data/lib/active_storage_validations/matchers/limit_validator_matcher.rb +127 -0
- data/lib/active_storage_validations/matchers/processable_file_validator_matcher.rb +78 -0
- data/lib/active_storage_validations/matchers/shared/asv_active_storageable.rb +19 -0
- data/lib/active_storage_validations/matchers/shared/asv_allow_blankable.rb +28 -0
- data/lib/active_storage_validations/matchers/shared/asv_attachable.rb +72 -0
- data/lib/active_storage_validations/matchers/shared/asv_contextable.rb +49 -0
- data/lib/active_storage_validations/matchers/shared/asv_messageable.rb +28 -0
- data/lib/active_storage_validations/matchers/shared/asv_rspecable.rb +27 -0
- data/lib/active_storage_validations/matchers/shared/asv_validatable.rb +56 -0
- data/lib/active_storage_validations/matchers/size_validator_matcher.rb +17 -71
- data/lib/active_storage_validations/matchers/total_size_validator_matcher.rb +47 -0
- data/lib/active_storage_validations/matchers.rb +11 -16
- data/lib/active_storage_validations/processable_file_validator.rb +37 -0
- data/lib/active_storage_validations/railtie.rb +11 -0
- data/lib/active_storage_validations/shared/asv_active_storageable.rb +30 -0
- data/lib/active_storage_validations/shared/asv_analyzable.rb +80 -0
- data/lib/active_storage_validations/shared/asv_attachable.rb +204 -0
- data/lib/active_storage_validations/shared/asv_errorable.rb +40 -0
- data/lib/active_storage_validations/shared/asv_loggable.rb +11 -0
- data/lib/active_storage_validations/shared/asv_optionable.rb +29 -0
- data/lib/active_storage_validations/shared/asv_symbolizable.rb +14 -0
- data/lib/active_storage_validations/size_validator.rb +24 -40
- data/lib/active_storage_validations/total_size_validator.rb +51 -0
- data/lib/active_storage_validations/version.rb +1 -1
- data/lib/active_storage_validations.rb +20 -6
- metadata +127 -21
- data/lib/active_storage_validations/metadata.rb +0 -123
@@ -1,33 +1,92 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative 'shared/asv_active_storageable'
|
4
|
+
require_relative 'shared/asv_errorable'
|
5
|
+
require_relative 'shared/asv_optionable'
|
6
|
+
require_relative 'shared/asv_symbolizable'
|
7
|
+
|
3
8
|
module ActiveStorageValidations
|
4
9
|
class LimitValidator < ActiveModel::EachValidator # :nodoc:
|
10
|
+
include ASVActiveStorageable
|
11
|
+
include ASVErrorable
|
12
|
+
include ASVOptionable
|
13
|
+
include ASVSymbolizable
|
14
|
+
|
5
15
|
AVAILABLE_CHECKS = %i[max min].freeze
|
16
|
+
ERROR_TYPES = %i[
|
17
|
+
limit_out_of_range
|
18
|
+
limit_min_not_reached
|
19
|
+
limit_max_exceeded
|
20
|
+
].freeze
|
6
21
|
|
7
22
|
def check_validity!
|
8
|
-
|
9
|
-
|
10
|
-
raise ArgumentError, 'You must pass either :max or :min to the validator'
|
23
|
+
ensure_at_least_one_validator_option
|
24
|
+
ensure_arguments_validity
|
11
25
|
end
|
12
26
|
|
13
|
-
def validate_each(record, attribute,
|
14
|
-
|
27
|
+
def validate_each(record, attribute, _value)
|
28
|
+
files = attached_files(record, attribute).reject(&:blank?)
|
29
|
+
flat_options = set_flat_options(record)
|
30
|
+
count = files.count
|
31
|
+
|
32
|
+
return if files_count_valid?(count, flat_options)
|
33
|
+
|
34
|
+
errors_options = initialize_error_options(options)
|
35
|
+
errors_options[:min] = flat_options[:min]
|
36
|
+
errors_options[:max] = flat_options[:max]
|
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)
|
47
|
+
end
|
15
48
|
|
16
|
-
|
17
|
-
errors_options = { min: options[:min], max: options[:max] }
|
49
|
+
private
|
18
50
|
|
19
|
-
|
20
|
-
|
51
|
+
def files_count_valid?(count, flat_options)
|
52
|
+
if flat_options[:max].present? && flat_options[:min].present?
|
53
|
+
count >= flat_options[:min] && count <= flat_options[:max]
|
54
|
+
elsif flat_options[:max].present?
|
55
|
+
count <= flat_options[:max]
|
56
|
+
elsif flat_options[:min].present?
|
57
|
+
count >= flat_options[:min]
|
58
|
+
end
|
21
59
|
end
|
22
60
|
|
23
|
-
def
|
24
|
-
|
25
|
-
|
26
|
-
elsif options[:max].present?
|
27
|
-
count <= options[:max]
|
28
|
-
elsif options[:min].present?
|
29
|
-
count >= options[:min]
|
61
|
+
def ensure_at_least_one_validator_option
|
62
|
+
unless AVAILABLE_CHECKS.any? { |argument| options.key?(argument) }
|
63
|
+
raise ArgumentError, 'You must pass either :max or :min to the validator'
|
30
64
|
end
|
31
65
|
end
|
66
|
+
|
67
|
+
def ensure_arguments_validity
|
68
|
+
return true if min_max_are_proc? || min_or_max_is_proc_and_other_not_present?
|
69
|
+
|
70
|
+
raise ArgumentError, 'You must pass integers to :min and :max' if min_or_max_defined_and_not_integer?
|
71
|
+
raise ArgumentError, 'You must pass a higher value to :max than to :min' if min_higher_than_max?
|
72
|
+
end
|
73
|
+
|
74
|
+
def min_max_are_proc?
|
75
|
+
options[:min]&.is_a?(Proc) && options[:max]&.is_a?(Proc)
|
76
|
+
end
|
77
|
+
|
78
|
+
def min_or_max_is_proc_and_other_not_present?
|
79
|
+
(options[:min]&.is_a?(Proc) && options[:max].nil?) ||
|
80
|
+
(options[:min].nil? && options[:max]&.is_a?(Proc))
|
81
|
+
end
|
82
|
+
|
83
|
+
def min_or_max_defined_and_not_integer?
|
84
|
+
(options.key?(:min) && !options[:min].is_a?(Integer)) ||
|
85
|
+
(options.key?(:max) && !options[:max].is_a?(Integer))
|
86
|
+
end
|
87
|
+
|
88
|
+
def min_higher_than_max?
|
89
|
+
options[:min] > options[:max] if options[:min].is_a?(Integer) && options[:max].is_a?(Integer)
|
90
|
+
end
|
32
91
|
end
|
33
92
|
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'shared/asv_active_storageable'
|
4
|
+
require_relative 'shared/asv_allow_blankable'
|
5
|
+
require_relative 'shared/asv_attachable'
|
6
|
+
require_relative 'shared/asv_contextable'
|
7
|
+
require_relative 'shared/asv_messageable'
|
8
|
+
require_relative 'shared/asv_rspecable'
|
9
|
+
require_relative 'shared/asv_validatable'
|
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 ASVActiveStorageable
|
19
|
+
include ASVAllowBlankable
|
20
|
+
include ASVAttachable
|
21
|
+
include ASVContextable
|
22
|
+
include ASVMessageable
|
23
|
+
include ASVRspecable
|
24
|
+
include ASVValidatable
|
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: width, height: 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
|
@@ -1,58 +1,81 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative 'shared/asv_active_storageable'
|
4
|
+
require_relative 'shared/asv_attachable'
|
5
|
+
require_relative 'shared/asv_contextable'
|
6
|
+
require_relative 'shared/asv_messageable'
|
7
|
+
require_relative 'shared/asv_rspecable'
|
8
|
+
require_relative 'shared/asv_validatable'
|
9
|
+
|
3
10
|
module ActiveStorageValidations
|
4
11
|
module Matchers
|
5
|
-
def validate_attached_of(
|
6
|
-
AttachedValidatorMatcher.new(
|
12
|
+
def validate_attached_of(attribute_name)
|
13
|
+
AttachedValidatorMatcher.new(attribute_name)
|
7
14
|
end
|
8
15
|
|
9
16
|
class AttachedValidatorMatcher
|
17
|
+
include ASVActiveStorageable
|
18
|
+
include ASVAttachable
|
19
|
+
include ASVContextable
|
20
|
+
include ASVMessageable
|
21
|
+
include ASVRspecable
|
22
|
+
include ASVValidatable
|
23
|
+
|
10
24
|
def initialize(attribute_name)
|
25
|
+
initialize_contextable
|
26
|
+
initialize_messageable
|
27
|
+
initialize_rspecable
|
11
28
|
@attribute_name = attribute_name
|
12
29
|
end
|
13
30
|
|
14
31
|
def description
|
15
|
-
"validate
|
32
|
+
"validate that :#{@attribute_name} must be attached"
|
33
|
+
end
|
34
|
+
|
35
|
+
def failure_message
|
36
|
+
"is expected to validate attachment of :#{@attribute_name}"
|
16
37
|
end
|
17
38
|
|
18
39
|
def matches?(subject)
|
19
40
|
@subject = subject.is_a?(Class) ? subject.new : subject
|
20
|
-
|
41
|
+
|
42
|
+
is_a_valid_active_storage_attribute? &&
|
43
|
+
is_context_valid? &&
|
44
|
+
is_custom_message_valid? &&
|
45
|
+
is_valid_when_file_attached? &&
|
46
|
+
is_invalid_when_file_not_attached?
|
21
47
|
end
|
22
48
|
|
23
|
-
|
24
|
-
|
49
|
+
private
|
50
|
+
|
51
|
+
def is_valid_when_file_attached?
|
52
|
+
attach_file unless file_attached?
|
53
|
+
validate
|
54
|
+
is_valid?
|
25
55
|
end
|
26
56
|
|
27
|
-
def
|
28
|
-
|
57
|
+
def is_invalid_when_file_not_attached?
|
58
|
+
detach_file if file_attached?
|
59
|
+
validate
|
60
|
+
!is_valid?
|
29
61
|
end
|
30
62
|
|
31
|
-
|
63
|
+
def is_custom_message_valid?
|
64
|
+
return true unless @custom_message
|
32
65
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
@subject.public_send(@attribute_name).respond_to?(:detach)
|
66
|
+
detach_file if file_attached?
|
67
|
+
validate
|
68
|
+
has_an_error_message_which_is_custom_message?
|
37
69
|
end
|
38
70
|
|
39
|
-
def
|
40
|
-
@subject.public_send(@attribute_name).
|
41
|
-
@subject.validate
|
42
|
-
@subject.errors.details[@attribute_name].exclude?(error: :blank)
|
71
|
+
def file_attached?
|
72
|
+
@subject.public_send(@attribute_name).attached?
|
43
73
|
end
|
44
74
|
|
45
|
-
def
|
75
|
+
def detach_file
|
46
76
|
@subject.public_send(@attribute_name).detach
|
47
77
|
# Unset the direct relation since `detach` on an unpersisted record does not set `attached?` to false.
|
48
78
|
@subject.public_send("#{@attribute_name}=", nil)
|
49
|
-
|
50
|
-
@subject.validate
|
51
|
-
@subject.errors.details[@attribute_name].include?(error: :blank)
|
52
|
-
end
|
53
|
-
|
54
|
-
def attachable
|
55
|
-
{ io: Tempfile.new('.'), filename: 'dummy.txt', content_type: 'text/plain' }
|
56
79
|
end
|
57
80
|
end
|
58
81
|
end
|
@@ -0,0 +1,140 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Big thank you to the paperclip validation matchers:
|
4
|
+
# https://github.com/thoughtbot/paperclip/blob/v6.1.0/lib/paperclip/matchers/validate_attachment_size_matcher.rb
|
5
|
+
|
6
|
+
require_relative 'shared/asv_active_storageable'
|
7
|
+
require_relative 'shared/asv_allow_blankable'
|
8
|
+
require_relative 'shared/asv_attachable'
|
9
|
+
require_relative 'shared/asv_contextable'
|
10
|
+
require_relative 'shared/asv_messageable'
|
11
|
+
require_relative 'shared/asv_rspecable'
|
12
|
+
require_relative 'shared/asv_validatable'
|
13
|
+
|
14
|
+
module ActiveStorageValidations
|
15
|
+
module Matchers
|
16
|
+
class BaseComparisonValidatorMatcher
|
17
|
+
# BaseComparisonValidatorMatcher is an abstract class and shouldn't be instantiated directly.
|
18
|
+
|
19
|
+
include ASVActiveStorageable
|
20
|
+
include ASVAllowBlankable
|
21
|
+
include ASVAttachable
|
22
|
+
include ASVContextable
|
23
|
+
include ASVMessageable
|
24
|
+
include ASVRspecable
|
25
|
+
include ASVValidatable
|
26
|
+
|
27
|
+
def initialize(attribute_name)
|
28
|
+
initialize_allow_blankable
|
29
|
+
initialize_contextable
|
30
|
+
initialize_messageable
|
31
|
+
initialize_rspecable
|
32
|
+
@attribute_name = attribute_name
|
33
|
+
@min = @max = nil
|
34
|
+
end
|
35
|
+
|
36
|
+
def less_than(value)
|
37
|
+
@max = value - smallest_measurement
|
38
|
+
self
|
39
|
+
end
|
40
|
+
|
41
|
+
def less_than_or_equal_to(value)
|
42
|
+
@max = value
|
43
|
+
self
|
44
|
+
end
|
45
|
+
|
46
|
+
def greater_than(value)
|
47
|
+
@min = value + smallest_measurement
|
48
|
+
self
|
49
|
+
end
|
50
|
+
|
51
|
+
def greater_than_or_equal_to(value)
|
52
|
+
@min = value
|
53
|
+
self
|
54
|
+
end
|
55
|
+
|
56
|
+
def between(range)
|
57
|
+
@min, @max = range.first, range.last
|
58
|
+
self
|
59
|
+
end
|
60
|
+
|
61
|
+
def matches?(subject)
|
62
|
+
@subject = subject.is_a?(Class) ? subject.new : subject
|
63
|
+
|
64
|
+
is_a_valid_active_storage_attribute? &&
|
65
|
+
is_context_valid? &&
|
66
|
+
is_allowing_blank? &&
|
67
|
+
is_custom_message_valid? &&
|
68
|
+
not_lower_than_min? &&
|
69
|
+
higher_than_min? &&
|
70
|
+
lower_than_max? &&
|
71
|
+
not_higher_than_max?
|
72
|
+
end
|
73
|
+
|
74
|
+
protected
|
75
|
+
|
76
|
+
def build_failure_message(message)
|
77
|
+
return unless @failure_message_artefacts.present?
|
78
|
+
|
79
|
+
message << " but there seem to have issues with the matcher methods you used, since:"
|
80
|
+
@failure_message_artefacts.each do |error_case|
|
81
|
+
message << " validation failed when provided with a #{error_case[:value]} #{failure_message_unit} test file"
|
82
|
+
end
|
83
|
+
message << " whereas it should have passed"
|
84
|
+
end
|
85
|
+
|
86
|
+
def failure_message_unit
|
87
|
+
raise NotImplementedError
|
88
|
+
end
|
89
|
+
|
90
|
+
def not_lower_than_min?
|
91
|
+
@min.nil? || !passes_validation_with_value(@min - 1)
|
92
|
+
end
|
93
|
+
|
94
|
+
def higher_than_min?
|
95
|
+
@min.nil? || passes_validation_with_value(@min + 1)
|
96
|
+
end
|
97
|
+
|
98
|
+
def lower_than_max?
|
99
|
+
@max.nil? || @max == Float::INFINITY || passes_validation_with_value(@max - 1)
|
100
|
+
end
|
101
|
+
|
102
|
+
def not_higher_than_max?
|
103
|
+
@max.nil? || @max == Float::INFINITY || !passes_validation_with_value(@max + 1)
|
104
|
+
end
|
105
|
+
|
106
|
+
def smallest_measurement
|
107
|
+
raise NotImplementedError
|
108
|
+
end
|
109
|
+
|
110
|
+
def passes_validation_with_value(value)
|
111
|
+
mock_value_for(io, value) do
|
112
|
+
attach_file
|
113
|
+
validate
|
114
|
+
detach_file
|
115
|
+
is_valid? || add_failure_message_artefact(value)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def add_failure_message_artefact(value)
|
120
|
+
@failure_message_artefacts << { value: value }
|
121
|
+
false
|
122
|
+
end
|
123
|
+
|
124
|
+
def is_custom_message_valid?
|
125
|
+
return true unless @custom_message
|
126
|
+
|
127
|
+
mock_value_for(io, -smallest_measurement) do
|
128
|
+
attach_file
|
129
|
+
validate
|
130
|
+
detach_file
|
131
|
+
has_an_error_message_which_is_custom_message?
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def mock_value_for(io, size)
|
136
|
+
raise NotImplementedError
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
@@ -2,109 +2,144 @@
|
|
2
2
|
|
3
3
|
# Big thank you to the paperclip validation matchers:
|
4
4
|
# https://github.com/thoughtbot/paperclip/blob/v6.1.0/lib/paperclip/matchers/validate_attachment_content_type_matcher.rb
|
5
|
+
|
6
|
+
require_relative 'shared/asv_active_storageable'
|
7
|
+
require_relative 'shared/asv_allow_blankable'
|
8
|
+
require_relative 'shared/asv_attachable'
|
9
|
+
require_relative 'shared/asv_contextable'
|
10
|
+
require_relative 'shared/asv_messageable'
|
11
|
+
require_relative 'shared/asv_rspecable'
|
12
|
+
require_relative 'shared/asv_validatable'
|
13
|
+
|
5
14
|
module ActiveStorageValidations
|
6
15
|
module Matchers
|
7
|
-
def validate_content_type_of(
|
8
|
-
ContentTypeValidatorMatcher.new(
|
16
|
+
def validate_content_type_of(attribute_name)
|
17
|
+
ContentTypeValidatorMatcher.new(attribute_name)
|
9
18
|
end
|
10
19
|
|
11
20
|
class ContentTypeValidatorMatcher
|
21
|
+
include ASVActiveStorageable
|
22
|
+
include ASVAllowBlankable
|
23
|
+
include ASVAttachable
|
24
|
+
include ASVContextable
|
25
|
+
include ASVMessageable
|
26
|
+
include ASVRspecable
|
27
|
+
include ASVValidatable
|
28
|
+
|
12
29
|
def initialize(attribute_name)
|
30
|
+
initialize_allow_blankable
|
31
|
+
initialize_contextable
|
32
|
+
initialize_messageable
|
33
|
+
initialize_rspecable
|
13
34
|
@attribute_name = attribute_name
|
35
|
+
@allowed_content_types = @rejected_content_types = []
|
14
36
|
end
|
15
37
|
|
16
38
|
def description
|
17
|
-
"validate the content types allowed on
|
39
|
+
"validate the content types allowed on :#{@attribute_name}"
|
40
|
+
end
|
41
|
+
|
42
|
+
def failure_message
|
43
|
+
message = ["is expected to validate the content types of :#{@attribute_name}"]
|
44
|
+
build_failure_message(message)
|
45
|
+
message.join("\n")
|
18
46
|
end
|
19
47
|
|
20
|
-
def allowing(*
|
21
|
-
@
|
48
|
+
def allowing(*content_types)
|
49
|
+
@allowed_content_types = content_types.map { |content_type| normalize_content_type(content_type) }.flatten
|
22
50
|
self
|
23
51
|
end
|
24
52
|
|
25
|
-
def rejecting(*
|
26
|
-
@
|
53
|
+
def rejecting(*content_types)
|
54
|
+
@rejected_content_types = content_types.map { |content_type| normalize_content_type(content_type) }.flatten
|
27
55
|
self
|
28
56
|
end
|
29
57
|
|
30
58
|
def matches?(subject)
|
31
59
|
@subject = subject.is_a?(Class) ? subject.new : subject
|
32
|
-
responds_to_methods && allowed_types_allowed? && rejected_types_rejected?
|
33
|
-
end
|
34
|
-
|
35
|
-
def failure_message
|
36
|
-
<<~MESSAGE
|
37
|
-
Expected #{@attribute_name}
|
38
60
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
61
|
+
is_a_valid_active_storage_attribute? &&
|
62
|
+
is_context_valid? &&
|
63
|
+
is_allowing_blank? &&
|
64
|
+
is_custom_message_valid? &&
|
65
|
+
all_allowed_content_types_allowed? &&
|
66
|
+
all_rejected_content_types_rejected?
|
45
67
|
end
|
46
68
|
|
47
69
|
protected
|
48
70
|
|
49
|
-
def
|
50
|
-
@
|
51
|
-
@
|
52
|
-
|
71
|
+
def build_failure_message(message)
|
72
|
+
if @allowed_content_types_not_allowed.present?
|
73
|
+
message << " the following content type#{'s' if @allowed_content_types.count > 1} should be allowed: :#{@allowed_content_types.join(", :")}"
|
74
|
+
message << " but #{pluralize(@allowed_content_types_not_allowed)} rejected"
|
75
|
+
end
|
76
|
+
|
77
|
+
if @rejected_content_types_not_rejected.present?
|
78
|
+
message << " the following content type#{'s' if @rejected_content_types.count > 1} should be rejected: :#{@rejected_content_types.join(", :")}"
|
79
|
+
message << " but #{pluralize(@rejected_content_types_not_rejected)} accepted"
|
80
|
+
end
|
53
81
|
end
|
54
82
|
|
55
|
-
def
|
56
|
-
|
83
|
+
def pluralize(types)
|
84
|
+
if types.count == 1
|
85
|
+
":#{types[0]} was"
|
86
|
+
else
|
87
|
+
":#{types.join(", :")} were"
|
88
|
+
end
|
57
89
|
end
|
58
90
|
|
59
|
-
def
|
60
|
-
|
91
|
+
def normalize_content_type(content_type)
|
92
|
+
Marcel::MimeType.for(declared_type: content_type.to_s, extension: content_type.to_s)
|
61
93
|
end
|
62
94
|
|
63
|
-
def
|
64
|
-
@
|
65
|
-
@
|
95
|
+
def all_allowed_content_types_allowed?
|
96
|
+
@allowed_content_types_not_allowed ||= @allowed_content_types.reject { |type| type_allowed?(type) }
|
97
|
+
@allowed_content_types_not_allowed.empty?
|
66
98
|
end
|
67
99
|
|
68
|
-
def
|
69
|
-
@
|
70
|
-
@
|
100
|
+
def all_rejected_content_types_rejected?
|
101
|
+
@rejected_content_types_not_rejected ||= @rejected_content_types.select { |type| type_allowed?(type) }
|
102
|
+
@rejected_content_types_not_rejected.empty?
|
71
103
|
end
|
72
104
|
|
73
|
-
def
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
end
|
105
|
+
def type_allowed?(content_type)
|
106
|
+
attach_file_with_content_type(content_type)
|
107
|
+
validate
|
108
|
+
detach_file
|
109
|
+
is_valid?
|
79
110
|
end
|
80
111
|
|
81
|
-
def
|
82
|
-
|
83
|
-
"#{@missing_rejected_types.join(", ")} were accepted."
|
84
|
-
else
|
85
|
-
"All were rejected successfully."
|
86
|
-
end
|
112
|
+
def attach_file_with_content_type(content_type)
|
113
|
+
@subject.public_send(@attribute_name).attach(attachment_for(content_type))
|
87
114
|
end
|
88
115
|
|
89
|
-
def
|
90
|
-
@
|
91
|
-
|
92
|
-
|
116
|
+
def is_custom_message_valid?
|
117
|
+
return true unless @custom_message
|
118
|
+
|
119
|
+
attach_invalid_content_type_file
|
120
|
+
validate
|
121
|
+
has_an_error_message_which_is_custom_message?
|
93
122
|
end
|
94
123
|
|
95
|
-
def
|
96
|
-
|
97
|
-
{ io: Tempfile.new('.'), filename: "test.#{suffix}", content_type: type }
|
124
|
+
def attach_invalid_content_type_file
|
125
|
+
@subject.public_send(@attribute_name).attach(attachment_for('fake/fake'))
|
98
126
|
end
|
99
127
|
|
100
|
-
|
128
|
+
def attachment_for(content_type)
|
129
|
+
suffix = Marcel::TYPE_EXTS[content_type.to_s]&.first || 'fake'
|
101
130
|
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
131
|
+
{
|
132
|
+
io: Tempfile.new('.'),
|
133
|
+
filename: "test.#{suffix}",
|
134
|
+
content_type: content_type
|
135
|
+
}
|
136
|
+
end
|
137
|
+
|
138
|
+
# Due to the way we build test attachments in #attachment_for
|
139
|
+
# (ie spoofed file basically), we need to ignore the error related to
|
140
|
+
# content type spoofing in our matcher to pass the tests
|
141
|
+
def validator_errors_for_attribute
|
142
|
+
super.reject { |hash| hash[:error] == :content_type_spoofed }
|
108
143
|
end
|
109
144
|
end
|
110
145
|
end
|