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 
     | 
    
         |