active_storage_validations 0.9.7 → 2.0.2
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 +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
|