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.
- checksums.yaml +4 -4
- data/README.md +81 -55
- data/lib/active_storage_validations/aspect_ratio_validator.rb +47 -22
- data/lib/active_storage_validations/attached_validator.rb +12 -3
- data/lib/active_storage_validations/concerns/errorable.rb +38 -0
- data/lib/active_storage_validations/concerns/symbolizable.rb +8 -6
- data/lib/active_storage_validations/content_type_validator.rb +41 -6
- data/lib/active_storage_validations/dimension_validator.rb +15 -15
- data/lib/active_storage_validations/limit_validator.rb +44 -7
- data/lib/active_storage_validations/matchers/aspect_ratio_validator_matcher.rb +128 -0
- data/lib/active_storage_validations/matchers/attached_validator_matcher.rb +20 -23
- data/lib/active_storage_validations/matchers/concerns/active_storageable.rb +17 -0
- data/lib/active_storage_validations/matchers/concerns/allow_blankable.rb +26 -0
- data/lib/active_storage_validations/matchers/concerns/contextable.rb +35 -0
- data/lib/active_storage_validations/matchers/concerns/messageable.rb +26 -0
- data/lib/active_storage_validations/matchers/concerns/rspecable.rb +25 -0
- data/lib/active_storage_validations/matchers/concerns/validatable.rb +5 -10
- data/lib/active_storage_validations/matchers/content_type_validator_matcher.rb +39 -25
- data/lib/active_storage_validations/matchers/dimension_validator_matcher.rb +61 -44
- data/lib/active_storage_validations/matchers/size_validator_matcher.rb +41 -24
- data/lib/active_storage_validations/matchers.rb +1 -0
- data/lib/active_storage_validations/metadata.rb +42 -28
- data/lib/active_storage_validations/processable_image_validator.rb +14 -5
- data/lib/active_storage_validations/size_validator.rb +7 -6
- data/lib/active_storage_validations/version.rb +1 -1
- data/lib/active_storage_validations.rb +1 -1
- metadata +9 -3
- 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:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 305a698168303d4889b8ba6b3972201842af8679758f6f9c81140658a0abdc8a
|
4
|
+
data.tar.gz: 414b60017f2da20463daa783e56028c927c2090a80e7728877f569323ec0b60a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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
|
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
|
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
|
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
|
-
###
|
191
|
-
The `
|
192
|
-
- `min` containing the minimum
|
193
|
-
- `max` containing the maximum
|
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
|
-
|
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
|
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
|
-
###
|
214
|
-
The
|
215
|
-
- `
|
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
|
-
|
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
|
[](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
|
-
|
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
|
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
|
-
|
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/
|
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/
|
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/
|
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
|
10
|
+
include Errorable
|
10
11
|
include Symbolizable
|
11
12
|
|
12
13
|
AVAILABLE_CHECKS = %i[with].freeze
|
13
|
-
|
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
|
-
|
17
|
-
|
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
|
-
|
87
|
-
|
88
|
-
|
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
|
-
|
115
|
+
def ensure_aspect_ratio_validity
|
116
|
+
return true if options[:with]&.is_a?(Proc)
|
91
117
|
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
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
|
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
|
2
|
-
|
1
|
+
module ActiveStorageValidations
|
2
|
+
module Symbolizable
|
3
|
+
extend ActiveSupport::Concern
|
3
4
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
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
|
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
|
-
|
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
|
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,
|
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
|
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] &&
|
102
|
-
(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] &&
|
112
|
-
(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] && (
|
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] &&
|
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] &&
|
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
|
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
|
|