active_storage_validations 0.9.7 → 2.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (69) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +737 -229
  3. data/config/locales/da.yml +53 -0
  4. data/config/locales/de.yml +50 -19
  5. data/config/locales/en.yml +50 -19
  6. data/config/locales/es.yml +50 -19
  7. data/config/locales/fr.yml +50 -19
  8. data/config/locales/it.yml +50 -19
  9. data/config/locales/ja.yml +50 -19
  10. data/config/locales/nl.yml +50 -19
  11. data/config/locales/pl.yml +50 -19
  12. data/config/locales/pt-BR.yml +50 -19
  13. data/config/locales/ru.yml +50 -19
  14. data/config/locales/sv.yml +53 -0
  15. data/config/locales/tr.yml +50 -19
  16. data/config/locales/uk.yml +50 -19
  17. data/config/locales/vi.yml +50 -19
  18. data/config/locales/zh-CN.yml +53 -0
  19. data/lib/active_storage_validations/analyzer/audio_analyzer.rb +58 -0
  20. data/lib/active_storage_validations/analyzer/content_type_analyzer.rb +60 -0
  21. data/lib/active_storage_validations/analyzer/image_analyzer/image_magick.rb +47 -0
  22. data/lib/active_storage_validations/analyzer/image_analyzer/vips.rb +57 -0
  23. data/lib/active_storage_validations/analyzer/image_analyzer.rb +49 -0
  24. data/lib/active_storage_validations/analyzer/null_analyzer.rb +18 -0
  25. data/lib/active_storage_validations/analyzer/shared/asv_ff_probable.rb +61 -0
  26. data/lib/active_storage_validations/analyzer/video_analyzer.rb +130 -0
  27. data/lib/active_storage_validations/analyzer.rb +87 -0
  28. data/lib/active_storage_validations/aspect_ratio_validator.rb +154 -99
  29. data/lib/active_storage_validations/attached_validator.rb +22 -5
  30. data/lib/active_storage_validations/base_comparison_validator.rb +71 -0
  31. data/lib/active_storage_validations/content_type_validator.rb +206 -25
  32. data/lib/active_storage_validations/dimension_validator.rb +105 -82
  33. data/lib/active_storage_validations/duration_validator.rb +55 -0
  34. data/lib/active_storage_validations/extensors/asv_blob_metadatable.rb +49 -0
  35. data/lib/active_storage_validations/extensors/asv_marcelable.rb +12 -0
  36. data/lib/active_storage_validations/limit_validator.rb +75 -16
  37. data/lib/active_storage_validations/matchers/aspect_ratio_validator_matcher.rb +119 -0
  38. data/lib/active_storage_validations/matchers/attached_validator_matcher.rb +48 -25
  39. data/lib/active_storage_validations/matchers/base_comparison_validator_matcher.rb +140 -0
  40. data/lib/active_storage_validations/matchers/content_type_validator_matcher.rb +94 -59
  41. data/lib/active_storage_validations/matchers/dimension_validator_matcher.rb +97 -55
  42. data/lib/active_storage_validations/matchers/duration_validator_matcher.rb +39 -0
  43. data/lib/active_storage_validations/matchers/limit_validator_matcher.rb +127 -0
  44. data/lib/active_storage_validations/matchers/processable_file_validator_matcher.rb +78 -0
  45. data/lib/active_storage_validations/matchers/shared/asv_active_storageable.rb +19 -0
  46. data/lib/active_storage_validations/matchers/shared/asv_allow_blankable.rb +28 -0
  47. data/lib/active_storage_validations/matchers/shared/asv_attachable.rb +72 -0
  48. data/lib/active_storage_validations/matchers/shared/asv_contextable.rb +49 -0
  49. data/lib/active_storage_validations/matchers/shared/asv_messageable.rb +28 -0
  50. data/lib/active_storage_validations/matchers/shared/asv_rspecable.rb +27 -0
  51. data/lib/active_storage_validations/matchers/shared/asv_validatable.rb +56 -0
  52. data/lib/active_storage_validations/matchers/size_validator_matcher.rb +17 -71
  53. data/lib/active_storage_validations/matchers/total_size_validator_matcher.rb +47 -0
  54. data/lib/active_storage_validations/matchers.rb +11 -16
  55. data/lib/active_storage_validations/processable_file_validator.rb +37 -0
  56. data/lib/active_storage_validations/railtie.rb +11 -0
  57. data/lib/active_storage_validations/shared/asv_active_storageable.rb +30 -0
  58. data/lib/active_storage_validations/shared/asv_analyzable.rb +80 -0
  59. data/lib/active_storage_validations/shared/asv_attachable.rb +204 -0
  60. data/lib/active_storage_validations/shared/asv_errorable.rb +40 -0
  61. data/lib/active_storage_validations/shared/asv_loggable.rb +11 -0
  62. data/lib/active_storage_validations/shared/asv_optionable.rb +29 -0
  63. data/lib/active_storage_validations/shared/asv_symbolizable.rb +14 -0
  64. data/lib/active_storage_validations/size_validator.rb +24 -40
  65. data/lib/active_storage_validations/total_size_validator.rb +51 -0
  66. data/lib/active_storage_validations/version.rb +1 -1
  67. data/lib/active_storage_validations.rb +20 -6
  68. metadata +127 -21
  69. data/lib/active_storage_validations/metadata.rb +0 -123
data/README.md CHANGED
@@ -1,213 +1,735 @@
1
+ [<img src="https://github.com/igorkasyanchuk/rails_time_travel/blob/main/docs/more_gems.png?raw=true"
2
+ />](https://www.railsjazz.com/?utm_source=github&utm_medium=top&utm_campaign=active_storage_validations)
3
+
1
4
  # Active Storage Validations
2
5
 
3
6
  [![MiniTest](https://github.com/igorkasyanchuk/active_storage_validations/workflows/MiniTest/badge.svg)](https://github.com/igorkasyanchuk/active_storage_validations/actions)
4
7
  [![RailsJazz](https://github.com/igorkasyanchuk/rails_time_travel/blob/main/docs/my_other.svg?raw=true)](https://www.railsjazz.com)
5
8
  [![https://www.patreon.com/igorkasyanchuk](https://github.com/igorkasyanchuk/rails_time_travel/blob/main/docs/patron.svg?raw=true)](https://www.patreon.com/igorkasyanchuk)
6
9
 
7
- If you are using `active_storage` gem and you want to add simple validations for it, like presence or content_type you need to write a custom validation method.
8
10
 
9
- This gems doing it for you. Just use `attached: true` or `content_type: 'image/png'` validation.
11
+ Active Storage Validations is a gem that allows you to add validations for Active Storage attributes.
12
+
13
+ This gems is doing it right for you! Just use `validates :avatar, attached: true, content_type: 'image/png'` and that's it!
14
+
15
+ ## Table of Contents
16
+
17
+ - [Getting started](#getting-started)
18
+ - [Installation](#installation)
19
+ - [Error messages (I18n)](#error-messages-i18n)
20
+ - [Using image metadata validators](#using-image-metadata-validators)
21
+ - [Using video and audio metadata validators](#using-video-and-audio-metadata-validators)
22
+ - [Validators](#validators)
23
+ - [Attached](#attached)
24
+ - [Limit](#limit)
25
+ - [Content type](#content-type)
26
+ - [Size](#size)
27
+ - [Total size](#total-size)
28
+ - [Dimension](#dimension)
29
+ - [Duration](#duration)
30
+ - [Aspect ratio](#aspect-ratio)
31
+ - [Processable file](#processable-file)
32
+ - [Upgrading from 1.x to 2.x](#upgrading-from-1x-to-2x)
33
+ - [Internationalization (I18n)](#internationalization-i18n)
34
+ - [Test matchers](#test-matchers)
35
+ - [Contributing](#contributing)
36
+ - [Additional information](#additional-information)
37
+
38
+ ## Getting started
39
+
40
+ ### Installation
41
+
42
+ Active Storage Validations work with Rails 6.1.4 onwards. Add this line to your application's Gemfile:
43
+
44
+ ```ruby
45
+ gem 'active_storage_validations'
46
+ ```
47
+
48
+ And then execute:
49
+
50
+ ```sh
51
+ $ bundle
52
+ ```
53
+
54
+ ### Error messages (I18n)
55
+
56
+ Once you have installed the gem, you need to add the gem I18n error messages to your app. See [Internationalization (I18n)](#internationalization-i18n) section for more details.
57
+
58
+ ### Using image metadata validators
59
+
60
+ Optionally, to use the image metadata validators (`dimension`, `aspect_ratio` and `processable_file`), you will have to add one of the corresponding gems:
61
+
62
+ ```ruby
63
+ gem 'mini_magick', '>= 4.9.5'
64
+ # Or
65
+ gem 'ruby-vips', '>= 2.1.0'
66
+ ```
67
+
68
+ Plus, you have to be sure to have the corresponding command-line tool installed on your system. For example, to use `mini_magick` gem, you need to have `imagemagick` installed on your system (both on your local and in your CI / production environments).
69
+
70
+ ### Using video and audio metadata validators
71
+
72
+ To use the video and audio metadata validators (`dimension`, `aspect_ratio`, `processable_file` and `duration`), you will not need to add any gems. However you will need to have the `ffmpeg` command-line tool installed on your system (once again, be sure to have it installed both on your local and in your CI / production environments).
10
73
 
11
- ## What it can do
74
+ ### Using content type spoofing protection validator option
12
75
 
13
- * validates if file(s) attached
14
- * validates content type
15
- * validates size of files
16
- * validates dimension of images/videos
17
- * validates number of uploaded files (min/max required)
18
- * validates aspect ratio (if square, portrait, landscape, is_16_9, ...)
19
- * custom error messages
76
+ To use the `spoofing_protection` option with the `content_type` validator, you only need to have the UNIX `file` command on your system.
20
77
 
21
- ## Usage
78
+ ## Validators
22
79
 
23
- For example you have a model like this and you want to add validation.
80
+ **List of validators:**
81
+ - [Attached](#attached): validates if file(s) attached
82
+ - [Limit](#limit): validates number of uploaded files
83
+ - [Content type](#content-type): validates file content type
84
+ - [Size](#size): validates file size
85
+ - [Total size](#total-size): validates total file size for several files
86
+ - [Dimension](#dimension): validates image / video dimensions
87
+ - [Duration](#duration): validates video / audio duration
88
+ - [Aspect ratio](#aspect-ratio): validates image / video aspect ratio
89
+ - [Processable file](#processable-file): validates if a file can be processed
90
+ <br>
91
+ <br>
24
92
 
93
+ **Proc usage**<br>
94
+ Every validator can use procs instead of values in all the validator examples:
25
95
  ```ruby
26
96
  class User < ApplicationRecord
27
- has_one_attached :avatar
28
- has_many_attached :photos
29
- has_one_attached :image
30
-
31
- validates :name, presence: true
32
-
33
- validates :avatar, attached: true, content_type: 'image/png',
34
- dimension: { width: 200, height: 200 }
35
- validates :photos, attached: true, content_type: ['image/png', 'image/jpeg'],
36
- dimension: { width: { min: 800, max: 2400 },
37
- height: { min: 600, max: 1800 }, message: 'is not given between dimension' }
38
- validates :image, attached: true,
39
- content_type: ['image/png', 'image/jpeg'],
40
- aspect_ratio: :landscape
97
+ has_many_attached :files
98
+
99
+ validates :files, limit: { max: -> (record) { record.admin? ? 100 : 10 } }
41
100
  end
42
101
  ```
43
102
 
44
- or
103
+ **Performance optimization**<br>
104
+ Some validators rely on an expensive operation (metadata analysis and content type analysis). To mitigate the performance cost, the gem leverages the `ActiveStorage::Blob.metadata` method to store retrieved metadata. Therefore, once the file has been analyzed by our gem, the expensive analysis operation will not be triggered again for new validations.
105
+
106
+ As stated in the Rails documentation: "Blobs are intended to be immutable in so far as their reference to a specific file goes". We based our performance optimization on the same assumption, so if you do not follow it, the gem will not work as expected.
107
+
108
+ ---
109
+
110
+ ### Attached
111
+
112
+ Validates if the attachment is present.
113
+
114
+ #### Options
115
+
116
+ The `attached` validator has no options.
45
117
 
118
+ #### Examples
119
+
120
+ Use it like this:
46
121
  ```ruby
47
- class Project < ApplicationRecord
48
- has_one_attached :logo
49
- has_one_attached :preview
50
- has_one_attached :attachment
51
- has_many_attached :documents
52
-
53
- validates :title, presence: true
54
-
55
- validates :logo, attached: true, size: { less_than: 100.megabytes , message: 'is too large' }
56
- validates :preview, attached: true, size: { between: 1.kilobyte..100.megabytes , message: 'is not given between size' }
57
- validates :attachment, attached: true, content_type: { in: 'application/pdf', message: 'is not a PDF' }
58
- validates :documents, limit: { min: 1, max: 3 }
122
+ class User < ApplicationRecord
123
+ has_one_attached :avatar
124
+
125
+ validates :avatar, attached: true # ensures that avatar has an attached file
59
126
  end
60
127
  ```
61
128
 
62
- ### More examples
129
+ #### Error messages (I18n)
130
+
131
+ ```yml
132
+ en:
133
+ errors:
134
+ messages:
135
+ blank: "can't be blank"
136
+ ```
137
+
138
+ The error message for this validator relies on Rails own `blank` error message.
139
+
140
+ ---
141
+
142
+ ### Limit
143
+
144
+ Validates the number of uploaded files.
145
+
146
+ #### Options
63
147
 
64
- - 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).
148
+ The `limit` validator has 2 possible options:
149
+ - `min`: defines the minimum allowed number of files
150
+ - `max`: defines the maximum allowed number of files
65
151
 
152
+ #### Examples
153
+
154
+ Use it like this:
66
155
  ```ruby
67
156
  class User < ApplicationRecord
68
- has_one_attached :avatar
69
- has_many_attached :photos
70
-
71
- validates :avatar, attached: true, content_type: :png # Marcel::Magic.by_extension(:png).to_s => 'image/png'
72
- # Rails <= 6.1.3; MimeMagic.by_extension(:png).to_s => 'image/png'
73
- # or
74
- validates :photos, attached: true, content_type: [:png, :jpg, :jpeg]
75
- # or
76
- validates :avatar, content_type: /\Aimage\/.*\z/
157
+ has_many_attached :certificates
158
+
159
+ validates :certificates, limit: { min: 1, max: 10 } # restricts the number of files to between 1 and 10
77
160
  end
78
161
  ```
79
162
 
80
- - Dimension validation with `width`, `height` and `in`.
163
+ #### Error messages (I18n)
164
+
165
+ ```yml
166
+ en:
167
+ errors:
168
+ messages:
169
+ limit_out_of_range:
170
+ zero: "no files attached (must have between %{min} and %{max} files)"
171
+ one: "only 1 file attached (must have between %{min} and %{max} files)"
172
+ other: "total number of files must be between %{min} and %{max} files (there are %{count} files attached)"
173
+ limit_min_not_reached:
174
+ zero: "no files attached (must have at least %{min} files)"
175
+ one: "only 1 file attached (must have at least %{min} files)"
176
+ other: "%{count} files attached (must have at least %{min} files)"
177
+ limit_max_exceeded:
178
+ zero: "no files attached (maximum is %{max} files)"
179
+ one: "too many files attached (maximum is %{max} files, got %{count})"
180
+ other: "too many files attached (maximum is %{max} files, got %{count})"
181
+ ```
182
+
183
+ The `limit` validator error messages expose 3 values that you can use:
184
+ - `min` containing the minimum allowed number of files (e.g. `1`)
185
+ - `max` containing the maximum allowed number of files (e.g. `10`)
186
+ - `count` containing the current number of files (e.g. `5`)
187
+
188
+ ---
189
+
190
+ ### Content type
191
+
192
+ Validates if the attachment has an allowed content type.
193
+
194
+ #### Options
195
+
196
+ The `content_type` validator has 3 possible options:
197
+ - `with`: defines the exact allowed content type (string, symbol or regex)
198
+ - `in`: defines the allowed content types (array of strings or symbols)
199
+ - `spoofing_protection`: enables content type spoofing protection (boolean, defaults to `false`)
81
200
 
201
+ As mentioned above, this validator can define content types in several ways:
202
+ - String: `image/png` or `png`
203
+ - Symbol: `:png`
204
+ - Regex: `/\Avideo\/.*\z/`
205
+
206
+ #### Examples
207
+
208
+ Use it like this:
82
209
  ```ruby
83
210
  class User < ApplicationRecord
84
211
  has_one_attached :avatar
85
- has_many_attached :photos
86
212
 
87
- validates :avatar, dimension: { width: { in: 80..100 }, message: 'is not given between dimension' }
88
- validates :photos, dimension: { height: { in: 600..1800 } }
213
+ validates :avatar, content_type: 'image/png' # only allows PNG images
214
+ validates :avatar, content_type: :png # only allows PNG images, same as { with: :png }
215
+ validates :avatar, content_type: /\Avideo\/.*\z/ # only allows video files
216
+ validates :avatar, content_type: ['image/png', 'image/jpeg'] # only allows PNG and JPEG images
217
+ validates :avatar, content_type: { in: [:png, :jpeg], spoofing_protection: true } # only allows PNG, JPEG and their variants, with spoofing protection enabled
89
218
  end
90
219
  ```
91
220
 
92
- - Dimension validation with `min` and `max` range for width and height:
221
+ #### Best practices
93
222
 
223
+ When using the `content_type` validator, it is recommended to reflect the allowed content types in the html [`accept`](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/accept) attribute in the corresponding file field in your views. This will prevent users from trying to upload files with not allowed content types (however it is only an UX improvement, a malicious user can still try to upload files with not allowed content types therefore the backend validation).
224
+
225
+ For example, if you want to allow PNG and JPEG images only, you can do this:
94
226
  ```ruby
95
227
  class User < ApplicationRecord
228
+ ACCEPTED_CONTENT_TYPES = ['image/png', 'image/jpeg'].freeze
229
+
96
230
  has_one_attached :avatar
97
- has_many_attached :photos
98
-
99
- validates :avatar, dimension: { min: 200..100 }
100
- # Equivalent to:
101
- # validates :avatar, dimension: { width: { min: 200 }, height: { min: 100 } }
102
- validates :photos, dimension: { min: 200..100, max: 400..200 }
103
- # Equivalent to:
104
- # validates :avatar, dimension: { width: { min: 200, max: 400 }, height: { min: 100, max: 200 } }
231
+
232
+ validates :avatar, content_type: ACCEPTED_CONTENT_TYPES
105
233
  end
106
234
  ```
107
235
 
108
- - Aspect ratio validation:
236
+ ```erb
237
+ <%= form_with model: @user do |f| %>
238
+ <%= f.file_field :avatar,
239
+ accept: ACCEPTED_CONTENT_TYPES.join(',') %>
240
+ <% end %>
241
+ ```
242
+
243
+ #### Content type shorthands
244
+
245
+ If you choose to use a content_type 'shorthand' (like `png`), note that it will be converted to a full content type using `Marcel::MimeType.for` under the hood. Therefore, you should check if the content_type is registered by [`Marcel::EXTENSIONS`](https://github.com/rails/marcel/blob/main/lib/marcel/tables.rb). If it's not, you can register it by adding the following code to your `config/initializers/mime_types.rb` file:
246
+
247
+ ```ruby
248
+ Marcel::MimeType.extend "application/ino", extensions: %w(ino), parents: "text/plain" # Registering arduino INO files
249
+ ```
250
+
251
+ Be sure to at least include one the the `extensions`, `parents` or `magic` option, otherwise the content type will not be registered.
252
+
253
+ #### Content type spoofing protection
254
+
255
+ By default, the gem does not prevent content type spoofing. You can enable it by setting the `spoofing_protection` option to `true` in your validator options.
256
+
257
+ <details>
258
+ <summary>
259
+ What is content type spoofing?
260
+ </summary>
261
+
262
+ File content type spoofing happens when an ill-intentioned user uploads a file which hides its true content type by faking its extension and its declared content type value. For example, a user may try to upload a `.exe` file (application/x-msdownload content type) dissimulated as a `.jpg` file (image/jpeg content type).
263
+ </details>
264
+
265
+ <details>
266
+ <summary>
267
+ How do we prevent it?
268
+ </summary>
269
+
270
+ The spoofing protection relies on both the UNIX `file` command and `Marcel` gem. Be careful, since it needs to load the whole file io to perform the analysis, it will use a lot of RAM for very large files. Therefore it could be a wise decision not to enable it in this case.
271
+
272
+ Take note that the `file` analyzer will not find the exactly same content type as the ActiveStorage blob (ActiveStorage content type detection relies on a different logic using first 4kb of content + filename + extension). To handle this issue, we consider a close parent content type to be a match. For example, for an ActiveStorage blob which content type is `video/x-ms-wmv`, the `file` analyzer will probably detect a `video/x-ms-asf` content type, this will be considered as a valid match because these 2 content types are closely related. The correlation mapping is based on `Marcel::TYPE_PARENTS` table.
273
+ </details>
274
+
275
+ <details>
276
+ <summary>
277
+ Edge cases
278
+ </summary>
279
+
280
+ The difficulty to accurately predict a mime type may generate false positives, if so there are two solutions available:
281
+ - If the ActiveStorage blob content type is closely related to the detected content type using the `file` analyzer, you can enhance `Marcel::TYPE_PARENTS` mapping using `Marcel::MimeType.extend "application/x-rar-compressed", parents: %(application/x-rar)` in the `config/initializers/mime_types.rb` file. (Please drop an issue so we can add it to the gem for everyone!)
282
+ - If the ActiveStorage blob content type is not closely related, you still can disable the content type spoofing protection in the validator, if so, please drop us an issue so we can fix it for everyone!
283
+ </details>
284
+
285
+
286
+ #### Error messages (I18n)
287
+
288
+ ```yml
289
+ en:
290
+ errors:
291
+ messages:
292
+ content_type_invalid:
293
+ one: "has an invalid content type (authorized content type is %{authorized_human_content_types})"
294
+ other: "has an invalid content type (authorized content types are %{authorized_human_content_types})"
295
+ content_type_spoofed:
296
+ one: "has a content type that is not equivalent to the one that is detected through its content (authorized content type is %{authorized_human_content_types})"
297
+ other: "has a content type that is not equivalent to the one that is detected through its content (authorized content types are %{authorized_human_content_types})"
298
+ ```
299
+
300
+ The `content_type` validator error messages expose 7 values that you can use:
301
+ - `content_type` containing the content type of the sent file (e.g. `image/png`)
302
+ - `human_content_type` containing a more user-friendly version of the sent file content type (e.g. 'TXT' for 'text/plain')
303
+ - `detected_content_type` containing the detected content type of the sent file using `spoofing_protection` option (e.g. `image/png`)
304
+ - `detected_human_content_type` containing a more user-friendly version of the sent file detected content type using `spoofing_protection` option (e.g. 'TXT' for 'text/plain')
305
+ - `authorized_human_content_types` containing the list of authorized content types (e.g. 'PNG, JPEG' for `['image/png', 'image/jpeg']`)
306
+ - `count` containing the number of authorized content types (e.g. `2`)
307
+ - `filename` containing the filename
308
+
309
+ ---
310
+
311
+ ### Size
312
+
313
+ Validates each attached file size.
314
+
315
+ #### Options
316
+
317
+ The `size` validator has 5 possible options:
318
+ - `less_than`: defines the strict maximum allowed file size
319
+ - `less_than_or_equal_to`: defines the maximum allowed file size
320
+ - `greater_than`: defines the strict minimum allowed file size
321
+ - `greater_than_or_equal_to`: defines the minimum allowed file size
322
+ - `between`: defines the allowed file size range
323
+
324
+ #### Examples
109
325
 
326
+ Use it like this:
110
327
  ```ruby
111
328
  class User < ApplicationRecord
112
329
  has_one_attached :avatar
113
- has_one_attached :photo
114
- has_many_attached :photos
115
330
 
116
- validates :avatar, aspect_ratio: :square
117
- validates :photo, aspect_ratio: :landscape
331
+ validates :avatar, size: { less_than: 2.megabytes } # restricts the file size to < 2MB
332
+ validates :avatar, size: { less_than_or_equal_to: 2.megabytes } # restricts the file size to <= 2MB
333
+ validates :avatar, size: { greater_than: 1.kilobyte } # restricts the file size to > 1KB
334
+ validates :avatar, size: { greater_than_or_equal_to: 1.kilobyte } # restricts the file size to >= 1KB
335
+ validates :avatar, size: { between: 1.kilobyte..2.megabytes } # restricts the file size to between 1KB and 2MB
336
+ end
337
+ ```
338
+
339
+ #### Best practices
340
+
341
+ It is always a good practice to limit the maximum file size to a reasonable value (like 2MB for avatar images). This helps prevent server storage issues, reduces upload/download times, and ensures better performance. Large files can consume excessive bandwidth and storage space, potentially impacting both server resources and user experience.
342
+ Plus, not setting a size limit inside your Rails app might lead into your server throwing a `413 Content Too Large` error, which is not as nice as a Rails validation error.
343
+
344
+ #### Error messages (I18n)
345
+
346
+ ```yml
347
+ en:
348
+ errors:
349
+ messages:
350
+ file_size_not_less_than: "file size must be less than %{max} (current size is %{file_size})"
351
+ file_size_not_less_than_or_equal_to: "file size must be less than or equal to %{max} (current size is %{file_size})"
352
+ file_size_not_greater_than: "file size must be greater than %{min} (current size is %{file_size})"
353
+ file_size_not_greater_than_or_equal_to: "file size must be greater than or equal to %{min} (current size is %{file_size})"
354
+ file_size_not_between: "file size must be between %{min} and %{max} (current size is %{file_size})"
355
+ ```
356
+
357
+ The `size` validator error messages expose 4 values that you can use:
358
+ - `file_size` containing the current file size (e.g. `1.5MB`)
359
+ - `min` containing the minimum allowed file size (e.g. `1KB`)
360
+ - `max` containing the maximum allowed file size (e.g. `2MB`)
361
+ - `filename` containing the current file name
362
+
363
+ ---
364
+
365
+ ### Total size
366
+
367
+ Validates the total file size for several files.
118
368
 
119
- # you can also pass dynamic aspect ratio, like :is_4_3, :is_16_9, etc
120
- validates :photos, aspect_ratio: :is_4_3
369
+ #### Options
370
+
371
+ The `total_size` validator has 5 possible options:
372
+ - `less_than`: defines the strict maximum allowed total file size
373
+ - `less_than_or_equal_to`: defines the maximum allowed total file size
374
+ - `greater_than`: defines the strict minimum allowed total file size
375
+ - `greater_than_or_equal_to`: defines the minimum allowed total file size
376
+ - `between`: defines the allowed total file size range
377
+
378
+ #### Examples
379
+
380
+ Use it like this:
381
+ ```ruby
382
+ class User < ApplicationRecord
383
+ has_many_attached :certificates
384
+
385
+ validates :certificates, total_size: { less_than: 10.megabytes } # restricts the total size to < 10MB
386
+ validates :certificates, total_size: { less_than_or_equal_to: 10.megabytes } # restricts the total size to <= 10MB
387
+ validates :certificates, total_size: { greater_than: 1.kilobyte } # restricts the total size to > 1KB
388
+ validates :certificates, total_size: { greater_than_or_equal_to: 1.kilobyte } # restricts the total size to >= 1KB
389
+ validates :certificates, total_size: { between: 1.kilobyte..10.megabytes } # restricts the total size to between 1KB and 10MB
121
390
  end
122
391
  ```
123
392
 
124
- ## Internationalization (I18n)
393
+ #### Error messages (I18n)
394
+
395
+ ```yml
396
+ en:
397
+ errors:
398
+ messages:
399
+ total_file_size_not_less_than: "total file size must be less than %{max} (current size is %{total_file_size})"
400
+ total_file_size_not_less_than_or_equal_to: "total file size must be less than or equal to %{max} (current size is %{total_file_size})"
401
+ total_file_size_not_greater_than: "total file size must be greater than %{min} (current size is %{total_file_size})"
402
+ total_file_size_not_greater_than_or_equal_to: "total file size must be greater than or equal to %{min} (current size is %{total_file_size})"
403
+ total_file_size_not_between: "total file size must be between %{min} and %{max} (current size is %{total_file_size})"
404
+ ```
405
+
406
+ The `total_size` validator error messages expose 4 values that you can use:
407
+ - `total_file_size` containing the current total file size (e.g. `1.5MB`)
408
+ - `min` containing the minimum allowed total file size (e.g. `1KB`)
409
+ - `max` containing the maximum allowed total file size (e.g. `2MB`)
410
+
411
+ ---
125
412
 
126
- Active Storage Validations uses I18n for error messages. For this, add these keys in your translation file:
413
+ ### Dimension
414
+
415
+ Validates the dimension of the attached image / video files.
416
+
417
+ #### Options
418
+
419
+ The `dimension` validator has several possible options:
420
+ - `width`: defines the exact allowed width (integer)
421
+ - `min`: defines the minimum allowed width (integer)
422
+ - `max`: defines the maximum allowed width (integer)
423
+ - `in`: defines the allowed width range (range)
424
+ - `height`: defines the exact allowed height (integer)
425
+ - `min`: defines the minimum allowed height (integer)
426
+ - `max`: defines the maximum allowed height (integer)
427
+ - `in`: defines the allowed height range (range)
428
+ - `min`: defines the minimum allowed width and height (range)
429
+ - `max`: defines the maximum allowed width and height (range)
430
+
431
+ #### Examples
432
+
433
+ Use it like this:
434
+ ```ruby
435
+ class User < ApplicationRecord
436
+ has_one_attached :avatar
437
+
438
+ validates :avatar, dimension: { width: 100 } # restricts the width to 100 pixels
439
+ validates :avatar, dimension: { width: { min: 80, max: 100 } } # restricts the width to between 80 and 100 pixels
440
+ validates :avatar, dimension: { width: { in: 80..100 } } # restricts the width to between 80 and 100 pixels
441
+ validates :avatar, dimension: { height: 100 } # restricts the height to 100 pixels
442
+ validates :avatar, dimension: { height: { min: 600, max: 1800 } } # restricts the height to between 600 and 1800 pixels
443
+ validates :avatar, dimension: { height: { in: 600..1800 } } # restricts the height to between 600 and 1800 pixels
444
+ validates :avatar, dimension: { min: 80..600, max: 100..1800 } # restricts the width to between 80 and 100 pixels, and the height to between 600 and 1800 pixels
445
+ end
446
+ ```
447
+
448
+ #### Error messages (I18n)
127
449
 
128
450
  ```yml
129
451
  en:
130
452
  errors:
131
453
  messages:
132
- content_type_invalid: "has an invalid content type"
133
- file_size_out_of_range: "size %{file_size} is not between required range"
134
- limit_out_of_range: "total number is out of range"
135
- image_metadata_missing: "is not a valid image"
136
- dimension_min_inclusion: "must be greater than or equal to %{width} x %{height} pixel."
137
- dimension_max_inclusion: "must be less than or equal to %{width} x %{height} pixel."
138
- dimension_width_inclusion: "width is not included between %{min} and %{max} pixel."
139
- dimension_height_inclusion: "height is not included between %{min} and %{max} pixel."
140
- dimension_width_greater_than_or_equal_to: "width must be greater than or equal to %{length} pixel."
141
- dimension_height_greater_than_or_equal_to: "height must be greater than or equal to %{length} pixel."
142
- dimension_width_less_than_or_equal_to: "width must be less than or equal to %{length} pixel."
143
- dimension_height_less_than_or_equal_to: "height must be less than or equal to %{length} pixel."
144
- dimension_width_equal_to: "width must be equal to %{length} pixel."
145
- dimension_height_equal_to: "height must be equal to %{length} pixel."
146
- aspect_ratio_not_square: "must be a square image"
147
- aspect_ratio_not_portrait: "must be a portrait image"
148
- aspect_ratio_not_landscape: "must be a landscape image"
149
- aspect_ratio_is_not: "must have an aspect ratio of %{aspect_ratio}"
150
- aspect_ratio_unknown: "has an unknown aspect ratio"
151
- ```
152
-
153
- In some cases, Active Storage Validations provides variables to help you customize messages:
154
-
155
- The "content_type_invalid" key has two variables that you can use, a variable named "content_type" containing the content type of the send file and a variable named "authorized_types" containing the list of authorized content types.
156
-
157
- The variables are not used by default to leave the choice to the user.
158
-
159
- For example :
454
+ dimension_min_not_included_in: "must be greater than or equal to %{width} x %{height} pixel"
455
+ dimension_max_not_included_in: "must be less than or equal to %{width} x %{height} pixel"
456
+ dimension_width_not_included_in: "width is not included between %{min} and %{max} pixel"
457
+ dimension_height_not_included_in: "height is not included between %{min} and %{max} pixel"
458
+ dimension_width_not_greater_than_or_equal_to: "width must be greater than or equal to %{length} pixel"
459
+ dimension_height_not_greater_than_or_equal_to: "height must be greater than or equal to %{length} pixel"
460
+ dimension_width_not_less_than_or_equal_to: "width must be less than or equal to %{length} pixel"
461
+ dimension_height_not_less_than_or_equal_to: "height must be less than or equal to %{length} pixel"
462
+ dimension_width_not_equal_to: "width must be equal to %{length} pixel"
463
+ dimension_height_not_equal_to: "height must be equal to %{length} pixel"
464
+ media_metadata_missing: "is not a valid media file"
465
+ ```
466
+
467
+ The `dimension` validator error messages expose 6 values that you can use:
468
+ - `min` containing the minimum width or height allowed
469
+ - `max` containing the maximum width or height allowed
470
+ - `width` containing the minimum or maximum width allowed
471
+ - `height` containing the minimum or maximum width allowed
472
+ - `length` containing the exact width or height allowed
473
+ - `filename` containing the current filename in error
474
+
475
+ ---
476
+
477
+ ### Duration
478
+
479
+ Validates the duration of the attached audio / video files.
480
+
481
+ #### Options
482
+
483
+ The `duration` validator has 5 possible options:
484
+ - `less_than`: defines the strict maximum allowed file duration
485
+ - `less_than_or_equal_to`: defines the maximum allowed file duration
486
+ - `greater_than`: defines the strict minimum allowed file duration
487
+ - `greater_than_or_equal_to`: defines the minimum allowed file duration
488
+ - `between`: defines the allowed file duration range
489
+
490
+ #### Examples
491
+
492
+ Use it like this:
493
+ ```ruby
494
+ class User < ApplicationRecord
495
+ has_one_attached :avatar
496
+
497
+ validates :avatar, duration: { less_than: 2.minutes } # restricts the file duration to < 2 minutes
498
+ validates :avatar, duration: { less_than_or_equal_to: 2.minutes } # restricts the file duration to <= 2 minutes
499
+ validates :avatar, duration: { greater_than: 1.second } # restricts the file duration to > 1 second
500
+ validates :avatar, duration: { greater_than_or_equal_to: 1.second } # restricts the file duration to >= 1 second
501
+ validates :avatar, duration: { between: 1.second..2.minutes } # restricts the file duration to between 1 second and 2 minutes
502
+ end
503
+ ```
504
+
505
+ #### Error messages (I18n)
160
506
 
161
507
  ```yml
162
- content_type_invalid: "has an invalid content type : %{content_type}"
508
+ en:
509
+ errors:
510
+ messages:
511
+ duration_not_less_than: "duration must be less than %{max} (current duration is %{duration})"
512
+ duration_not_less_than_or_equal_to: "duration must be less than or equal to %{max} (current duration is %{duration})"
513
+ duration_not_greater_than: "duration must be greater than %{min} (current duration is %{duration})"
514
+ duration_not_greater_than_or_equal_to: "duration must be greater than or equal to %{min} (current duration is %{duration})"
515
+ duration_not_between: "duration must be between %{min} and %{max} (current duration is %{duration})"
163
516
  ```
164
517
 
165
- Also the "limit_out_of_range" key supports two variables the "min" and "max".
518
+ The `duration` validator error messages expose 4 values that you can use:
519
+ - `duration` containing the current duration size (e.g. `2 minutes`)
520
+ - `min` containing the minimum allowed duration size (e.g. `1 second`)
521
+ - `max` containing the maximum allowed duration size (e.g. `2 minutes`)
522
+ - `filename` containing the current file name
523
+
524
+ ---
525
+
526
+ ### Aspect ratio
527
+
528
+ Validates the aspect ratio of the attached files.
529
+
530
+ #### Options
531
+
532
+ The `aspect_ratio` validator has several options:
533
+ - `with`: defines the exact allowed aspect ratio (e.g. `:is_16/9`)
534
+ - `in`: defines the allowed aspect ratios (e.g. `%i[square landscape]`)
535
+
536
+ This validator can define aspect ratios in several ways:
537
+ - Symbols:
538
+ - prebuilt aspect ratios: `:square`, `:portrait`, `:landscape`
539
+ - custom aspect ratios (it must be of type `is_xx_yy`): `:is_16_9`, `:is_4_3`, etc.
540
+
541
+ #### Examples
542
+
543
+ Use it like this:
544
+ ```ruby
545
+ class User < ApplicationRecord
546
+ has_one_attached :avatar
547
+
548
+ validates :avatar, aspect_ratio: :square # restricts the aspect ratio to 1:1
549
+ validates :avatar, aspect_ratio: :portrait # restricts the aspect ratio to x:y where y > x
550
+ validates :avatar, aspect_ratio: :landscape # restricts the aspect ratio to x:y where x > y
551
+ validates :avatar, aspect_ratio: :is_16_9 # restricts the aspect ratio to 16:9
552
+ validates :avatar, aspect_ratio: %i[square is_16_9] # restricts the aspect ratio to 1:1 and 16:9
553
+ end
554
+ ```
166
555
 
167
- For example :
556
+ #### Error messages (I18n)
168
557
 
169
558
  ```yml
170
- limit_out_of_range: "total number is out of range. range: [%{min}, %{max}]"
559
+ en:
560
+ errors:
561
+ messages:
562
+ aspect_ratio_not_square: "must be square (current file is %{width}x%{height}px)"
563
+ aspect_ratio_not_portrait: "must be portrait (current file is %{width}x%{height}px)"
564
+ aspect_ratio_not_landscape: "must be landscape (current file is %{width}x%{height}px)"
565
+ aspect_ratio_not_x_y: "must be %{authorized_aspect_ratios} (current file is %{width}x%{height}px)"
566
+ aspect_ratio_invalid: "has an invalid aspect ratio (valid aspect ratios are %{authorized_aspect_ratios})"
567
+ media_metadata_missing: "is not a valid media file"
171
568
  ```
172
569
 
173
- ## Installation
570
+ The `aspect_ratio` validator error messages expose 4 values that you can use:
571
+ - `authorized_aspect_ratios` containing the authorized aspect ratios
572
+ - `width` containing the current width of the image/video
573
+ - `height` containing the current height of the image/video
574
+ - `filename` containing the current filename in error
575
+
576
+ ---
577
+
578
+ ### Processable file
579
+
580
+ Validates if the attached files can be processed by MiniMagick or Vips (image) or ffmpeg (video/audio).
581
+
582
+ #### Options
583
+
584
+ The `processable_file` validator has no options.
174
585
 
175
- Add this line to your application's Gemfile:
586
+ #### Examples
176
587
 
588
+ Use it like this:
177
589
  ```ruby
178
- # Rails 5.2 and Rails 6
179
- gem 'active_storage_validations'
590
+ class User < ApplicationRecord
591
+ has_one_attached :avatar
180
592
 
181
- # Optional, to use :dimension validator or :aspect_ratio validator
182
- gem 'mini_magick', '>= 4.9.5'
183
- # Or
184
- gem 'ruby-vips', '>= 2.1.0'
593
+ validates :avatar, processable_file: true # ensures that the file is processable by MiniMagick or Vips (image) or ffmpeg (video/audio)
594
+ end
185
595
  ```
186
596
 
187
- And then execute:
597
+ #### Error messages (I18n)
188
598
 
189
- ```bash
190
- $ bundle
599
+ ```yml
600
+ en:
601
+ errors:
602
+ messages:
603
+ file_not_processable: "is not identified as a valid media file"
191
604
  ```
192
605
 
193
- ## Sample
606
+ The `processable_file` validator error messages expose 1 value that you can use:
607
+ - `filename` containing the current filename in error
608
+
609
+ ---
610
+
611
+ ## Upgrading from 1.x to 2.x
612
+
613
+ If you are upgrading from 1.x to 2.x, you will be pleased to note that a lot of things have been added and improved!
614
+
615
+ Added features:
616
+ - `duration` validator has been added for audio / video files
617
+ - `dimension` validator now supports videos
618
+ - `aspect_ratio` validator now supports videos
619
+ - `processable_image` validator is now `processable_file` validator and supports image/video/audio
620
+ - Major performance improvement have been added: we now only perform the expensive io analysis operation on the newly attached files. For previously attached files, we validate them using Rails `ActiveStorage::Blob#metadata` internal mecanism ([more here](https://github.com/rails/rails/blob/main/activestorage/app/models/active_storage/blob/analyzable.rb)).
621
+ - All error messages have been given an upgrade and new variables that you can use
622
+
623
+ But this major version bump also comes with some breaking changes. Below are the main breaking changes you need to be aware of:
624
+ - Error messages
625
+ - We advise you to replace all the v1 translations by the new v2 rather than changing them one by one. A majority of messages have been completely rewritten to be more consistent and easier to understand.
626
+ - If you wish to change them one by one, here is the list of changes to make:
627
+ - Some validator errors have been totally changed:
628
+ - `limit` validator keys have been totally reworked
629
+ - `dimension` validator keys have been totally reworked
630
+ - `content_type` validator keys have been totally reworked
631
+ - `processable_image` validator keys have been totally reworked
632
+ - Some keys have been changed:
633
+ - `image_metadata_missing` has been replaced by `media_metadata_missing`
634
+ - `aspect_ratio_is_not` has been replaced by `aspect_ratio_not_x_y`
635
+ - Some error messages variables names have been changed to improve readability:
636
+ - `aspect_ratio` validator:
637
+ - `aspect_ratio` has been replaced by `authorized_aspect_ratios`
638
+ - `content_type` validator:
639
+ - `authorized_types` has been replaced by `authorized_human_content_types`
640
+ - `size` validator:
641
+ - `min_size` has been replaced by `min`
642
+ - `max_size` has been replaced by `max`
643
+ - `total_size` validator:
644
+ - `min_size` has been replaced by `min`
645
+ - `max_size` has been replaced by `max`
646
+
647
+ - `content_type` validator
648
+ - The `:in` option now only accepts 'valid' content types (ie content types deemed by Marcel as valid).
649
+ - The check was mistakenly only performed on the `:with` option previously. Therefore, invalid content types were accepted in the `:in` option, which is not the expected behavior.
650
+ - This might break some cases when you had for example `content_type: ['image/png', 'image/jpg']`, because `image/jpg` is not a valid content type, it should be replaced by `image/jpeg`.
651
+ - An `ArgumentError` is now raised if `image/jpg` is used to make it easier to fix. You should now only use `image/jpeg`.
652
+
653
+ - `processable_image` validator
654
+ - The validator has been replaced by `processable_file` validator, be sure to replace `processable_image: true` to `processable_file: true`
655
+ - The associated matcher has also been updated accordingly, be sure to replace `validate_processable_image_of` to `validate_processable_file_of`
194
656
 
195
- Very simple example of validation with file attached, content type check and custom error message.
657
+ ## Internationalization (I18n)
658
+
659
+ Active Storage Validations uses I18n for error messages. Add these keys in your translation files to make them available:
660
+
661
+ ```yml
662
+ en:
663
+ errors:
664
+ messages:
665
+ content_type_invalid:
666
+ one: "has an invalid content type (authorized content type is %{authorized_human_content_types})"
667
+ other: "has an invalid content type (authorized content types are %{authorized_human_content_types})"
668
+ content_type_spoofed:
669
+ one: "has a content type that is not equivalent to the one that is detected through its content (authorized content type is %{authorized_human_content_types})"
670
+ other: "has a content type that is not equivalent to the one that is detected through its content (authorized content types are %{authorized_human_content_types})"
671
+ file_size_not_less_than: "file size must be less than %{max} (current size is %{file_size})"
672
+ file_size_not_less_than_or_equal_to: "file size must be less than or equal to %{max} (current size is %{file_size})"
673
+ file_size_not_greater_than: "file size must be greater than %{min} (current size is %{file_size})"
674
+ file_size_not_greater_than_or_equal_to: "file size must be greater than or equal to %{min} (current size is %{file_size})"
675
+ file_size_not_between: "file size must be between %{min} and %{max} (current size is %{file_size})"
676
+ total_file_size_not_less_than: "total file size must be less than %{max} (current size is %{total_file_size})"
677
+ total_file_size_not_less_than_or_equal_to: "total file size must be less than or equal to %{max} (current size is %{total_file_size})"
678
+ total_file_size_not_greater_than: "total file size must be greater than %{min} (current size is %{total_file_size})"
679
+ total_file_size_not_greater_than_or_equal_to: "total file size must be greater than or equal to %{min} (current size is %{total_file_size})"
680
+ total_file_size_not_between: "total file size must be between %{min} and %{max} (current size is %{total_file_size})"
681
+ duration_not_less_than: "duration must be less than %{max} (current duration is %{duration})"
682
+ duration_not_less_than_or_equal_to: "duration must be less than or equal to %{max} (current duration is %{duration})"
683
+ duration_not_greater_than: "duration must be greater than %{min} (current duration is %{duration})"
684
+ duration_not_greater_than_or_equal_to: "duration must be greater than or equal to %{min} (current duration is %{duration})"
685
+ duration_not_between: "duration must be between %{min} and %{max} (current duration is %{duration})"
686
+ limit_out_of_range:
687
+ zero: "no files attached (must have between %{min} and %{max} files)"
688
+ one: "only 1 file attached (must have between %{min} and %{max} files)"
689
+ other: "total number of files must be between %{min} and %{max} files (there are %{count} files attached)"
690
+ limit_min_not_reached:
691
+ zero: "no files attached (must have at least %{min} files)"
692
+ one: "only 1 file attached (must have at least %{min} files)"
693
+ other: "%{count} files attached (must have at least %{min} files)"
694
+ limit_max_exceeded:
695
+ zero: "no files attached (maximum is %{max} files)"
696
+ one: "too many files attached (maximum is %{max} files, got %{count})"
697
+ other: "too many files attached (maximum is %{max} files, got %{count})"
698
+ media_metadata_missing: "is not a valid media file"
699
+ dimension_min_not_included_in: "must be greater than or equal to %{width} x %{height} pixel"
700
+ dimension_max_not_included_in: "must be less than or equal to %{width} x %{height} pixel"
701
+ dimension_width_not_included_in: "width is not included between %{min} and %{max} pixel"
702
+ dimension_height_not_included_in: "height is not included between %{min} and %{max} pixel"
703
+ dimension_width_not_greater_than_or_equal_to: "width must be greater than or equal to %{length} pixel"
704
+ dimension_height_not_greater_than_or_equal_to: "height must be greater than or equal to %{length} pixel"
705
+ dimension_width_not_less_than_or_equal_to: "width must be less than or equal to %{length} pixel"
706
+ dimension_height_not_less_than_or_equal_to: "height must be less than or equal to %{length} pixel"
707
+ dimension_width_not_equal_to: "width must be equal to %{length} pixel"
708
+ dimension_height_not_equal_to: "height must be equal to %{length} pixel"
709
+ aspect_ratio_not_square: "must be square (current file is %{width}x%{height}px)"
710
+ aspect_ratio_not_portrait: "must be portrait (current file is %{width}x%{height}px)"
711
+ aspect_ratio_not_landscape: "must be landscape (current file is %{width}x%{height}px)"
712
+ aspect_ratio_not_x_y: "must be %{authorized_aspect_ratios} (current file is %{width}x%{height}px)"
713
+ aspect_ratio_invalid: "has an invalid aspect ratio (valid aspect ratios are %{authorized_aspect_ratios})"
714
+ file_not_processable: "is not identified as a valid media file"
715
+ ```
196
716
 
197
- [![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)
717
+ Other translation files are available [here](https://github.com/igorkasyanchuk/active_storage_validations/tree/master/config/locales).
198
718
 
199
719
  ## Test matchers
200
- Provides RSpec-compatible and Minitest-compatible matchers for testing the validators.
720
+
721
+ The gem also provides RSpec-compatible and Minitest-compatible matchers for testing the validators.
201
722
 
202
723
  ### RSpec
203
724
 
725
+ #### Setup
204
726
  In `spec_helper.rb`, you'll need to require the matchers:
205
727
 
206
728
  ```ruby
207
729
  require 'active_storage_validations/matchers'
208
730
  ```
209
731
 
210
- And _include_ the module:
732
+ And include the module:
211
733
 
212
734
  ```ruby
213
735
  RSpec.configure do |config|
@@ -215,41 +737,98 @@ RSpec.configure do |config|
215
737
  end
216
738
  ```
217
739
 
218
- Example (Note that the options are chainable):
740
+ #### Matchers
741
+ Matcher methods available:
219
742
 
220
743
  ```ruby
221
744
  describe User do
745
+ # aspect_ratio:
746
+ # #allowing, #rejecting
747
+ it { is_expected.to validate_aspect_ratio_of(:avatar).allowing(:square) }
748
+ it { is_expected.to validate_aspect_ratio_of(:avatar).rejecting(:portrait) }
749
+
750
+ # attached
222
751
  it { is_expected.to validate_attached_of(:avatar) }
223
752
 
753
+ # processable_file
754
+ it { is_expected.to validate_processable_file_of(:avatar) }
755
+
756
+ # limit
757
+ # #min, #max
758
+ it { is_expected.to validate_limits_of(:avatar).min(1) }
759
+ it { is_expected.to validate_limits_of(:avatar).max(5) }
760
+
761
+ # content_type:
762
+ # #allowing, #rejecting
224
763
  it { is_expected.to validate_content_type_of(:avatar).allowing('image/png', 'image/gif') }
225
764
  it { is_expected.to validate_content_type_of(:avatar).rejecting('text/plain', 'text/xml') }
226
765
 
766
+ # dimension:
767
+ # #width, #height, #width_min, #height_min, #width_max, #height_max, #width_between, #height_between
227
768
  it { is_expected.to validate_dimensions_of(:avatar).width(250) }
228
769
  it { is_expected.to validate_dimensions_of(:avatar).height(200) }
229
- it { is_expected.to validate_dimensions_of(:avatar).width(250).height(200).with_message('Invalid dimensions.') }
230
770
  it { is_expected.to validate_dimensions_of(:avatar).width_min(200) }
231
- it { is_expected.to validate_dimensions_of(:avatar).width_max(500) }
232
771
  it { is_expected.to validate_dimensions_of(:avatar).height_min(100) }
772
+ it { is_expected.to validate_dimensions_of(:avatar).width_max(500) }
233
773
  it { is_expected.to validate_dimensions_of(:avatar).height_max(300) }
234
774
  it { is_expected.to validate_dimensions_of(:avatar).width_between(200..500) }
235
775
  it { is_expected.to validate_dimensions_of(:avatar).height_between(100..300) }
236
776
 
777
+ # size:
778
+ # #less_than, #less_than_or_equal_to, #greater_than, #greater_than_or_equal_to, #between
237
779
  it { is_expected.to validate_size_of(:avatar).less_than(50.kilobytes) }
238
780
  it { is_expected.to validate_size_of(:avatar).less_than_or_equal_to(50.kilobytes) }
239
781
  it { is_expected.to validate_size_of(:avatar).greater_than(1.kilobyte) }
240
782
  it { is_expected.to validate_size_of(:avatar).greater_than_or_equal_to(1.kilobyte) }
241
783
  it { is_expected.to validate_size_of(:avatar).between(100..500.kilobytes) }
784
+
785
+ # total_size:
786
+ # #less_than, #less_than_or_equal_to, #greater_than, #greater_than_or_equal_to, #between
787
+ it { is_expected.to validate_total_size_of(:avatar).less_than(50.kilobytes) }
788
+ it { is_expected.to validate_total_size_of(:avatar).less_than_or_equal_to(50.kilobytes) }
789
+ it { is_expected.to validate_total_size_of(:avatar).greater_than(1.kilobyte) }
790
+ it { is_expected.to validate_total_size_of(:avatar).greater_than_or_equal_to(1.kilobyte) }
791
+ it { is_expected.to validate_total_size_of(:avatar).between(100..500.kilobytes) }
792
+
793
+ # duration:
794
+ # #less_than, #less_than_or_equal_to, #greater_than, #greater_than_or_equal_to, #between
795
+ it { is_expected.to validate_duration_of(:introduction).less_than(50.seconds) }
796
+ it { is_expected.to validate_duration_of(:introduction).less_than_or_equal_to(50.seconds) }
797
+ it { is_expected.to validate_duration_of(:introduction).greater_than(1.minute) }
798
+ it { is_expected.to validate_duration_of(:introduction).greater_than_or_equal_to(1.minute) }
799
+ it { is_expected.to validate_duration_of(:introduction).between(100..500.seconds) }
800
+ end
801
+ ```
802
+ (Note that matcher methods are chainable)
803
+
804
+ All matchers can currently be customized with Rails validation options:
805
+
806
+ ```ruby
807
+ describe User do
808
+ # :allow_blank
809
+ it { is_expected.to validate_attached_of(:avatar).allow_blank }
810
+
811
+ # :on
812
+ it { is_expected.to validate_attached_of(:avatar).on(:update) }
813
+ it { is_expected.to validate_attached_of(:avatar).on(%i[update custom]) }
814
+
815
+ # :message
816
+ it { is_expected.to validate_dimensions_of(:avatar).width(250).with_message('Invalid dimensions.') }
242
817
  end
243
818
  ```
244
819
 
245
820
  ### Minitest
246
- 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:
821
+
822
+ #### Setup
823
+ To use the matchers, make sure you have the [shoulda-context](https://github.com/thoughtbot/shoulda-context) gem up and running.
824
+
825
+ You need to require the matchers:
247
826
 
248
827
  ```ruby
249
828
  require 'active_storage_validations/matchers'
250
829
  ```
251
830
 
252
- And _extend_ the module:
831
+ And extend the module:
253
832
 
254
833
  ```ruby
255
834
  class ActiveSupport::TestCase
@@ -257,128 +836,57 @@ class ActiveSupport::TestCase
257
836
  end
258
837
  ```
259
838
 
260
- Example (Note that the options are chainable):
839
+ #### Matchers
840
+ 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.
261
841
 
262
- ```ruby
263
- class UserTest < ActiveSupport::TestCase
264
- should validate_attached_of(:avatar)
265
-
266
- should validate_content_type_of(:avatar).allowing('image/png', 'image/gif')
267
- should validate_content_type_of(:avatar).rejecting('text/plain', 'text/xml')
268
-
269
- should validate_dimensions_of(:avatar).width(250)
270
- should validate_dimensions_of(:avatar).height(200)
271
- should validate_dimensions_of(:avatar).width(250).height(200).with_message('Invalid dimensions.')
272
- should validate_dimensions_of(:avatar).width_min(200)
273
- should validate_dimensions_of(:avatar).width_max(500)
274
- should validate_dimensions_of(:avatar).height_min(100)
275
- should validate_dimensions_of(:avatar).height_max(300)
276
- should validate_dimensions_of(:avatar).width_between(200..500)
277
- should validate_dimensions_of(:avatar).height_between(100..300)
278
-
279
- should validate_size_of(:avatar).less_than(50.kilobytes)
280
- should validate_size_of(:avatar).less_than_or_equal_to(50.kilobytes)
281
- should validate_size_of(:avatar).greater_than(1.kilobyte)
282
- should validate_size_of(:avatar).greater_than_or_equal_to(1.kilobyte)
283
- should validate_size_of(:avatar).between(100..500.kilobytes)
284
- end
285
- ```
286
842
 
287
- ## Todo
843
+ ## Contributing
288
844
 
289
- * verify with remote storages (s3, etc)
290
- * verify how it works with direct upload
291
- * better error message when content_size is invalid
292
- * add more translations
845
+ If you want to contribute to the project, you will have to fork the repository and create a new branch from the `master` branch. Then build your feature, or fix the issue, and create a pull request. Be sure to add tests for your changes.
293
846
 
294
- ## Tests & Contributing
847
+ Before submitting your pull request, run the tests to make sure everything works as expected.
295
848
 
296
- To run tests in root folder of gem:
849
+ To run the gem tests, launch the following commands in the root folder of gem repository:
297
850
 
298
- * `BUNDLE_GEMFILE=gemfiles/rails_5_2.gemfile bundle exec rake test` to run for Rails 5.2
299
- * `BUNDLE_GEMFILE=gemfiles/rails_6_0.gemfile bundle exec rake test` to run for Rails 6.0
300
- * `BUNDLE_GEMFILE=gemfiles/rails_6_1.gemfile bundle exec rake test` to run for Rails 6.1
301
- * `BUNDLE_GEMFILE=gemfiles/rails_next.gemfile bundle exec rake test` to run for Rails main branch
851
+ * `BUNDLE_GEMFILE=gemfiles/rails_6_1_4.gemfile bundle exec rake test` to run for Rails 6.1.4
852
+ * `BUNDLE_GEMFILE=gemfiles/rails_7_0.gemfile bundle exec rake test` to run for Rails 7.0
853
+ * `BUNDLE_GEMFILE=gemfiles/rails_7_1.gemfile bundle exec rake test` to run for Rails 7.1
854
+ * `BUNDLE_GEMFILE=gemfiles/rails_7_2.gemfile bundle exec rake test` to run for Rails 7.2
855
+ * `BUNDLE_GEMFILE=gemfiles/rails_8_0.gemfile bundle exec rake test` to run for Rails 8.0
302
856
 
303
857
  Snippet to run in console:
304
858
 
305
- ```
306
- BUNDLE_GEMFILE=gemfiles/rails_5_2.gemfile bundle
307
- BUNDLE_GEMFILE=gemfiles/rails_6_0.gemfile bundle
308
- BUNDLE_GEMFILE=gemfiles/rails_6_1.gemfile bundle
859
+ ```bash
860
+ BUNDLE_GEMFILE=gemfiles/rails_6_1_4.gemfile bundle
309
861
  BUNDLE_GEMFILE=gemfiles/rails_7_0.gemfile bundle
310
- BUNDLE_GEMFILE=gemfiles/rails_next.gemfile bundle
311
- BUNDLE_GEMFILE=gemfiles/rails_5_2.gemfile bundle exec rake test
312
- BUNDLE_GEMFILE=gemfiles/rails_6_0.gemfile bundle exec rake test
313
- BUNDLE_GEMFILE=gemfiles/rails_6_1.gemfile bundle exec rake test
862
+ BUNDLE_GEMFILE=gemfiles/rails_7_1.gemfile bundle
863
+ BUNDLE_GEMFILE=gemfiles/rails_7_2.gemfile bundle
864
+ BUNDLE_GEMFILE=gemfiles/rails_8_0.gemfile bundle
865
+ BUNDLE_GEMFILE=gemfiles/rails_6_1_4.gemfile bundle exec rake test
314
866
  BUNDLE_GEMFILE=gemfiles/rails_7_0.gemfile bundle exec rake test
315
- BUNDLE_GEMFILE=gemfiles/rails_next.gemfile bundle exec rake test
867
+ BUNDLE_GEMFILE=gemfiles/rails_7_1.gemfile bundle exec rake test
868
+ BUNDLE_GEMFILE=gemfiles/rails_7_2.gemfile bundle exec rake test
869
+ BUNDLE_GEMFILE=gemfiles/rails_8_0.gemfile bundle exec rake test
316
870
  ```
317
871
 
318
- ## Known issues
872
+ Tips:
873
+ - To focus a specific test, use the `focus` class method provided by [minitest-focus](https://github.com/minitest/minitest-focus)
874
+ - To focus a specific file, use the TEST option provided by minitest, e.g. to only run `size_validator_test.rb` file you will launch the following command: `bundle exec rake test TEST=test/validators/size_validator_test.rb`
319
875
 
320
- - There is an issue in Rails which it possible to get if you have added a validation and generating for example an image preview of attachments. It can be fixed with this:
321
876
 
322
- ```erb
323
- <% if @user.avatar.attached? && @user.avatar.attachment.blob.present? && @user.avatar.attachment.blob.persisted? %>
324
- <%= image_tag @user.avatar %>
325
- <% end %>
326
- ```
877
+ ## Additional information
327
878
 
328
- This is a Rails issue, and is fixed in Rails 6.
879
+ ### Contributors (BIG THANK YOU!)
329
880
 
330
- ## Contributing
331
- You are welcome to contribute.
332
-
333
- ## Contributors (BIG THANK YOU)
334
- - https://github.com/schweigert
335
- - https://github.com/tleneveu
336
- - https://github.com/reckerswartz
337
- - https://github.com/Uysim
338
- - https://github.com/D-system
339
- - https://github.com/ivanelrey
340
- - https://github.com/phlegx
341
- - https://github.com/rr-dev
342
- - https://github.com/dsmalko
343
- - https://github.com/danderozier
344
- - https://github.com/cseelus
345
- - https://github.com/vkinelev
346
- - https://github.com/reed
347
- - https://github.com/connorshea
348
- - https://github.com/Atul9
349
- - https://github.com/victorbueno
350
- - https://github.com/UICJohn
351
- - https://github.com/giovannibonetti
352
- - https://github.com/dlepage
353
- - https://github.com/StefSchenkelaars
354
- - https://github.com/willnet
355
- - https://github.com/mohanklein
356
- - https://github.com/High5Apps
357
- - https://github.com/mschnitzer
358
- - https://github.com/sinankeskin
359
- - https://github.com/alejandrodevs
360
- - https://github.com/molfar
361
- - https://github.com/connorshea
362
- - https://github.com/yshmarov
363
- - https://github.com/fongfan999
364
- - https://github.com/cooperka
365
- - https://github.com/dolarsrg
366
- - https://github.com/jayshepherd
367
- - https://github.com/ohbarye
368
- - https://github.com/randsina
369
- - https://github.com/vietqhoang
370
- - https://github.com/kemenaran
371
- - https://github.com/jrmhaig
372
- - https://github.com/tagliala
373
- - https://github.com/evedovelli
374
- - https://github.com/JuanVqz
375
- - https://github.com/luiseugenio
376
- - https://github.com/equivalent
377
- - https://github.com/NARKOZ
378
-
379
- ## License
881
+ We have a long list of valued contributors. Check them all at:
882
+
883
+ https://github.com/igorkasyanchuk/active_storage_validations/graphs/contributors
884
+
885
+ ### License
380
886
 
381
887
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
382
888
 
889
+ <br>
890
+
383
891
  [<img src="https://github.com/igorkasyanchuk/rails_time_travel/blob/main/docs/more_gems.png?raw=true"
384
- />](https://www.railsjazz.com/)
892
+ />](https://www.railsjazz.com/?utm_source=github&utm_medium=bottom&utm_campaign=active_storage_validations)