active_storage_validations 1.0.4 → 3.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.
Files changed (75) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +785 -245
  3. data/config/locales/da.yml +63 -0
  4. data/config/locales/de.yml +60 -19
  5. data/config/locales/en-GB.yml +63 -0
  6. data/config/locales/en.yml +60 -20
  7. data/config/locales/es.yml +60 -19
  8. data/config/locales/fr.yml +60 -19
  9. data/config/locales/it.yml +60 -19
  10. data/config/locales/ja.yml +60 -19
  11. data/config/locales/nl.yml +60 -19
  12. data/config/locales/pl.yml +60 -19
  13. data/config/locales/pt-BR.yml +60 -19
  14. data/config/locales/ru.yml +60 -19
  15. data/config/locales/sv.yml +63 -0
  16. data/config/locales/tr.yml +60 -19
  17. data/config/locales/uk.yml +60 -19
  18. data/config/locales/vi.yml +60 -19
  19. data/config/locales/zh-CN.yml +60 -19
  20. data/lib/active_storage_validations/analyzer/audio_analyzer.rb +58 -0
  21. data/lib/active_storage_validations/analyzer/content_type_analyzer.rb +60 -0
  22. data/lib/active_storage_validations/analyzer/image_analyzer/image_magick.rb +46 -0
  23. data/lib/active_storage_validations/analyzer/image_analyzer/vips.rb +56 -0
  24. data/lib/active_storage_validations/analyzer/image_analyzer.rb +49 -0
  25. data/lib/active_storage_validations/analyzer/null_analyzer.rb +18 -0
  26. data/lib/active_storage_validations/analyzer/pdf_analyzer.rb +89 -0
  27. data/lib/active_storage_validations/analyzer/shared/asv_ff_probable.rb +61 -0
  28. data/lib/active_storage_validations/analyzer/video_analyzer.rb +130 -0
  29. data/lib/active_storage_validations/analyzer.rb +88 -0
  30. data/lib/active_storage_validations/aspect_ratio_validator.rb +157 -97
  31. data/lib/active_storage_validations/attached_validator.rb +22 -5
  32. data/lib/active_storage_validations/base_comparison_validator.rb +83 -0
  33. data/lib/active_storage_validations/content_type_validator.rb +219 -31
  34. data/lib/active_storage_validations/dimension_validator.rb +187 -97
  35. data/lib/active_storage_validations/duration_validator.rb +70 -0
  36. data/lib/active_storage_validations/extensors/asv_blob_metadatable.rb +56 -0
  37. data/lib/active_storage_validations/extensors/asv_marcelable.rb +12 -0
  38. data/lib/active_storage_validations/limit_validator.rb +76 -9
  39. data/lib/active_storage_validations/matchers/aspect_ratio_validator_matcher.rb +119 -0
  40. data/lib/active_storage_validations/matchers/attached_validator_matcher.rb +48 -25
  41. data/lib/active_storage_validations/matchers/base_comparison_validator_matcher.rb +150 -0
  42. data/lib/active_storage_validations/matchers/content_type_validator_matcher.rb +98 -39
  43. data/lib/active_storage_validations/matchers/dimension_validator_matcher.rb +93 -55
  44. data/lib/active_storage_validations/matchers/duration_validator_matcher.rb +39 -0
  45. data/lib/active_storage_validations/matchers/limit_validator_matcher.rb +127 -0
  46. data/lib/active_storage_validations/matchers/pages_validator_matcher.rb +39 -0
  47. data/lib/active_storage_validations/matchers/processable_file_validator_matcher.rb +78 -0
  48. data/lib/active_storage_validations/matchers/shared/asv_active_storageable.rb +19 -0
  49. data/lib/active_storage_validations/matchers/shared/asv_allow_blankable.rb +28 -0
  50. data/lib/active_storage_validations/matchers/shared/asv_attachable.rb +72 -0
  51. data/lib/active_storage_validations/matchers/shared/asv_contextable.rb +57 -0
  52. data/lib/active_storage_validations/matchers/shared/asv_messageable.rb +28 -0
  53. data/lib/active_storage_validations/matchers/shared/asv_rspecable.rb +27 -0
  54. data/lib/active_storage_validations/matchers/shared/asv_validatable.rb +56 -0
  55. data/lib/active_storage_validations/matchers/size_validator_matcher.rb +17 -71
  56. data/lib/active_storage_validations/matchers/total_size_validator_matcher.rb +47 -0
  57. data/lib/active_storage_validations/matchers.rb +17 -21
  58. data/lib/active_storage_validations/pages_validator.rb +61 -0
  59. data/lib/active_storage_validations/processable_file_validator.rb +37 -0
  60. data/lib/active_storage_validations/railtie.rb +14 -0
  61. data/lib/active_storage_validations/shared/asv_active_storageable.rb +30 -0
  62. data/lib/active_storage_validations/shared/asv_analyzable.rb +89 -0
  63. data/lib/active_storage_validations/shared/asv_attachable.rb +236 -0
  64. data/lib/active_storage_validations/shared/asv_errorable.rb +64 -0
  65. data/lib/active_storage_validations/shared/asv_loggable.rb +11 -0
  66. data/lib/active_storage_validations/shared/asv_optionable.rb +29 -0
  67. data/lib/active_storage_validations/shared/asv_symbolizable.rb +14 -0
  68. data/lib/active_storage_validations/size_validator.rb +24 -41
  69. data/lib/active_storage_validations/total_size_validator.rb +52 -0
  70. data/lib/active_storage_validations/version.rb +1 -1
  71. data/lib/active_storage_validations.rb +27 -13
  72. metadata +113 -31
  73. data/lib/active_storage_validations/metadata.rb +0 -151
  74. data/lib/active_storage_validations/option_proc_unfolding.rb +0 -16
  75. data/lib/active_storage_validations/processable_image_validator.rb +0 -43
@@ -1,136 +1,226 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'metadata.rb'
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"
4
9
 
5
10
  module ActiveStorageValidations
6
11
  class DimensionValidator < ActiveModel::EachValidator # :nodoc
7
- include OptionProcUnfolding
12
+ include ASVActiveStorageable
13
+ include ASVAnalyzable
14
+ include ASVAttachable
15
+ include ASVErrorable
16
+ include ASVOptionable
17
+ include ASVSymbolizable
8
18
 
9
19
  AVAILABLE_CHECKS = %i[width height min max].freeze
20
+ ERROR_TYPES = %i[
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
+ ].freeze
33
+ METADATA_KEYS = %i[width height].freeze
10
34
 
11
- def process_options(record)
12
- flat_options = unfold_procs(record, self.options, AVAILABLE_CHECKS)
35
+ def check_validity!
36
+ ensure_at_least_one_validator_option
37
+ ensure_dimension_in_option_validity
38
+ ensure_min_max_option_validity
39
+ end
13
40
 
14
- [:width, :height].each do |length|
15
- if flat_options[length] and flat_options[length].is_a?(Hash)
16
- if (range = flat_options[length][:in])
17
- raise ArgumentError, ":in must be a Range" unless range.is_a?(Range)
18
- flat_options[length][:min], flat_options[length][:max] = range.min, range.max
19
- end
41
+ def validate_each(record, attribute, _value)
42
+ return if no_attachments?(record, attribute)
43
+
44
+ validate_changed_files_from_metadata(record, attribute, METADATA_KEYS)
45
+ end
46
+
47
+ private
48
+
49
+ def ensure_at_least_one_validator_option
50
+ unless AVAILABLE_CHECKS.any? { |argument| options.key?(argument) }
51
+ raise ArgumentError, "You must pass either :width, :height, :min or :max to the validator"
52
+ end
53
+ end
54
+
55
+ def ensure_dimension_in_option_validity
56
+ %i[width height].each do |dimension|
57
+ if options[dimension]&.is_a?(Hash) && options[dimension][:in].present?
58
+ raise ArgumentError, "{ #{dimension}: { in: value } } value must be a Range (min..max)" if !options[dimension][:in].is_a?(Range) && !options[dimension][:in].is_a?(Proc)
20
59
  end
21
60
  end
22
- [:min, :max].each do |dim|
23
- if (range = flat_options[dim])
24
- raise ArgumentError, ":#{dim} must be a Range (width..height)" unless range.is_a?(Range)
25
- flat_options[:width] = { dim => range.first }
26
- flat_options[:height] = { dim => range.last }
61
+ end
62
+
63
+ def ensure_min_max_option_validity
64
+ %i[min max].each do |bound|
65
+ if options[bound].present?
66
+ raise ArgumentError, "{ #{bound}: value } value must be a Range (#{bound}_width..#{bound}_height)" if !options[bound]&.is_a?(Range) && !options[bound]&.is_a?(Proc)
27
67
  end
28
68
  end
69
+ end
29
70
 
30
- flat_options
71
+ def is_valid?(record, attribute, file, metadata)
72
+ flat_options = process_options(record)
73
+ errors_options = initialize_error_options(options, file)
74
+
75
+ return add_media_metadata_missing_error(record, attribute, file, errors_options) unless valid_metadata?(metadata)
76
+
77
+ if min_max_validation?(flat_options)
78
+ validate_min_max(record, attribute, metadata, flat_options, errors_options)
79
+ else
80
+ validate_width_height(record, attribute, metadata, flat_options, errors_options)
81
+ end
31
82
  end
32
83
 
84
+ def valid_metadata?(metadata)
85
+ metadata[:width].to_i > 0 && metadata[:height].to_i > 0
86
+ end
33
87
 
34
- def check_validity!
35
- return true if AVAILABLE_CHECKS.any? { |argument| options.key?(argument) }
36
- raise ArgumentError, 'You must pass either :width, :height, :min or :max to the validator'
88
+ def min_max_validation?(flat_options)
89
+ flat_options[:min] || flat_options[:max]
37
90
  end
38
91
 
92
+ def validate_min_max(record, attribute, metadata, flat_options, errors_options)
93
+ return false unless validate_min(record, attribute, metadata, flat_options, errors_options)
94
+ return false unless validate_max(record, attribute, metadata, flat_options, errors_options)
39
95
 
40
- if Rails.gem_version >= Gem::Version.new('6.0.0')
41
- def validate_each(record, attribute, _value)
42
- return true unless record.send(attribute).attached?
96
+ true
97
+ end
43
98
 
44
- changes = record.attachment_changes[attribute.to_s]
45
- return true if changes.blank?
99
+ def validate_width_height(record, attribute, metadata, flat_options, errors_options)
100
+ %i[width height].each do |dimension|
101
+ next unless flat_options[dimension]
46
102
 
47
- files = Array.wrap(changes.is_a?(ActiveStorage::Attached::Changes::CreateMany) ? changes.attachables : changes.attachable)
48
- files.each do |file|
49
- metadata = Metadata.new(file).metadata
50
- next if is_valid?(record, attribute, metadata)
51
- break
52
- end
53
- end
54
- else
55
- # Rails 5
56
- def validate_each(record, attribute, _value)
57
- return true unless record.send(attribute).attached?
58
-
59
- files = Array.wrap(record.send(attribute))
60
- files.each do |file|
61
- # Analyze file first if not analyzed to get all required metadata.
62
- file.analyze; file.reload unless file.analyzed?
63
- metadata = file.metadata rescue {}
64
- next if is_valid?(record, attribute, metadata)
65
- break
103
+ if flat_options[dimension].is_a?(Hash)
104
+ validate_range(record, attribute, dimension, metadata, flat_options, errors_options)
105
+ else
106
+ validate_exact(record, attribute, dimension, metadata, flat_options, errors_options)
66
107
  end
67
108
  end
68
109
  end
69
110
 
70
-
71
- def is_valid?(record, attribute, file_metadata)
72
- flat_options = process_options(record)
73
- # Validation fails unless file metadata contains valid width and height.
74
- if file_metadata[:width].to_i <= 0 || file_metadata[:height].to_i <= 0
75
- add_error(record, attribute, :image_metadata_missing)
76
- return false
77
- end
78
-
79
- # Validation based on checks :min and :max (:min, :max has higher priority to :width, :height).
80
- if flat_options[:min] || flat_options[:max]
81
- if flat_options[:min] && (
82
- (flat_options[:width][:min] && file_metadata[:width] < flat_options[:width][:min]) ||
83
- (flat_options[:height][:min] && file_metadata[:height] < flat_options[:height][:min])
84
- )
85
- add_error(record, attribute, :dimension_min_inclusion, width: flat_options[:width][:min], height: flat_options[:height][:min])
86
- return false
111
+ # rubocop:disable Metrics/BlockLength
112
+ %i[min max].each do |bound|
113
+ define_method("validate_#{bound}") do |record, attribute, metadata, flat_options, errors_options|
114
+ if send(:"invalid_#{bound}?", flat_options, metadata)
115
+ send(:"add_#{bound}_error", record, attribute, flat_options, errors_options)
116
+ false
117
+ else
118
+ true
87
119
  end
88
- if flat_options[:max] && (
89
- (flat_options[:width][:max] && file_metadata[:width] > flat_options[:width][:max]) ||
90
- (flat_options[:height][:max] && file_metadata[:height] > flat_options[:height][:max])
91
- )
92
- add_error(record, attribute, :dimension_max_inclusion, width: flat_options[:width][:max], height: flat_options[:height][:max])
93
- return false
120
+ end
121
+
122
+ define_method("validate_dimension_#{bound}") do |record, attribute, dimension, metadata, flat_options, errors_options|
123
+ if send(:"invalid_dimension_#{bound}?", flat_options, dimension, metadata)
124
+ send(:"add_dimension_#{bound}_error", record, attribute, dimension, flat_options, errors_options)
125
+ false
126
+ else
127
+ true
94
128
  end
129
+ end
130
+
131
+ define_method("invalid_#{bound}?") do |flat_options, metadata|
132
+ flat_options[bound] && (
133
+ send(:"invalid_dimension_#{bound}?", flat_options, :width, metadata) ||
134
+ send(:"invalid_dimension_#{bound}?", flat_options, :height, metadata)
135
+ )
136
+ end
137
+
138
+ define_method("invalid_dimension_#{bound}?") do |flat_options, dimension, metadata|
139
+ flat_options[dimension][bound] && metadata[dimension].public_send(bound == :min ? :< : :>, flat_options[dimension][bound])
140
+ end
141
+
142
+ define_method("add_#{bound}_error") do |record, attribute, flat_options, errors_options|
143
+ errors_options[:width] = flat_options[:width][bound]
144
+ errors_options[:height] = flat_options[:height][bound]
145
+ add_error(record, attribute, :"dimension_#{bound}_not_included_in", **errors_options)
146
+ end
95
147
 
96
- # Validation based on checks :width and :height.
148
+ define_method("add_dimension_#{bound}_error") do |record, attribute, dimension, flat_options, errors_options|
149
+ error_type = bound == :min ? :not_greater_than_or_equal_to : :not_less_than_or_equal_to
150
+ errors_options[:length] = flat_options[dimension][bound]
151
+ add_error(record, attribute, :"dimension_#{dimension}_#{error_type}", **errors_options)
152
+ end
153
+ end
154
+ # rubocop:enable Metrics/BlockLength
155
+
156
+ def validate_range(record, attribute, dimension, metadata, flat_options, errors_options)
157
+ if in_option_used?(flat_options, dimension)
158
+ return false unless validate_in(record, attribute, dimension, metadata, flat_options, errors_options)
97
159
  else
98
- width_or_height_invalid = false
99
- [:width, :height].each do |length|
100
- next unless flat_options[length]
101
- if flat_options[length].is_a?(Hash)
102
- if flat_options[length][:in] && (file_metadata[length] < flat_options[length][:min] || file_metadata[length] > flat_options[length][:max])
103
- add_error(record, attribute, :"dimension_#{length}_inclusion", min: flat_options[length][:min], max: flat_options[length][:max])
104
- width_or_height_invalid = true
105
- else
106
- if flat_options[length][:min] && file_metadata[length] < flat_options[length][:min]
107
- add_error(record, attribute, :"dimension_#{length}_greater_than_or_equal_to", length: flat_options[length][:min])
108
- width_or_height_invalid = true
109
- end
110
- if flat_options[length][:max] && file_metadata[length] > flat_options[length][:max]
111
- add_error(record, attribute, :"dimension_#{length}_less_than_or_equal_to", length: flat_options[length][:max])
112
- width_or_height_invalid = true
113
- end
114
- end
115
- else
116
- if file_metadata[length] != flat_options[length]
117
- add_error(record, attribute, :"dimension_#{length}_equal_to", length: flat_options[length])
118
- width_or_height_invalid = true
119
- end
120
- end
121
- end
160
+ return false unless validate_dimension_min_max(record, attribute, dimension, metadata, flat_options, errors_options)
161
+ end
122
162
 
123
- return false if width_or_height_invalid
163
+ true
164
+ end
165
+
166
+ def validate_in(record, attribute, dimension, metadata, flat_options, errors_options)
167
+ if outside_range?(metadata[dimension], flat_options[dimension])
168
+ add_range_error(record, attribute, dimension, flat_options, errors_options)
169
+ false
170
+ else
171
+ true
124
172
  end
173
+ end
125
174
 
126
- true # valid file
175
+ def in_option_used?(flat_options, dimension)
176
+ flat_options[dimension][:in]
127
177
  end
128
178
 
129
- def add_error(record, attribute, default_message, **attrs)
130
- message = options[:message].presence || default_message
131
- return if record.errors.added?(attribute, message)
132
- record.errors.add(attribute, message, **attrs)
179
+ def outside_range?(value, options)
180
+ value < options[:min] || value > options[:max]
133
181
  end
134
182
 
183
+ def add_range_error(record, attribute, dimension, flat_options, errors_options)
184
+ errors_options[:min] = flat_options[dimension][:min]
185
+ errors_options[:max] = flat_options[dimension][:max]
186
+ add_error(record, attribute, :"dimension_#{dimension}_not_included_in", **errors_options)
187
+ end
188
+
189
+ def validate_dimension_min_max(record, attribute, dimension, metadata, flat_options, errors_options)
190
+ %i[min max].each do |bound|
191
+ send(:"validate_dimension_#{bound}", record, attribute, dimension, metadata, flat_options, errors_options)
192
+ end
193
+ end
194
+
195
+ def validate_exact(record, attribute, dimension, metadata, flat_options, errors_options)
196
+ if metadata[dimension] != flat_options[dimension]
197
+ errors_options[:length] = flat_options[dimension]
198
+ add_error(record, attribute, :"dimension_#{dimension}_not_equal_to", **errors_options)
199
+ false
200
+ else
201
+ true
202
+ end
203
+ end
204
+
205
+ def process_options(record)
206
+ flat_options = set_flat_options(record)
207
+
208
+ %i[width height].each do |dimension|
209
+ if flat_options[dimension] and flat_options[dimension].is_a?(Hash)
210
+ if (range = flat_options[dimension][:in])
211
+ flat_options[dimension][:min], flat_options[dimension][:max] = range.min, range.max
212
+ end
213
+ end
214
+ end
215
+
216
+ %i[min max].each do |bound|
217
+ if (range = flat_options[bound])
218
+ flat_options[:width] = { bound => range.first }
219
+ flat_options[:height] = { bound => range.last }
220
+ end
221
+ end
222
+
223
+ flat_options
224
+ end
135
225
  end
136
226
  end
@@ -0,0 +1,70 @@
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
+ duration_not_equal_to
17
+ ].freeze
18
+ METADATA_KEYS = %i[duration].freeze
19
+
20
+ def validate_each(record, attribute, _value)
21
+ return if no_attachments?(record, attribute)
22
+
23
+ flat_options = set_flat_options(record)
24
+
25
+ attachables_and_blobs(record, attribute).each do |attachable, blob|
26
+ duration = begin
27
+ metadata_for(blob, attachable, METADATA_KEYS)&.fetch(:duration, nil)
28
+ rescue ActiveStorage::FileNotFoundError
29
+ add_attachment_missing_error(record, attribute, attachable)
30
+ next
31
+ end
32
+
33
+ if duration.to_i <= 0
34
+ add_media_metadata_missing_error(record, attribute, attachable)
35
+ next
36
+ end
37
+
38
+ is_valid?(duration, flat_options) || populate_error_options_and_add_error(record, attribute, attachable, flat_options, duration)
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ def populate_error_options_and_add_error(record, attribute, attachable, flat_options, duration)
45
+ errors_options = initialize_error_options(options, attachable)
46
+ populate_error_options(errors_options, flat_options, duration)
47
+
48
+ error_type = set_error_type(flat_options)
49
+
50
+ add_error(record, attribute, error_type, **errors_options)
51
+ end
52
+
53
+ def format_bound_value(value)
54
+ return nil unless value
55
+
56
+ custom_value = value == value.to_i ? value.to_i : value
57
+ ActiveSupport::Duration.build(custom_value).inspect
58
+ end
59
+
60
+ def populate_error_options(errors_options, flat_options, duration)
61
+ super(errors_options, flat_options)
62
+ errors_options[:duration] = format_bound_value(duration)
63
+ end
64
+
65
+ def set_error_type(flat_options)
66
+ keys = AVAILABLE_CHECKS & flat_options.keys
67
+ "duration_not_#{keys.first}".to_sym
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveStorageValidations
4
+ module ASVBlobMetadatable
5
+ extend ActiveSupport::Concern
6
+
7
+ # rubocop:disable Metrics/BlockLength
8
+ included do
9
+ # This method returns the metadata that has been set by our gem.
10
+ # The metadata is stored in the blob's custom metadata. All keys are prefixed with 'asv_'
11
+ # to avoid conflicts with other metadata.
12
+ # It is not to set a active_storage_validation key equal to a a hash of our gem's metadata,
13
+ # because this would result in errors down the road with services such as S3.
14
+ #
15
+ # Because of how the metadata is stored, we need to convert the values from String
16
+ # to Integer or Boolean.
17
+ def active_storage_validations_metadata
18
+ metadata.dig("custom")
19
+ &.select { |key, _| key.to_s.start_with?("asv_") }
20
+ &.transform_keys { |key| key.to_s.delete_prefix("asv_") }
21
+ &.transform_values do |value|
22
+ case value
23
+ when /\A\d+\z/ then value.to_i
24
+ when /\A\d+\.\d+\z/ then value.to_f
25
+ when "true" then true
26
+ when "false" then false
27
+ else value
28
+ end
29
+ end || {}
30
+ end
31
+
32
+ # This method sets the metadata that has been detected by our gem.
33
+ # The metadata is stored in the blob's custom metadata. All keys are prefixed with 'asv_'.
34
+ # We need to store values as String, because services such as S3 will not accept other types.
35
+ def merge_into_active_storage_validations_metadata(hash)
36
+ aws_compatible_metadata = normalize_active_storage_validations_metadata_for_aws(hash)
37
+
38
+ metadata["custom"] ||= {}
39
+ metadata["custom"].merge!(aws_compatible_metadata)
40
+
41
+ active_storage_validations_metadata
42
+ end
43
+
44
+ def normalize_active_storage_validations_metadata_for_aws(hash)
45
+ hash.transform_keys { |key, _| key.to_s.start_with?("asv_") ? key : "asv_#{key}" }
46
+ .transform_values(&:to_s)
47
+ end
48
+
49
+ def remove_active_storage_validations_metadata!
50
+ metadata["custom"] ||= {}
51
+ metadata["custom"].delete_if { |key, _| key.to_s.start_with?("asv_") }
52
+ end
53
+ end
54
+ # rubocop:enable Metrics/BlockLength
55
+ end
56
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "marcel"
4
+
5
+ Marcel::MimeType.extend "application/x-rar-compressed", parents: %(application/x-rar)
6
+ Marcel::MimeType.extend "audio/x-hx-aac-adts", parents: %(audio/x-aac)
7
+ Marcel::MimeType.extend "audio/x-m4a", parents: %(audio/mp4)
8
+ Marcel::MimeType.extend "text/xml", parents: %(application/xml) # alias
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]
@@ -1,25 +1,43 @@
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:
5
- include OptionProcUnfolding
10
+ include ASVActiveStorageable
11
+ include ASVErrorable
12
+ include ASVOptionable
13
+ include ASVSymbolizable
6
14
 
7
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
8
21
 
9
22
  def check_validity!
10
- return true if AVAILABLE_CHECKS.any? { |argument| options.key?(argument) }
11
- raise ArgumentError, 'You must pass either :max or :min to the validator'
23
+ ensure_at_least_one_validator_option
24
+ ensure_arguments_validity
12
25
  end
13
26
 
14
- def validate_each(record, attribute, _)
15
- files = Array.wrap(record.send(attribute)).compact.uniq
16
- flat_options = unfold_procs(record, self.options, AVAILABLE_CHECKS)
17
- errors_options = { min: flat_options[:min], max: flat_options[:max] }
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)
18
33
 
19
- return true if files_count_valid?(files.count, flat_options)
20
- record.errors.add(attribute, options[:message].presence || :limit_out_of_range, **errors_options)
34
+ errors_options = initialize_and_populate_error_options(options, flat_options, count)
35
+ error_type = set_error_type(flat_options, count)
36
+ add_error(record, attribute, error_type, **errors_options)
21
37
  end
22
38
 
39
+ private
40
+
23
41
  def files_count_valid?(count, flat_options)
24
42
  if flat_options[:max].present? && flat_options[:min].present?
25
43
  count >= flat_options[:min] && count <= flat_options[:max]
@@ -29,5 +47,54 @@ module ActiveStorageValidations
29
47
  count >= flat_options[:min]
30
48
  end
31
49
  end
50
+
51
+ def ensure_at_least_one_validator_option
52
+ unless AVAILABLE_CHECKS.any? { |argument| options.key?(argument) }
53
+ raise ArgumentError, "You must pass either :max or :min to the validator"
54
+ end
55
+ end
56
+
57
+ def ensure_arguments_validity
58
+ return true if min_max_are_proc? || min_or_max_is_proc_and_other_not_present?
59
+
60
+ raise ArgumentError, "You must pass integers to :min and :max" if min_or_max_defined_and_not_integer?
61
+ raise ArgumentError, "You must pass a higher value to :max than to :min" if min_higher_than_max?
62
+ end
63
+
64
+ def min_max_are_proc?
65
+ options[:min]&.is_a?(Proc) && options[:max]&.is_a?(Proc)
66
+ end
67
+
68
+ def min_or_max_is_proc_and_other_not_present?
69
+ (options[:min]&.is_a?(Proc) && options[:max].nil?) ||
70
+ (options[:min].nil? && options[:max]&.is_a?(Proc))
71
+ end
72
+
73
+ def min_or_max_defined_and_not_integer?
74
+ (options.key?(:min) && !options[:min].is_a?(Integer)) ||
75
+ (options.key?(:max) && !options[:max].is_a?(Integer))
76
+ end
77
+
78
+ def min_higher_than_max?
79
+ options[:min] > options[:max] if options[:min].is_a?(Integer) && options[:max].is_a?(Integer)
80
+ end
81
+
82
+ def initialize_and_populate_error_options(options, flat_options, count)
83
+ errors_options = initialize_error_options(options)
84
+ errors_options[:min] = flat_options[:min]
85
+ errors_options[:max] = flat_options[:max]
86
+ errors_options[:count] = count
87
+ errors_options
88
+ end
89
+
90
+ def set_error_type(flat_options, count)
91
+ if flat_options[:min] && flat_options[:max]
92
+ :limit_out_of_range
93
+ elsif flat_options[:min] && count < flat_options[:min]
94
+ :limit_min_not_reached
95
+ else
96
+ :limit_max_exceeded
97
+ end
98
+ end
32
99
  end
33
100
  end