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
@@ -0,0 +1,87 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'shared/asv_attachable'
|
4
|
+
require_relative 'shared/asv_loggable'
|
5
|
+
|
6
|
+
module ActiveStorageValidations
|
7
|
+
# = Active Storage Validations \Analyzer
|
8
|
+
#
|
9
|
+
# This is an abstract base class for analyzers, which extract metadata from attachables.
|
10
|
+
# See ActiveStorageValidations::Analyzer::VideoAnalyzer for an example of a concrete subclass.
|
11
|
+
#
|
12
|
+
# Heavily (not to say 100%) inspired by Rails own ActiveStorage::Analyzer
|
13
|
+
class Analyzer
|
14
|
+
include ASVAttachable
|
15
|
+
include ASVLoggable
|
16
|
+
|
17
|
+
attr_reader :attachable
|
18
|
+
|
19
|
+
def initialize(attachable)
|
20
|
+
@attachable = attachable
|
21
|
+
end
|
22
|
+
|
23
|
+
# Override this method in a concrete subclass. Have it return a String content type.
|
24
|
+
def content_type
|
25
|
+
raise NotImplementedError
|
26
|
+
end
|
27
|
+
|
28
|
+
# Override this method in a concrete subclass. Have it return a Hash of metadata.
|
29
|
+
def metadata
|
30
|
+
raise NotImplementedError
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
# Override this method in a concrete subclass. Have it yield a media object.
|
36
|
+
def read_media
|
37
|
+
raise NotImplementedError
|
38
|
+
end
|
39
|
+
|
40
|
+
def media(tempfile)
|
41
|
+
@media ||= case @attachable
|
42
|
+
when ActiveStorage::Blob, String
|
43
|
+
blob = @attachable.is_a?(String) ? ActiveStorage::Blob.find_signed!(@attachable) : @attachable
|
44
|
+
media_from_tempfile_path(tempfile, blob)
|
45
|
+
when Hash
|
46
|
+
io = @attachable[:io]
|
47
|
+
if io.is_a?(StringIO)
|
48
|
+
media_from_tempfile_path(tempfile, io)
|
49
|
+
else
|
50
|
+
File.open(io) do |file|
|
51
|
+
media_from_path(file.path)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
when ActionDispatch::Http::UploadedFile, Rack::Test::UploadedFile
|
55
|
+
media_from_path(@attachable.path)
|
56
|
+
when File
|
57
|
+
supports_file_attachment? ? media_from_path(@attachable.path) : raise_rails_like_error(@attachable)
|
58
|
+
when Pathname
|
59
|
+
supports_pathname_attachment? ? media_from_path(@attachable.to_s) : raise_rails_like_error(@attachable)
|
60
|
+
else
|
61
|
+
raise_rails_like_error(@attachable)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def media_from_tempfile_path(tempfile, file_representation)
|
66
|
+
if file_representation.is_a?(ActiveStorage::Blob)
|
67
|
+
file_representation.download { |chunk| tempfile.write(chunk) }
|
68
|
+
else
|
69
|
+
IO.copy_stream(file_representation, tempfile)
|
70
|
+
file_representation.rewind
|
71
|
+
end
|
72
|
+
|
73
|
+
tempfile.flush
|
74
|
+
tempfile.rewind
|
75
|
+
media_from_path(tempfile.path)
|
76
|
+
end
|
77
|
+
|
78
|
+
# Override this method in a concrete subclass. Have it return a media object.
|
79
|
+
def media_from_path(path)
|
80
|
+
raise NotImplementedError
|
81
|
+
end
|
82
|
+
|
83
|
+
def instrument(analyzer, &block)
|
84
|
+
ActiveSupport::Notifications.instrument("analyze.active_storage_validations", analyzer: analyzer, &block)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -1,99 +1,154 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require_relative '
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
def
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'shared/asv_active_storageable'
|
4
|
+
require_relative 'shared/asv_analyzable'
|
5
|
+
require_relative 'shared/asv_attachable'
|
6
|
+
require_relative 'shared/asv_errorable'
|
7
|
+
require_relative 'shared/asv_optionable'
|
8
|
+
require_relative 'shared/asv_symbolizable'
|
9
|
+
|
10
|
+
module ActiveStorageValidations
|
11
|
+
class AspectRatioValidator < ActiveModel::EachValidator # :nodoc
|
12
|
+
include ASVActiveStorageable
|
13
|
+
include ASVAnalyzable
|
14
|
+
include ASVAttachable
|
15
|
+
include ASVErrorable
|
16
|
+
include ASVOptionable
|
17
|
+
include ASVSymbolizable
|
18
|
+
|
19
|
+
AVAILABLE_CHECKS = %i[with in].freeze
|
20
|
+
NAMED_ASPECT_RATIOS = %i[square portrait landscape].freeze
|
21
|
+
ASPECT_RATIO_REGEX = /is_([1-9]\d*)_([1-9]\d*)/.freeze
|
22
|
+
ERROR_TYPES = %i[
|
23
|
+
aspect_ratio_not_square
|
24
|
+
aspect_ratio_not_portrait
|
25
|
+
aspect_ratio_not_landscape
|
26
|
+
aspect_ratio_not_x_y
|
27
|
+
aspect_ratio_invalid
|
28
|
+
media_metadata_missing
|
29
|
+
].freeze
|
30
|
+
PRECISION = 3.freeze
|
31
|
+
METADATA_KEYS = %i[width height].freeze
|
32
|
+
|
33
|
+
def check_validity!
|
34
|
+
ensure_at_least_one_validator_option
|
35
|
+
ensure_aspect_ratio_validity
|
36
|
+
end
|
37
|
+
|
38
|
+
def validate_each(record, attribute, _value)
|
39
|
+
return if no_attachments?(record, attribute)
|
40
|
+
|
41
|
+
flat_options = set_flat_options(record)
|
42
|
+
@authorized_aspect_ratios = authorized_aspect_ratios_from_options(flat_options).compact
|
43
|
+
return if @authorized_aspect_ratios.empty?
|
44
|
+
|
45
|
+
validate_changed_files_from_metadata(record, attribute, METADATA_KEYS)
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def is_valid?(record, attribute, attachable, metadata)
|
51
|
+
!media_metadata_missing?(record, attribute, attachable, metadata) &&
|
52
|
+
authorized_aspect_ratio?(record, attribute, attachable, metadata)
|
53
|
+
end
|
54
|
+
|
55
|
+
def authorized_aspect_ratio?(record, attribute, attachable, metadata)
|
56
|
+
attachable_aspect_ratio_is_authorized = @authorized_aspect_ratios.any? do |authorized_aspect_ratio|
|
57
|
+
case authorized_aspect_ratio
|
58
|
+
when :square then valid_square_aspect_ratio?(metadata)
|
59
|
+
when :portrait then valid_portrait_aspect_ratio?(metadata)
|
60
|
+
when :landscape then valid_landscape_aspect_ratio?(metadata)
|
61
|
+
when ASPECT_RATIO_REGEX then valid_regex_aspect_ratio?(authorized_aspect_ratio, metadata)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
return true if attachable_aspect_ratio_is_authorized
|
66
|
+
|
67
|
+
errors_options = initialize_error_options(options, attachable)
|
68
|
+
error_type = aspect_ratio_error_mapping
|
69
|
+
errors_options[:authorized_aspect_ratios] = string_aspect_ratios
|
70
|
+
errors_options[:width] = metadata[:width]
|
71
|
+
errors_options[:height] = metadata[:height]
|
72
|
+
add_error(record, attribute, error_type, **errors_options)
|
73
|
+
false
|
74
|
+
end
|
75
|
+
|
76
|
+
def aspect_ratio_error_mapping
|
77
|
+
return :aspect_ratio_invalid if @authorized_aspect_ratios.many?
|
78
|
+
|
79
|
+
aspect_ratio = @authorized_aspect_ratios.first
|
80
|
+
NAMED_ASPECT_RATIOS.include?(aspect_ratio) ? :"aspect_ratio_not_#{aspect_ratio}" : :aspect_ratio_not_x_y
|
81
|
+
end
|
82
|
+
|
83
|
+
def media_metadata_missing?(record, attribute, attachable, metadata)
|
84
|
+
return false if metadata[:width].to_i > 0 && metadata[:height].to_i > 0
|
85
|
+
|
86
|
+
errors_options = initialize_error_options(options, attachable)
|
87
|
+
errors_options[:authorized_aspect_ratios] = string_aspect_ratios
|
88
|
+
add_error(record, attribute, :media_metadata_missing, **errors_options)
|
89
|
+
true
|
90
|
+
end
|
91
|
+
|
92
|
+
def valid_square_aspect_ratio?(metadata)
|
93
|
+
metadata[:width] == metadata[:height]
|
94
|
+
end
|
95
|
+
|
96
|
+
def valid_portrait_aspect_ratio?(metadata)
|
97
|
+
metadata[:width] < metadata[:height]
|
98
|
+
end
|
99
|
+
|
100
|
+
def valid_landscape_aspect_ratio?(metadata)
|
101
|
+
metadata[:width] > metadata[:height]
|
102
|
+
end
|
103
|
+
|
104
|
+
def valid_regex_aspect_ratio?(aspect_ratio, metadata)
|
105
|
+
aspect_ratio =~ ASPECT_RATIO_REGEX
|
106
|
+
x = ::Regexp.last_match(1).to_i
|
107
|
+
y = ::Regexp.last_match(2).to_i
|
108
|
+
|
109
|
+
x > 0 && y > 0 && (x.to_f / y).round(PRECISION) == (metadata[:width].to_f / metadata[:height]).round(PRECISION)
|
110
|
+
end
|
111
|
+
|
112
|
+
def ensure_at_least_one_validator_option
|
113
|
+
return if AVAILABLE_CHECKS.any? { |argument| options.key?(argument) }
|
114
|
+
|
115
|
+
raise ArgumentError, 'You must pass either :with or :in to the validator'
|
116
|
+
end
|
117
|
+
|
118
|
+
def ensure_aspect_ratio_validity
|
119
|
+
return true if options[:with]&.is_a?(Proc) || options[:in]&.is_a?(Proc)
|
120
|
+
|
121
|
+
authorized_aspect_ratios_from_options(options).each do |aspect_ratio|
|
122
|
+
unless NAMED_ASPECT_RATIOS.include?(aspect_ratio) || aspect_ratio =~ ASPECT_RATIO_REGEX
|
123
|
+
raise ArgumentError, invalid_aspect_ratio_message
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def invalid_aspect_ratio_message
|
129
|
+
<<~ERROR_MESSAGE
|
130
|
+
You must pass a valid aspect ratio to the validator
|
131
|
+
It should either be a named aspect ratio (#{NAMED_ASPECT_RATIOS.join(', ')})
|
132
|
+
Or an aspect ratio like 'is_16_9' (matching /#{ASPECT_RATIO_REGEX.source}/)
|
133
|
+
ERROR_MESSAGE
|
134
|
+
end
|
135
|
+
|
136
|
+
def authorized_aspect_ratios_from_options(flat_options)
|
137
|
+
(Array.wrap(flat_options[:with]) + Array.wrap(flat_options[:in]))
|
138
|
+
end
|
139
|
+
|
140
|
+
def string_aspect_ratios
|
141
|
+
@authorized_aspect_ratios.map do |aspect_ratio|
|
142
|
+
if NAMED_ASPECT_RATIOS.include?(aspect_ratio)
|
143
|
+
aspect_ratio
|
144
|
+
else
|
145
|
+
aspect_ratio =~ ASPECT_RATIO_REGEX
|
146
|
+
x = ::Regexp.last_match(1).to_i
|
147
|
+
y = ::Regexp.last_match(2).to_i
|
148
|
+
|
149
|
+
"#{x}:#{y}"
|
150
|
+
end
|
151
|
+
end.join(', ')
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
@@ -1,14 +1,31 @@
|
|
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_symbolizable'
|
6
|
+
|
3
7
|
module ActiveStorageValidations
|
4
8
|
class AttachedValidator < ActiveModel::EachValidator # :nodoc:
|
5
|
-
|
6
|
-
|
9
|
+
include ASVActiveStorageable
|
10
|
+
include ASVErrorable
|
11
|
+
include ASVSymbolizable
|
12
|
+
|
13
|
+
ERROR_TYPES = %i[blank].freeze
|
7
14
|
|
8
|
-
|
9
|
-
|
15
|
+
def check_validity!
|
16
|
+
%i[allow_nil allow_blank].each do |not_authorized_option|
|
17
|
+
if options.include?(not_authorized_option)
|
18
|
+
raise ArgumentError, "You cannot pass the :#{not_authorized_option} option to the #{self.class.to_sym} validator"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def validate_each(record, attribute, _value)
|
24
|
+
return if attachments_present?(record, attribute) &&
|
25
|
+
will_have_attachments_after_save?(record, attribute)
|
10
26
|
|
11
|
-
|
27
|
+
errors_options = initialize_error_options(options)
|
28
|
+
add_error(record, attribute, ERROR_TYPES.first, **errors_options)
|
12
29
|
end
|
13
30
|
end
|
14
31
|
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
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
|
+
|
8
|
+
module ActiveStorageValidations
|
9
|
+
class BaseComparisonValidator < ActiveModel::EachValidator # :nodoc:
|
10
|
+
include ASVActiveStorageable
|
11
|
+
include ASVErrorable
|
12
|
+
include ASVOptionable
|
13
|
+
include ASVSymbolizable
|
14
|
+
|
15
|
+
AVAILABLE_CHECKS = %i[
|
16
|
+
less_than
|
17
|
+
less_than_or_equal_to
|
18
|
+
greater_than
|
19
|
+
greater_than_or_equal_to
|
20
|
+
between
|
21
|
+
].freeze
|
22
|
+
|
23
|
+
def initialize(*args)
|
24
|
+
if self.class == BaseComparisonValidator
|
25
|
+
raise NotImplementedError, 'BaseComparisonValidator is an abstract class and cannot be instantiated directly.'
|
26
|
+
end
|
27
|
+
super
|
28
|
+
end
|
29
|
+
|
30
|
+
def check_validity!
|
31
|
+
unless AVAILABLE_CHECKS.one? { |argument| options.key?(argument) }
|
32
|
+
raise ArgumentError, 'You must pass either :less_than(_or_equal_to), :greater_than(_or_equal_to), or :between to the validator'
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def is_valid?(value, flat_options)
|
39
|
+
return false if value < 0
|
40
|
+
|
41
|
+
if flat_options[:between].present?
|
42
|
+
flat_options[:between].include?(value)
|
43
|
+
elsif flat_options[:less_than].present?
|
44
|
+
value < flat_options[:less_than]
|
45
|
+
elsif flat_options[:less_than_or_equal_to].present?
|
46
|
+
value <= flat_options[:less_than_or_equal_to]
|
47
|
+
elsif flat_options[:greater_than].present?
|
48
|
+
value > flat_options[:greater_than]
|
49
|
+
elsif flat_options[:greater_than_or_equal_to].present?
|
50
|
+
value >= flat_options[:greater_than_or_equal_to]
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def populate_error_options(errors_options, flat_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
|
61
|
+
end
|
62
|
+
|
63
|
+
def min(flat_options)
|
64
|
+
flat_options[:between]&.min || flat_options[:greater_than] || flat_options[:greater_than_or_equal_to]
|
65
|
+
end
|
66
|
+
|
67
|
+
def max(flat_options)
|
68
|
+
flat_options[:between]&.max || flat_options[:less_than] || flat_options[:less_than_or_equal_to]
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|