active_storage_validations 1.1.3 → 1.1.4

Sign up to get free protection for your applications and to get access to all the features.
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