active_storage_validations 1.1.3 → 1.1.4

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 (28) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +81 -55
  3. data/lib/active_storage_validations/aspect_ratio_validator.rb +47 -22
  4. data/lib/active_storage_validations/attached_validator.rb +12 -3
  5. data/lib/active_storage_validations/concerns/errorable.rb +38 -0
  6. data/lib/active_storage_validations/concerns/symbolizable.rb +8 -6
  7. data/lib/active_storage_validations/content_type_validator.rb +41 -6
  8. data/lib/active_storage_validations/dimension_validator.rb +15 -15
  9. data/lib/active_storage_validations/limit_validator.rb +44 -7
  10. data/lib/active_storage_validations/matchers/aspect_ratio_validator_matcher.rb +128 -0
  11. data/lib/active_storage_validations/matchers/attached_validator_matcher.rb +20 -23
  12. data/lib/active_storage_validations/matchers/concerns/active_storageable.rb +17 -0
  13. data/lib/active_storage_validations/matchers/concerns/allow_blankable.rb +26 -0
  14. data/lib/active_storage_validations/matchers/concerns/contextable.rb +35 -0
  15. data/lib/active_storage_validations/matchers/concerns/messageable.rb +26 -0
  16. data/lib/active_storage_validations/matchers/concerns/rspecable.rb +25 -0
  17. data/lib/active_storage_validations/matchers/concerns/validatable.rb +5 -10
  18. data/lib/active_storage_validations/matchers/content_type_validator_matcher.rb +39 -25
  19. data/lib/active_storage_validations/matchers/dimension_validator_matcher.rb +61 -44
  20. data/lib/active_storage_validations/matchers/size_validator_matcher.rb +41 -24
  21. data/lib/active_storage_validations/matchers.rb +1 -0
  22. data/lib/active_storage_validations/metadata.rb +42 -28
  23. data/lib/active_storage_validations/processable_image_validator.rb +14 -5
  24. data/lib/active_storage_validations/size_validator.rb +7 -6
  25. data/lib/active_storage_validations/version.rb +1 -1
  26. data/lib/active_storage_validations.rb +1 -1
  27. metadata +9 -3
  28. data/lib/active_storage_validations/error_handler.rb +0 -21
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1ee85f42558858c115d91d107a0b67f6088d65b4cbc1ec8461f4f5baf230a025
4
- data.tar.gz: 4cf3fdb1bde0e805d22ad7d7d81d43cf5b378e06bde5443d938d1e390deee816
3
+ metadata.gz: 305a698168303d4889b8ba6b3972201842af8679758f6f9c81140658a0abdc8a
4
+ data.tar.gz: 414b60017f2da20463daa783e56028c927c2090a80e7728877f569323ec0b60a
5
5
  SHA512:
6
- metadata.gz: 9c3a532221860176a87f042126b7a068425d2efca7ec908393691ae807af36e55b10d38180f8226dd73c1da0cff9f7dc5d1408ac1049ccbef58033ce9fdca48c
7
- data.tar.gz: 469f3406a95dc8d0466d475d1cabc5b5658e813430728593140ce78b96892d0837da075f83d2901967b258f7ff57cf830390401737cb2f739b0bcd6c13d16aa3
6
+ metadata.gz: 48123a7a880a4f8e1b7680d3ea21b2e54c6f13b884b351e11c3f912fcdeec01efe24a0d6fc26b1021ec9b92460f6c6df321c869d9a8ce4e1848d339b1e35b0ca
7
+ data.tar.gz: faee600c94d2534a943feb4eebe14574341e3920e9bfd03fe663afad85c80e01c8cb7f95ced957ce333a323553798c6d73c0f8bffb33bdc185b922047f176b20
data/README.md CHANGED
@@ -68,15 +68,14 @@ end
68
68
 
69
69
  ### More examples
70
70
 
71
- - Content type validation using symbols. In order to infer the correct mime type from the symbol, the types must be registered with `Marcel::EXTENSIONS` (`MimeMagic::EXTENSIONS` for Rails <= 6.1.3).
71
+ - Content type validation using symbols or regex. The symbol types must be registered by [`Marcel::EXTENSIONS`](https://github.com/rails/marcel/blob/main/lib/marcel/tables.rb) that's used by this gem to infer the full content type.
72
72
 
73
73
  ```ruby
74
74
  class User < ApplicationRecord
75
75
  has_one_attached :avatar
76
76
  has_many_attached :photos
77
77
 
78
- validates :avatar, attached: true, content_type: :png # Marcel::Magic.by_extension(:png).to_s => 'image/png'
79
- # Rails <= 6.1.3; MimeMagic.by_extension(:png).to_s => 'image/png'
78
+ validates :avatar, attached: true, content_type: :png
80
79
  # or
81
80
  validates :photos, attached: true, content_type: [:png, :jpg, :jpeg]
82
81
  # or
@@ -174,12 +173,24 @@ en:
174
173
  image_not_processable: "is not a valid image"
175
174
  ```
176
175
 
177
- In some cases, Active Storage Validations provides variables to help you customize messages:
176
+ In several cases, Active Storage Validations provides variables to help you customize messages:
177
+
178
+ ### Aspect ratio
179
+ The keys starting with `aspect_ratio_` support two variables that you can use:
180
+ - `aspect_ratio` containing the expected aspect ratio, especially usefull for custom aspect ratio
181
+ - `filename` containing the current file name
182
+
183
+ For example :
184
+
185
+ ```yml
186
+ aspect_ratio_is_not: "must be a %{aspect_ratio} image"
187
+ ```
178
188
 
179
189
  ### Content type
180
- The `content_type_invalid` key has two variables that you can use:
190
+ The `content_type_invalid` key has three variables that you can use:
181
191
  - `content_type` containing the content type of the sent file
182
192
  - `authorized_types` containing the list of authorized content types
193
+ - `filename` containing the current file name
183
194
 
184
195
  For example :
185
196
 
@@ -187,22 +198,27 @@ For example :
187
198
  content_type_invalid: "has an invalid content type : %{content_type}, authorized types are %{authorized_types}"
188
199
  ```
189
200
 
190
- ### Number of files
191
- The `limit_out_of_range` key supports two variables that you can use:
192
- - `min` containing the minimum number of files
193
- - `max` containing the maximum number of files
201
+ ### Dimension
202
+ The keys starting with `dimension_` support six variables that you can use:
203
+ - `min` containing the minimum width or height allowed
204
+ - `max` containing the maximum width or height allowed
205
+ - `width` containing the minimum or maximum width allowed
206
+ - `height` containing the minimum or maximum width allowed
207
+ - `lenght` containing the exact width or height allowed
208
+ - `filename` containing the current file name
194
209
 
195
210
  For example :
196
211
 
197
212
  ```yml
198
- limit_out_of_range: "total number is out of range. range: [%{min}, %{max}]"
213
+ dimension_min_inclusion: "must be greater than or equal to %{width} x %{height} pixel."
199
214
  ```
200
215
 
201
216
  ### File size
202
- The keys starting with `file_size_not_` support three variables that you can use:
217
+ The keys starting with `file_size_not_` support four variables that you can use:
203
218
  - `file_size` containing the current file size
204
219
  - `min` containing the minimum file size
205
220
  - `max` containing the maxmimum file size
221
+ - `filename` containing the current file name
206
222
 
207
223
  For example :
208
224
 
@@ -210,14 +226,25 @@ For example :
210
226
  file_size_not_between: "file size must be between %{min_size} and %{max_size} (current size is %{file_size})"
211
227
  ```
212
228
 
213
- ### Aspect ratio
214
- The keys starting with `aspect_ratio_` support one variable that you can use:
215
- - `aspect_ratio` containing the expected aspect ratio, especially usefull for custom aspect ratio
229
+ ### Number of files
230
+ The `limit_out_of_range` key supports two variables that you can use:
231
+ - `min` containing the minimum number of files
232
+ - `max` containing the maximum number of files
216
233
 
217
234
  For example :
218
235
 
219
236
  ```yml
220
- aspect_ratio_is_not: "must be a %{aspect_ratio} image"
237
+ limit_out_of_range: "total number is out of range. range: [%{min}, %{max}]"
238
+ ```
239
+
240
+ ### Processable image
241
+ The `image_not_processable` key supports one variable that you can use:
242
+ - `filename` containing the current file name
243
+
244
+ For example :
245
+
246
+ ```yml
247
+ image_not_processable: "is not a valid image (file: %{filename})"
221
248
  ```
222
249
 
223
250
  ## Installation
@@ -247,7 +274,7 @@ Very simple example of validation with file attached, content type check and cus
247
274
  [![Sample](https://raw.githubusercontent.com/igorkasyanchuk/active_storage_validations/master/docs/preview.png)](https://raw.githubusercontent.com/igorkasyanchuk/active_storage_validations/master/docs/preview.png)
248
275
 
249
276
  ## Test matchers
250
- Provides RSpec-compatible and Minitest-compatible matchers for testing the validators.
277
+ Provides RSpec-compatible and Minitest-compatible matchers for testing the validators. Only `aspect_ratio`, `attached`, `content_type`, `dimension` and `size` validators currently have their matcher developped.
251
278
 
252
279
  ### RSpec
253
280
 
@@ -265,38 +292,65 @@ RSpec.configure do |config|
265
292
  end
266
293
  ```
267
294
 
268
- Example (Note that the options are chainable):
295
+ Matcher methods available:
269
296
 
270
297
  ```ruby
271
298
  describe User do
299
+ # aspect_ratio:
300
+ # #allowing, #rejecting
301
+ it { is_expected.to validate_aspect_ratio_of(:avatar).allowing(:square) }
302
+ it { is_expected.to validate_aspect_ratio_of(:avatar).rejecting(:portrait) }
303
+
304
+ # attached
272
305
  it { is_expected.to validate_attached_of(:avatar) }
273
- it { is_expected.to validate_attached_of(:avatar).with_message('must not be blank') }
274
306
 
307
+ # content_type:
308
+ # #allowing, #rejecting
275
309
  it { is_expected.to validate_content_type_of(:avatar).allowing('image/png', 'image/gif') }
276
310
  it { is_expected.to validate_content_type_of(:avatar).rejecting('text/plain', 'text/xml') }
277
- it { is_expected.to validate_content_type_of(:avatar).rejecting('text/plain', 'text/xml').with_message("must be an authorized type") }
278
311
 
312
+ # dimension:
313
+ # #width, #height, #width_min, #height_min, #width_max, #height_max, #width_between, #height_between
279
314
  it { is_expected.to validate_dimensions_of(:avatar).width(250) }
280
315
  it { is_expected.to validate_dimensions_of(:avatar).height(200) }
281
- it { is_expected.to validate_dimensions_of(:avatar).width(250).height(200).with_message('Invalid dimensions.') }
282
316
  it { is_expected.to validate_dimensions_of(:avatar).width_min(200) }
283
- it { is_expected.to validate_dimensions_of(:avatar).width_max(500) }
284
317
  it { is_expected.to validate_dimensions_of(:avatar).height_min(100) }
318
+ it { is_expected.to validate_dimensions_of(:avatar).width_max(500) }
285
319
  it { is_expected.to validate_dimensions_of(:avatar).height_max(300) }
286
320
  it { is_expected.to validate_dimensions_of(:avatar).width_between(200..500) }
287
321
  it { is_expected.to validate_dimensions_of(:avatar).height_between(100..300) }
288
322
 
323
+ # size:
324
+ # #less_than, #less_than_or_equal_to, #greater_than, #greater_than_or_equal_to, #between
289
325
  it { is_expected.to validate_size_of(:avatar).less_than(50.kilobytes) }
290
326
  it { is_expected.to validate_size_of(:avatar).less_than_or_equal_to(50.kilobytes) }
291
327
  it { is_expected.to validate_size_of(:avatar).greater_than(1.kilobyte) }
292
- it { is_expected.to validate_size_of(:avatar).greater_than(1.kilobyte).with_message('is not in required file size range') }
293
328
  it { is_expected.to validate_size_of(:avatar).greater_than_or_equal_to(1.kilobyte) }
294
329
  it { is_expected.to validate_size_of(:avatar).between(100..500.kilobytes) }
295
330
  end
296
331
  ```
332
+ (Note that matcher methods are chainable)
333
+
334
+ All matchers can currently be customized with Rails validation options:
335
+
336
+ ```ruby
337
+ describe User do
338
+ # :allow_blank
339
+ it { is_expected.to validate_attached_of(:avatar).allow_blank }
340
+
341
+ # :on
342
+ it { is_expected.to validate_attached_of(:avatar).on(:update) }
343
+ it { is_expected.to validate_attached_of(:avatar).on(%i[update custom]) }
344
+
345
+ # :message
346
+ it { is_expected.to validate_dimensions_of(:avatar).width(250).with_message('Invalid dimensions.') }
347
+ end
348
+ ```
297
349
 
298
350
  ### Minitest
299
- To use the following syntax, make sure you have the [shoulda-context](https://github.com/thoughtbot/shoulda-context) gem up and running. To make use of the matchers you need to require the matchers:
351
+ To use the matchers, make sure you have the [shoulda-context](https://github.com/thoughtbot/shoulda-context) gem up and running.
352
+
353
+ You need to require the matchers:
300
354
 
301
355
  ```ruby
302
356
  require 'active_storage_validations/matchers'
@@ -310,35 +364,7 @@ class ActiveSupport::TestCase
310
364
  end
311
365
  ```
312
366
 
313
- Example (Note that the options are chainable):
314
-
315
- ```ruby
316
- class UserTest < ActiveSupport::TestCase
317
- should validate_attached_of(:avatar)
318
- should validate_attached_of(:avatar).with_message('must not be blank')
319
-
320
- should validate_content_type_of(:avatar).allowing('image/png', 'image/gif')
321
- should validate_content_type_of(:avatar).rejecting('text/plain', 'text/xml')
322
- should validate_content_type_of(:avatar).rejecting('text/plain', 'text/xml').with_message("must be an authorized type")
323
-
324
- should validate_dimensions_of(:avatar).width(250)
325
- should validate_dimensions_of(:avatar).height(200)
326
- should validate_dimensions_of(:avatar).width(250).height(200).with_message('Invalid dimensions.')
327
- should validate_dimensions_of(:avatar).width_min(200)
328
- should validate_dimensions_of(:avatar).width_max(500)
329
- should validate_dimensions_of(:avatar).height_min(100)
330
- should validate_dimensions_of(:avatar).height_max(300)
331
- should validate_dimensions_of(:avatar).width_between(200..500)
332
- should validate_dimensions_of(:avatar).height_between(100..300)
333
-
334
- should validate_size_of(:avatar).less_than(50.kilobytes)
335
- should validate_size_of(:avatar).less_than_or_equal_to(50.kilobytes)
336
- should validate_size_of(:avatar).greater_than(1.kilobyte)
337
- should validate_size_of(:avatar).greater_than(1.kilobyte).with_message('is not in required file size range')
338
- should validate_size_of(:avatar).greater_than_or_equal_to(1.kilobyte)
339
- should validate_size_of(:avatar).between(100..500.kilobytes)
340
- end
341
- ```
367
+ Then you can use the matchers with the syntax specified in the RSpec section, just use `should validate_method` instead of `it { is_expected_to validate_method }` as specified in the [shoulda-context](https://github.com/thoughtbot/shoulda-context) gem.
342
368
 
343
369
  ## Todo
344
370
 
@@ -350,7 +376,7 @@ end
350
376
 
351
377
  To run tests in root folder of gem:
352
378
 
353
- * `BUNDLE_GEMFILE=gemfiles/rails_6_1.gemfile bundle exec rake test` to run for Rails 6.1
379
+ * `BUNDLE_GEMFILE=gemfiles/rails_6_1_3_1.gemfile bundle exec rake test` to run for Rails 6.1
354
380
  * `BUNDLE_GEMFILE=gemfiles/rails_7_0.gemfile bundle exec rake test` to run for Rails 7.0
355
381
  * `BUNDLE_GEMFILE=gemfiles/rails_7_1.gemfile bundle exec rake test` to run for Rails 7.0
356
382
  * `BUNDLE_GEMFILE=gemfiles/rails_next.gemfile bundle exec rake test` to run for Rails main branch
@@ -358,11 +384,11 @@ To run tests in root folder of gem:
358
384
  Snippet to run in console:
359
385
 
360
386
  ```bash
361
- BUNDLE_GEMFILE=gemfiles/rails_6_1.gemfile bundle
387
+ BUNDLE_GEMFILE=gemfiles/rails_6_1_3_1.gemfile bundle
362
388
  BUNDLE_GEMFILE=gemfiles/rails_7_0.gemfile bundle
363
389
  BUNDLE_GEMFILE=gemfiles/rails_7_1.gemfile bundle
364
390
  BUNDLE_GEMFILE=gemfiles/rails_next.gemfile bundle
365
- BUNDLE_GEMFILE=gemfiles/rails_6_1.gemfile bundle exec rake test
391
+ BUNDLE_GEMFILE=gemfiles/rails_6_1_3_1.gemfile bundle exec rake test
366
392
  BUNDLE_GEMFILE=gemfiles/rails_7_0.gemfile bundle exec rake test
367
393
  BUNDLE_GEMFILE=gemfiles/rails_7_1.gemfile bundle exec rake test
368
394
  BUNDLE_GEMFILE=gemfiles/rails_next.gemfile bundle exec rake test
@@ -1,21 +1,31 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'concerns/errorable.rb'
3
4
  require_relative 'concerns/symbolizable.rb'
4
5
  require_relative 'metadata.rb'
5
6
 
6
7
  module ActiveStorageValidations
7
8
  class AspectRatioValidator < ActiveModel::EachValidator # :nodoc
8
9
  include OptionProcUnfolding
9
- include ErrorHandler
10
+ include Errorable
10
11
  include Symbolizable
11
12
 
12
13
  AVAILABLE_CHECKS = %i[with].freeze
13
- PRECISION = 3
14
+ NAMED_ASPECT_RATIOS = %i[square portrait landscape].freeze
15
+ ASPECT_RATIO_REGEX = /is_([1-9]\d*)_([1-9]\d*)/.freeze
16
+ ERROR_TYPES = %i[
17
+ image_metadata_missing
18
+ aspect_ratio_not_square
19
+ aspect_ratio_not_portrait
20
+ aspect_ratio_not_landscape
21
+ aspect_ratio_is_not
22
+ aspect_ratio_unknown
23
+ ].freeze
24
+ PRECISION = 3.freeze
14
25
 
15
26
  def check_validity!
16
- unless AVAILABLE_CHECKS.any? { |argument| options.key?(argument) }
17
- raise ArgumentError, 'You must pass :with to the validator'
18
- end
27
+ ensure_at_least_one_validator_option
28
+ ensure_aspect_ratio_validity
19
29
  end
20
30
 
21
31
  if Rails.gem_version >= Gem::Version.new('6.0.0')
@@ -29,7 +39,7 @@ module ActiveStorageValidations
29
39
 
30
40
  files.each do |file|
31
41
  metadata = Metadata.new(file).metadata
32
- next if is_valid?(record, attribute, metadata)
42
+ next if is_valid?(record, attribute, file, metadata)
33
43
  break
34
44
  end
35
45
  end
@@ -45,19 +55,17 @@ module ActiveStorageValidations
45
55
  file.analyze; file.reload unless file.analyzed?
46
56
  metadata = file.metadata
47
57
 
48
- next if is_valid?(record, attribute, metadata)
58
+ next if is_valid?(record, attribute, file, metadata)
49
59
  break
50
60
  end
51
61
  end
52
62
  end
53
63
 
54
-
55
64
  private
56
65
 
57
-
58
- def is_valid?(record, attribute, metadata)
66
+ def is_valid?(record, attribute, file, metadata)
59
67
  flat_options = unfold_procs(record, self.options, AVAILABLE_CHECKS)
60
- errors_options = initialize_error_options(options)
68
+ errors_options = initialize_error_options(options, file)
61
69
 
62
70
  if metadata[:width].to_i <= 0 || metadata[:height].to_i <= 0
63
71
  errors_options[:aspect_ratio] = flat_options[:with]
@@ -82,21 +90,38 @@ module ActiveStorageValidations
82
90
  errors_options[:aspect_ratio] = flat_options[:with]
83
91
  add_error(record, attribute, :aspect_ratio_not_landscape, **errors_options)
84
92
 
93
+ when ASPECT_RATIO_REGEX
94
+ flat_options[:with] =~ ASPECT_RATIO_REGEX
95
+ x = $1.to_i
96
+ y = $2.to_i
97
+
98
+ return true if x > 0 && y > 0 && (x.to_f / y).round(PRECISION) == (metadata[:width].to_f / metadata[:height]).round(PRECISION)
99
+
100
+ errors_options[:aspect_ratio] = "#{x}:#{y}"
101
+ add_error(record, attribute, :aspect_ratio_is_not, **errors_options)
85
102
  else
86
- if flat_options[:with] =~ /is_(\d*)_(\d*)/
87
- x = $1.to_i
88
- y = $2.to_i
103
+ errors_options[:aspect_ratio] = flat_options[:with]
104
+ add_error(record, attribute, :aspect_ratio_unknown, **errors_options)
105
+ return false
106
+ end
107
+ end
108
+
109
+ def ensure_at_least_one_validator_option
110
+ unless AVAILABLE_CHECKS.any? { |argument| options.key?(argument) }
111
+ raise ArgumentError, 'You must pass :with to the validator'
112
+ end
113
+ end
89
114
 
90
- return true if (x.to_f / y).round(PRECISION) == (metadata[:width].to_f / metadata[:height]).round(PRECISION)
115
+ def ensure_aspect_ratio_validity
116
+ return true if options[:with]&.is_a?(Proc)
91
117
 
92
- errors_options[:aspect_ratio] = "#{x}x#{y}"
93
- add_error(record, attribute, :aspect_ratio_is_not, **errors_options)
94
- else
95
- errors_options[:aspect_ratio] = flat_options[:with]
96
- add_error(record, attribute, :aspect_ratio_unknown, **errors_options)
97
- end
118
+ unless NAMED_ASPECT_RATIOS.include?(options[:with]) || options[:with] =~ ASPECT_RATIO_REGEX
119
+ raise ArgumentError, <<~ERROR_MESSAGE
120
+ You must pass a valid aspect ratio to the validator
121
+ It should either be a named aspect ratio (#{NAMED_ASPECT_RATIOS.join(', ')})
122
+ Or an aspect ratio like 'is_16_9' (matching /#{ASPECT_RATIO_REGEX.source}/)
123
+ ERROR_MESSAGE
98
124
  end
99
- false
100
125
  end
101
126
  end
102
127
  end
@@ -1,19 +1,28 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'concerns/errorable.rb'
3
4
  require_relative 'concerns/symbolizable.rb'
4
5
 
5
6
  module ActiveStorageValidations
6
7
  class AttachedValidator < ActiveModel::EachValidator # :nodoc:
7
- include ErrorHandler
8
+ include Errorable
8
9
  include Symbolizable
9
10
 
10
11
  ERROR_TYPES = %i[blank].freeze
11
12
 
13
+ def check_validity!
14
+ %i(allow_nil allow_blank).each do |not_authorized_option|
15
+ if options.include?(not_authorized_option)
16
+ raise ArgumentError, "You cannot pass the :#{not_authorized_option} option to this validator"
17
+ end
18
+ end
19
+ end
20
+
12
21
  def validate_each(record, attribute, _value)
13
- return if record.send(attribute).attached?
22
+ return if record.send(attribute).attached? &&
23
+ !Array.wrap(record.send(attribute)).all? { |file| file.marked_for_destruction? }
14
24
 
15
25
  errors_options = initialize_error_options(options)
16
-
17
26
  add_error(record, attribute, ERROR_TYPES.first, **errors_options)
18
27
  end
19
28
  end
@@ -0,0 +1,38 @@
1
+ module ActiveStorageValidations
2
+ module Errorable
3
+ extend ActiveSupport::Concern
4
+
5
+ def initialize_error_options(options, file = nil)
6
+ not_explicitly_written_options = %i(with in)
7
+ curated_options = options.except(*not_explicitly_written_options)
8
+
9
+ active_storage_validations_options = {
10
+ validator_type: self.class.to_sym,
11
+ custom_message: (options[:message] if options[:message].present?),
12
+ filename: get_filename(file)
13
+ }.compact
14
+
15
+ curated_options.merge(active_storage_validations_options)
16
+ end
17
+
18
+ def add_error(record, attribute, error_type, **errors_options)
19
+ type = errors_options[:custom_message].presence || error_type
20
+ return if record.errors.added?(attribute, type)
21
+
22
+ # You can read https://api.rubyonrails.org/classes/ActiveModel/Errors.html#method-i-add
23
+ # to better understand how Rails model errors work
24
+ record.errors.add(attribute, type, **errors_options)
25
+ end
26
+
27
+ private
28
+
29
+ def get_filename(file)
30
+ return nil unless file
31
+
32
+ case file
33
+ when ActiveStorage::Attached then file.blob.filename.to_s
34
+ when Hash then file[:filename]
35
+ end
36
+ end
37
+ end
38
+ end
@@ -1,10 +1,12 @@
1
- module Symbolizable
2
- extend ActiveSupport::Concern
1
+ module ActiveStorageValidations
2
+ module Symbolizable
3
+ extend ActiveSupport::Concern
3
4
 
4
- class_methods do
5
- def to_sym
6
- validator_class = self.name.split("::").last
7
- validator_class.sub(/Validator/, '').underscore.to_sym
5
+ class_methods do
6
+ def to_sym
7
+ validator_class = self.name.split("::").last
8
+ validator_class.sub(/Validator/, '').underscore.to_sym
9
+ end
8
10
  end
9
11
  end
10
12
  end
@@ -1,30 +1,35 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'concerns/errorable.rb'
3
4
  require_relative 'concerns/symbolizable.rb'
4
5
 
5
6
  module ActiveStorageValidations
6
7
  class ContentTypeValidator < ActiveModel::EachValidator # :nodoc:
7
8
  include OptionProcUnfolding
8
- include ErrorHandler
9
+ include Errorable
9
10
  include Symbolizable
10
11
 
11
12
  AVAILABLE_CHECKS = %i[with in].freeze
12
13
  ERROR_TYPES = %i[content_type_invalid].freeze
13
14
 
15
+ def check_validity!
16
+ ensure_exactly_one_validator_option
17
+ ensure_content_types_validity
18
+ end
19
+
14
20
  def validate_each(record, attribute, _value)
15
21
  return true unless record.send(attribute).attached?
16
22
 
17
23
  types = authorized_types(record)
18
24
  return true if types.empty?
19
-
20
- files = Array.wrap(record.send(attribute))
21
25
 
22
- errors_options = initialize_error_options(options)
23
- errors_options[:authorized_types] = types_to_human_format(types)
26
+ files = Array.wrap(record.send(attribute))
24
27
 
25
28
  files.each do |file|
26
29
  next if is_valid?(file, types)
27
30
 
31
+ errors_options = initialize_error_options(options, file)
32
+ errors_options[:authorized_types] = types_to_human_format(types)
28
33
  errors_options[:content_type] = content_type(file)
29
34
  add_error(record, attribute, ERROR_TYPES.first, **errors_options)
30
35
  break
@@ -44,7 +49,7 @@ module ActiveStorageValidations
44
49
 
45
50
  def types_to_human_format(types)
46
51
  types
47
- .map { |type| type.to_s.split('/').last.upcase }
52
+ .map { |type| type.is_a?(Regexp) ? type.source : type.to_s.split('/').last.upcase }
48
53
  .join(', ')
49
54
  end
50
55
 
@@ -58,5 +63,35 @@ module ActiveStorageValidations
58
63
  type == file_type || (type.is_a?(Regexp) && type.match?(file_type.to_s))
59
64
  end
60
65
  end
66
+
67
+ def ensure_exactly_one_validator_option
68
+ unless AVAILABLE_CHECKS.one? { |argument| options.key?(argument) }
69
+ raise ArgumentError, 'You must pass either :with or :in to the validator'
70
+ end
71
+ end
72
+
73
+ def ensure_content_types_validity
74
+ return true if options[:with]&.is_a?(Proc) || options[:in]&.is_a?(Proc)
75
+
76
+ ([options[:with]] || options[:in]).each do |content_type|
77
+ raise ArgumentError, invalid_content_type_message(content_type) if invalid_content_type?(content_type)
78
+ end
79
+ end
80
+
81
+ def invalid_content_type_message(content_type)
82
+ <<~ERROR_MESSAGE
83
+ You must pass valid content types to the validator
84
+ '#{content_type}' is not find in Marcel::EXTENSIONS mimes
85
+ ERROR_MESSAGE
86
+ end
87
+
88
+ def invalid_content_type?(content_type)
89
+ case content_type
90
+ when String, Symbol
91
+ Marcel::MimeType.for(declared_type: content_type.to_s, extension: content_type.to_s) == 'application/octet-stream'
92
+ when Regexp
93
+ false # We always validate regexes
94
+ end
95
+ end
61
96
  end
62
97
  end
@@ -1,12 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'concerns/errorable.rb'
3
4
  require_relative 'concerns/symbolizable.rb'
4
5
  require_relative 'metadata.rb'
5
6
 
6
7
  module ActiveStorageValidations
7
8
  class DimensionValidator < ActiveModel::EachValidator # :nodoc
8
9
  include OptionProcUnfolding
9
- include ErrorHandler
10
+ include Errorable
10
11
  include Symbolizable
11
12
 
12
13
  AVAILABLE_CHECKS = %i[width height min max].freeze
@@ -46,7 +47,6 @@ module ActiveStorageValidations
46
47
  flat_options
47
48
  end
48
49
 
49
-
50
50
  def check_validity!
51
51
  unless AVAILABLE_CHECKS.any? { |argument| options.key?(argument) }
52
52
  raise ArgumentError, 'You must pass either :width, :height, :min or :max to the validator'
@@ -64,7 +64,7 @@ module ActiveStorageValidations
64
64
  files = Array.wrap(changes.is_a?(ActiveStorage::Attached::Changes::CreateMany) ? changes.attachables : changes.attachable)
65
65
  files.each do |file|
66
66
  metadata = Metadata.new(file).metadata
67
- next if is_valid?(record, attribute, metadata)
67
+ next if is_valid?(record, attribute, file, metadata)
68
68
  break
69
69
  end
70
70
  end
@@ -78,19 +78,19 @@ module ActiveStorageValidations
78
78
  # Analyze file first if not analyzed to get all required metadata.
79
79
  file.analyze; file.reload unless file.analyzed?
80
80
  metadata = file.metadata rescue {}
81
- next if is_valid?(record, attribute, metadata)
81
+ next if is_valid?(record, attribute, file, metadata)
82
82
  break
83
83
  end
84
84
  end
85
85
  end
86
86
 
87
87
 
88
- def is_valid?(record, attribute, file_metadata)
88
+ def is_valid?(record, attribute, file, metadata)
89
89
  flat_options = process_options(record)
90
- errors_options = initialize_error_options(options)
90
+ errors_options = initialize_error_options(options, file)
91
91
 
92
92
  # Validation fails unless file metadata contains valid width and height.
93
- if file_metadata[:width].to_i <= 0 || file_metadata[:height].to_i <= 0
93
+ if metadata[:width].to_i <= 0 || metadata[:height].to_i <= 0
94
94
  add_error(record, attribute, :image_metadata_missing, **errors_options)
95
95
  return false
96
96
  end
@@ -98,8 +98,8 @@ module ActiveStorageValidations
98
98
  # Validation based on checks :min and :max (:min, :max has higher priority to :width, :height).
99
99
  if flat_options[:min] || flat_options[:max]
100
100
  if flat_options[:min] && (
101
- (flat_options[:width][:min] && file_metadata[:width] < flat_options[:width][:min]) ||
102
- (flat_options[:height][:min] && file_metadata[:height] < flat_options[:height][:min])
101
+ (flat_options[:width][:min] && metadata[:width] < flat_options[:width][:min]) ||
102
+ (flat_options[:height][:min] && metadata[:height] < flat_options[:height][:min])
103
103
  )
104
104
  errors_options[:width] = flat_options[:width][:min]
105
105
  errors_options[:height] = flat_options[:height][:min]
@@ -108,8 +108,8 @@ module ActiveStorageValidations
108
108
  return false
109
109
  end
110
110
  if flat_options[:max] && (
111
- (flat_options[:width][:max] && file_metadata[:width] > flat_options[:width][:max]) ||
112
- (flat_options[:height][:max] && file_metadata[:height] > flat_options[:height][:max])
111
+ (flat_options[:width][:max] && metadata[:width] > flat_options[:width][:max]) ||
112
+ (flat_options[:height][:max] && metadata[:height] > flat_options[:height][:max])
113
113
  )
114
114
  errors_options[:width] = flat_options[:width][:max]
115
115
  errors_options[:height] = flat_options[:height][:max]
@@ -125,7 +125,7 @@ module ActiveStorageValidations
125
125
  [:width, :height].each do |length|
126
126
  next unless flat_options[length]
127
127
  if flat_options[length].is_a?(Hash)
128
- if flat_options[length][:in] && (file_metadata[length] < flat_options[length][:min] || file_metadata[length] > flat_options[length][:max])
128
+ if flat_options[length][:in] && (metadata[length] < flat_options[length][:min] || metadata[length] > flat_options[length][:max])
129
129
  error_type = :"dimension_#{length}_inclusion"
130
130
  errors_options[:min] = flat_options[length][:min]
131
131
  errors_options[:max] = flat_options[length][:max]
@@ -133,13 +133,13 @@ module ActiveStorageValidations
133
133
  add_error(record, attribute, error_type, **errors_options)
134
134
  width_or_height_invalid = true
135
135
  else
136
- if flat_options[length][:min] && file_metadata[length] < flat_options[length][:min]
136
+ if flat_options[length][:min] && metadata[length] < flat_options[length][:min]
137
137
  error_type = :"dimension_#{length}_greater_than_or_equal_to"
138
138
  errors_options[:length] = flat_options[length][:min]
139
139
 
140
140
  add_error(record, attribute, error_type, **errors_options)
141
141
  width_or_height_invalid = true
142
- elsif flat_options[length][:max] && file_metadata[length] > flat_options[length][:max]
142
+ elsif flat_options[length][:max] && metadata[length] > flat_options[length][:max]
143
143
  error_type = :"dimension_#{length}_less_than_or_equal_to"
144
144
  errors_options[:length] = flat_options[length][:max]
145
145
 
@@ -148,7 +148,7 @@ module ActiveStorageValidations
148
148
  end
149
149
  end
150
150
  else
151
- if file_metadata[length] != flat_options[length]
151
+ if metadata[length] != flat_options[length]
152
152
  error_type = :"dimension_#{length}_equal_to"
153
153
  errors_options[:length] = flat_options[length]
154
154