active_storage_validations 1.3.5 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1fe705d1a3b76d875ddc0a61d7d3caa439ddad3ba4a8b5be3c203f8d8cdd00ca
4
- data.tar.gz: 826dbac96af4e1ed424ad16670aea3e428296ab07ae9bfdad29e4c0b453d114d
3
+ metadata.gz: 12a58907c84f972cb00aab13b6a60d26afda0383cbe88e13ec12b1c9d178ba36
4
+ data.tar.gz: 4df97d0b9e88054403ff7a39c89b00c4b3f9a0c3de7a38997f4f3a6193d803af
5
5
  SHA512:
6
- metadata.gz: 4ccb7a673c9c5a7d6508e49c6b4b92b50b08faa9395a6e6c7da3bef460a6b72b64900d2ce7e529c5f18224c5b96e23bf82e8142f5d6dc047c327216b117bff21
7
- data.tar.gz: f29bcdf4a6142b99d1cabf53086b682925256afc433e106a52feca3e96f097203c5e27ed7419ea51fc9b4ee6a2c454b06b0cdbb56a1d3f890fa6f452b529be4f
6
+ metadata.gz: 80210f9dd1636fac2f2dd9ab15c0090990312fdcd1c34f2c0aa6157c9c4e27b1aed9a03c064e91b148014388eaec3ed4171dd57d4c059e4f2e9ee919610d098a
7
+ data.tar.gz: f23b85dbb613af03faa0637fd5e9dd626e8e975b87590a0b87aedeba296bcd24c0f79c16dc20059ddb8f41b892887098d0f7dc58d1b951eaae0094b5dd15ea2a
data/README.md CHANGED
@@ -204,6 +204,7 @@ en:
204
204
  aspect_ratio_not_landscape: "must be a landscape image"
205
205
  aspect_ratio_is_not: "must have an aspect ratio of %{aspect_ratio}"
206
206
  image_not_processable: "is not a valid image"
207
+ aspect_ratio_invalid: "has invalid aspect ratio"
207
208
  ```
208
209
 
209
210
  In several cases, Active Storage Validations provides variables to help you customize messages:
@@ -437,7 +438,7 @@ Then you can use the matchers with the syntax specified in the RSpec section, ju
437
438
 
438
439
  To run tests in root folder of gem:
439
440
 
440
- * `BUNDLE_GEMFILE=gemfiles/rails_6_1_4.gemfile bundle exec rake test` to run for Rails 7.0
441
+ * `BUNDLE_GEMFILE=gemfiles/rails_6_1_4.gemfile bundle exec rake test` to run for Rails 6.1
441
442
  * `BUNDLE_GEMFILE=gemfiles/rails_7_0.gemfile bundle exec rake test` to run for Rails 7.0
442
443
  * `BUNDLE_GEMFILE=gemfiles/rails_7_1.gemfile bundle exec rake test` to run for Rails 7.1
443
444
  * `BUNDLE_GEMFILE=gemfiles/rails_7_2.gemfile bundle exec rake test` to run for Rails 7.2
@@ -471,71 +472,8 @@ You are welcome to contribute.
471
472
  />](https://opensource-heroes.com/r/igorkasyanchuk/active_storage_validations)
472
473
 
473
474
  ## Contributors (BIG THANK YOU)
474
- - https://github.com/schweigert
475
- - https://github.com/tleneveu
476
- - https://github.com/reckerswartz
477
- - https://github.com/Uysim
478
- - https://github.com/D-system
479
- - https://github.com/ivanelrey
480
- - https://github.com/phlegx
481
- - https://github.com/rr-dev
482
- - https://github.com/dsmalko
483
- - https://github.com/danderozier
484
- - https://github.com/cseelus
485
- - https://github.com/vkinelev
486
- - https://github.com/reed
487
- - https://github.com/connorshea
488
- - https://github.com/Atul9
489
- - https://github.com/victorbueno
490
- - https://github.com/UICJohn
491
- - https://github.com/giovannibonetti
492
- - https://github.com/dlepage
493
- - https://github.com/StefSchenkelaars
494
- - https://github.com/willnet
495
- - https://github.com/mohanklein
496
- - https://github.com/High5Apps
497
- - https://github.com/mschnitzer
498
- - https://github.com/sinankeskin
499
- - https://github.com/alejandrodevs
500
- - https://github.com/molfar
501
- - https://github.com/connorshea
502
- - https://github.com/yshmarov
503
- - https://github.com/fongfan999
504
- - https://github.com/cooperka
505
- - https://github.com/dolarsrg
506
- - https://github.com/jayshepherd
507
- - https://github.com/ohbarye
508
- - https://github.com/randsina
509
- - https://github.com/vietqhoang
510
- - https://github.com/kemenaran
511
- - https://github.com/jrmhaig
512
- - https://github.com/evedovelli
513
- - https://github.com/JuanVqz
514
- - https://github.com/luiseugenio
515
- - https://github.com/equivalent
516
- - https://github.com/NARKOZ
517
- - https://github.com/stephensolis
518
- - https://github.com/kwent
519
- - https://github.com/Animesh-Ghosh
520
- - https://github.com/gr8bit
521
- - https://github.com/codegeek319
522
- - https://github.com/clwy-cn
523
- - https://github.com/kukicola
524
- - https://github.com/sobrinho
525
- - https://github.com/iainbeeston
526
- - https://github.com/marckohlbrugge
527
- - https://github.com/Mth0158
528
- - https://github.com/technicalpickles
529
- - https://github.com/ricsdeol
530
- - https://github.com/Fonsan
531
- - https://github.com/tagliala
532
- - https://github.com/ocarreterom
533
- - https://github.com/aditya-cherukuri
534
- - https://github.com/searls
535
- - https://github.com/yenshirak
536
- - https://github.com/wataori
537
- - https://github.com/Scorpahr
538
475
 
476
+ https://github.com/igorkasyanchuk/active_storage_validations/graphs/contributors
539
477
 
540
478
  ## License
541
479
 
@@ -30,3 +30,4 @@ da:
30
30
  aspect_ratio_not_landscape: "skal være et landskabsbillede"
31
31
  aspect_ratio_is_not: "skal have et størrelsesforhold på %{aspect_ratio}"
32
32
  image_not_processable: "er ikke et gyldigt billede"
33
+ aspect_ratio_invalid: "har et ugyldigt billedformat"
@@ -30,3 +30,4 @@ de:
30
30
  aspect_ratio_not_landscape: "muss Querformat sein"
31
31
  aspect_ratio_is_not: "muss ein Bildseitenverhältnis von %{aspect_ratio} haben"
32
32
  image_not_processable: "ist kein gültiges Bild"
33
+ aspect_ratio_invalid: "hat ein ungültiges Seitenverhältnis"
@@ -30,3 +30,4 @@ en:
30
30
  aspect_ratio_not_landscape: "must be a landscape image"
31
31
  aspect_ratio_is_not: "must have an aspect ratio of %{aspect_ratio}"
32
32
  image_not_processable: "is not a valid image"
33
+ aspect_ratio_invalid: "has an invalid aspect ratio"
@@ -30,3 +30,4 @@ es:
30
30
  aspect_ratio_not_landscape: "debe ser una imagen apaisada"
31
31
  aspect_ratio_is_not: "debe tener una relación de aspecto de %{aspect_ratio}"
32
32
  image_not_processable: "no es una imagen válida"
33
+ aspect_ratio_invalid: "tiene una relación de aspecto no válida"
@@ -30,3 +30,4 @@ fr:
30
30
  aspect_ratio_not_landscape: "doit être une image en format paysage"
31
31
  aspect_ratio_is_not: "doit avoir un rapport hauteur / largeur de %{aspect_ratio}"
32
32
  image_not_processable: "n'est pas une image valide"
33
+ aspect_ratio_invalid: "a un rapport hauteur / largeur invalide"
@@ -30,3 +30,4 @@ it:
30
30
  aspect_ratio_not_landscape: "l’orientamento dell’immagine deve essere orizzontale"
31
31
  aspect_ratio_is_not: "deve avere un rapporto altezza / larghezza di %{aspect_ratio}"
32
32
  image_not_processable: "non è un'immagine valida"
33
+ aspect_ratio_invalid: "ha proporzioni non valide"
@@ -30,3 +30,4 @@ ja:
30
30
  aspect_ratio_not_landscape: "は横長にしてください"
31
31
  aspect_ratio_is_not: "のアスペクト比は %{aspect_ratio} にしてください"
32
32
  image_not_processable: "は不正な画像です"
33
+ aspect_ratio_invalid: "アスペクト比が無効です"
@@ -30,3 +30,4 @@ nl:
30
30
  aspect_ratio_not_landscape: "moet een liggende afbeelding zijn"
31
31
  aspect_ratio_is_not: "moet een beeldverhouding hebben van %{aspect_ratio}"
32
32
  image_not_processable: "is geen geldige afbeelding"
33
+ aspect_ratio_invalid: "heeft een ongeldige beeldverhouding"
@@ -30,3 +30,4 @@ pl:
30
30
  aspect_ratio_not_landscape: "musi mieć proporcje pejzażu"
31
31
  aspect_ratio_is_not: "musi mieć proporcje %{aspect_ratio}"
32
32
  image_not_processable: "nie jest prawidłowym obrazem"
33
+ aspect_ratio_invalid: "ma nieprawidłowy współczynnik proporcji"
@@ -30,3 +30,4 @@ pt-BR:
30
30
  aspect_ratio_not_landscape: "não está no formato paisagem"
31
31
  aspect_ratio_is_not: "não contém uma proporção de %{aspect_ratio}"
32
32
  image_not_processable: "não é uma imagem válida"
33
+ aspect_ratio_invalid: "tem uma proporção de aspecto inválida"
@@ -30,3 +30,4 @@ ru:
30
30
  aspect_ratio_not_landscape: "должно быть пейзажное изображение"
31
31
  aspect_ratio_is_not: "должен иметь соотношение сторон %{aspect_ratio}"
32
32
  image_not_processable: "не является допустимым изображением"
33
+ aspect_ratio_invalid: "имеет недопустимое соотношение сторон"
@@ -30,3 +30,4 @@ sv:
30
30
  aspect_ratio_not_landscape: "måste vara en landskapsorienterad bild"
31
31
  aspect_ratio_is_not: "måste ha en följande aspect ratio %{aspect_ratio}"
32
32
  image_not_processable: "är inte en giltig bild"
33
+ aspect_ratio_invalid: "har ett ogiltigt bildförhållande"
@@ -30,3 +30,4 @@ tr:
30
30
  aspect_ratio_not_landscape: "yatay bir imaj olmalı"
31
31
  aspect_ratio_is_not: "%{aspect_ratio} en boy oranına sahip olmalı"
32
32
  image_not_processable: "geçerli bir imaj değil"
33
+ aspect_ratio_invalid: "geçersiz bir en boy oranına sahip"
@@ -30,3 +30,4 @@ uk:
30
30
  aspect_ratio_not_landscape: "мусить бути пейзажне зображення"
31
31
  aspect_ratio_is_not: "мусить мати співвідношення сторін %{aspect_ratio}"
32
32
  image_not_processable: "не є допустимим зображенням"
33
+ aspect_ratio_invalid: "має недійсне співвідношення сторін"
@@ -30,3 +30,4 @@ vi:
30
30
  aspect_ratio_not_landscape: "phải là ảnh ngang"
31
31
  aspect_ratio_is_not: "phải có tỉ lệ ảnh %{aspect_ratio}"
32
32
  image_not_processable: "không phải là ảnh"
33
+ aspect_ratio_invalid: "có tỷ lệ khung hình không hợp lệ"
@@ -30,3 +30,4 @@ zh-CN:
30
30
  aspect_ratio_not_landscape: "必须是横屏图片"
31
31
  aspect_ratio_is_not: "纵横比必须是 %{aspect_ratio}"
32
32
  image_not_processable: "不是有效的图像"
33
+ aspect_ratio_invalid: "宽高比无效"
@@ -9,13 +9,6 @@ module ActiveStorageValidations
9
9
  private
10
10
 
11
11
  def read_image
12
- begin
13
- require "mini_magick" unless defined?(MiniMagick)
14
- rescue LoadError
15
- logger.info "Skipping image analysis because the mini_magick gem isn't installed"
16
- return {}
17
- end
18
-
19
12
  Tempfile.create(binmode: true) do |tempfile|
20
13
  begin
21
14
  if image(tempfile).valid?
@@ -42,5 +35,13 @@ module ActiveStorageValidations
42
35
  def rotated_image?(image)
43
36
  %w[ RightTop LeftBottom TopRight BottomLeft ].include?(image["%[orientation]"])
44
37
  end
38
+
39
+ def supported?
40
+ require "mini_magick"
41
+ true
42
+ rescue LoadError
43
+ logger.info "Skipping image analysis because the mini_magick gem isn't installed"
44
+ false
45
+ end
45
46
  end
46
47
  end
@@ -8,13 +8,6 @@ module ActiveStorageValidations
8
8
  private
9
9
 
10
10
  def read_image
11
- begin
12
- require "vips" unless defined?(::Vips)
13
- rescue LoadError
14
- logger.info "Skipping image analysis because the ruby-vips gem isn't installed"
15
- return {}
16
- end
17
-
18
11
  Tempfile.create(binmode: true) do |tempfile|
19
12
  begin
20
13
  if image(tempfile)
@@ -47,6 +40,14 @@ module ActiveStorageValidations
47
40
  end
48
41
  end
49
42
 
43
+ def supported?
44
+ require "vips"
45
+ true
46
+ rescue LoadError
47
+ logger.info "Skipping image analysis because the ruby-vips gem isn't installed"
48
+ false
49
+ end
50
+
50
51
  ROTATIONS = /Right-top|Left-bottom|Top-right|Bottom-left/
51
52
  def rotated_image?(image)
52
53
  ROTATIONS === image.get("exif-ifd0-Orientation")
@@ -12,7 +12,11 @@ module ActiveStorageValidations
12
12
  # ActiveStorage::Analyzer::ImageAnalyzer::ImageMagick.new(attachable).metadata
13
13
  # # => { width: 4104, height: 2736 }
14
14
  class Analyzer::ImageAnalyzer < Analyzer
15
+ @@supported_analyzers = {}
16
+
15
17
  def metadata
18
+ return {} unless analyzer_supported?
19
+
16
20
  read_image do |image|
17
21
  if rotated_image?(image)
18
22
  { width: image.height, height: image.width }
@@ -62,6 +66,14 @@ module ActiveStorageValidations
62
66
  image_from_path(tempfile.path)
63
67
  end
64
68
 
69
+ def analyzer_supported?
70
+ if @@supported_analyzers.key?(self)
71
+ @@supported_analyzers.fetch(self)
72
+ else
73
+ @@supported_analyzers[self] = supported?
74
+ end
75
+ end
76
+
65
77
  def read_image
66
78
  raise NotImplementedError
67
79
  end
@@ -73,5 +85,9 @@ module ActiveStorageValidations
73
85
  def rotated_image?(image)
74
86
  raise NotImplementedError
75
87
  end
88
+
89
+ def supported?
90
+ raise NotImplementedError
91
+ end
76
92
  end
77
93
  end
@@ -1,120 +1,150 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'shared/asv_active_storageable'
4
- require_relative 'shared/asv_analyzable'
5
- require_relative 'shared/asv_attachable'
6
- require_relative 'shared/asv_errorable'
7
- require_relative 'shared/asv_optionable'
8
- require_relative 'shared/asv_symbolizable'
9
-
10
- module ActiveStorageValidations
11
- class AspectRatioValidator < ActiveModel::EachValidator # :nodoc
12
- include ASVActiveStorageable
13
- include ASVAnalyzable
14
- include ASVAttachable
15
- include ASVErrorable
16
- include ASVOptionable
17
- include ASVSymbolizable
18
-
19
- AVAILABLE_CHECKS = %i[with].freeze
20
- NAMED_ASPECT_RATIOS = %i[square portrait landscape].freeze
21
- ASPECT_RATIO_REGEX = /is_([1-9]\d*)_([1-9]\d*)/.freeze
22
- ERROR_TYPES = %i[
23
- image_metadata_missing
24
- aspect_ratio_not_square
25
- aspect_ratio_not_portrait
26
- aspect_ratio_not_landscape
27
- aspect_ratio_is_not
28
- ].freeze
29
- PRECISION = 3.freeze
30
-
31
- def check_validity!
32
- ensure_at_least_one_validator_option
33
- ensure_aspect_ratio_validity
34
- end
35
-
36
- def validate_each(record, attribute, _value)
37
- return if no_attachments?(record, attribute)
38
-
39
- validate_changed_files_from_metadata(record, attribute)
40
- end
41
-
42
- private
43
-
44
- def is_valid?(record, attribute, attachable, metadata)
45
- flat_options = set_flat_options(record)
46
-
47
- return if image_metadata_missing?(record, attribute, attachable, flat_options, metadata)
48
-
49
- case flat_options[:with]
50
- when :square then validate_square_aspect_ratio(record, attribute, attachable, flat_options, metadata)
51
- when :portrait then validate_portrait_aspect_ratio(record, attribute, attachable, flat_options, metadata)
52
- when :landscape then validate_landscape_aspect_ratio(record, attribute, attachable, flat_options, metadata)
53
- when ASPECT_RATIO_REGEX then validate_regex_aspect_ratio(record, attribute, attachable, flat_options, metadata)
54
- end
55
- end
56
-
57
- def image_metadata_missing?(record, attribute, attachable, flat_options, metadata)
58
- return false if metadata.present? && metadata[:width].to_i > 0 && metadata[:height].to_i > 0
59
-
60
- errors_options = initialize_error_options(options, attachable)
61
- errors_options[:aspect_ratio] = flat_options[:with]
62
- add_error(record, attribute, :image_metadata_missing, **errors_options)
63
- true
64
- end
65
-
66
- def validate_square_aspect_ratio(record, attribute, attachable, flat_options, metadata)
67
- return if metadata[:width] == metadata[:height]
68
-
69
- errors_options = initialize_error_options(options, attachable)
70
- errors_options[:aspect_ratio] = flat_options[:with]
71
- add_error(record, attribute, :aspect_ratio_not_square, **errors_options)
72
- end
73
-
74
- def validate_portrait_aspect_ratio(record, attribute, attachable, flat_options, metadata)
75
- return if metadata[:width] < metadata[:height]
76
-
77
- errors_options = initialize_error_options(options, attachable)
78
- errors_options[:aspect_ratio] = flat_options[:with]
79
- add_error(record, attribute, :aspect_ratio_not_portrait, **errors_options)
80
- end
81
-
82
- def validate_landscape_aspect_ratio(record, attribute, attachable, flat_options, metadata)
83
- return if metadata[:width] > metadata[:height]
84
-
85
- errors_options = initialize_error_options(options, attachable)
86
- errors_options[:aspect_ratio] = flat_options[:with]
87
- add_error(record, attribute, :aspect_ratio_not_landscape, **errors_options)
88
- end
89
-
90
- def validate_regex_aspect_ratio(record, attribute, attachable, flat_options, metadata)
91
- flat_options[:with] =~ ASPECT_RATIO_REGEX
92
- x = $1.to_i
93
- y = $2.to_i
94
-
95
- return if x > 0 && y > 0 && (x.to_f / y).round(PRECISION) == (metadata[:width].to_f / metadata[:height]).round(PRECISION)
96
-
97
- errors_options = initialize_error_options(options, attachable)
98
- errors_options[:aspect_ratio] = "#{x}:#{y}"
99
- add_error(record, attribute, :aspect_ratio_is_not, **errors_options)
100
- end
101
-
102
- def ensure_at_least_one_validator_option
103
- unless AVAILABLE_CHECKS.any? { |argument| options.key?(argument) }
104
- raise ArgumentError, 'You must pass :with to the validator'
105
- end
106
- end
107
-
108
- def ensure_aspect_ratio_validity
109
- return true if options[:with]&.is_a?(Proc)
110
-
111
- unless NAMED_ASPECT_RATIOS.include?(options[:with]) || options[:with] =~ ASPECT_RATIO_REGEX
112
- raise ArgumentError, <<~ERROR_MESSAGE
113
- You must pass a valid aspect ratio to the validator
114
- It should either be a named aspect ratio (#{NAMED_ASPECT_RATIOS.join(', ')})
115
- Or an aspect ratio like 'is_16_9' (matching /#{ASPECT_RATIO_REGEX.source}/)
116
- ERROR_MESSAGE
117
- end
118
- end
119
- end
120
- end
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'shared/asv_active_storageable'
4
+ require_relative 'shared/asv_analyzable'
5
+ require_relative 'shared/asv_attachable'
6
+ require_relative 'shared/asv_errorable'
7
+ require_relative 'shared/asv_optionable'
8
+ require_relative 'shared/asv_symbolizable'
9
+
10
+ module ActiveStorageValidations
11
+ class AspectRatioValidator < ActiveModel::EachValidator # :nodoc
12
+ include ASVActiveStorageable
13
+ include ASVAnalyzable
14
+ include ASVAttachable
15
+ include ASVErrorable
16
+ include ASVOptionable
17
+ include ASVSymbolizable
18
+
19
+ AVAILABLE_CHECKS = %i[with in].freeze
20
+ NAMED_ASPECT_RATIOS = %i[square portrait landscape].freeze
21
+ ASPECT_RATIO_REGEX = /is_([1-9]\d*)_([1-9]\d*)/.freeze
22
+ ERROR_TYPES = %i[
23
+ aspect_ratio_not_square
24
+ aspect_ratio_not_portrait
25
+ aspect_ratio_not_landscape
26
+ aspect_ratio_is_not
27
+ aspect_ratio_invalid
28
+ image_metadata_missing
29
+ ].freeze
30
+ PRECISION = 3.freeze
31
+
32
+ def check_validity!
33
+ ensure_at_least_one_validator_option
34
+ ensure_aspect_ratio_validity
35
+ end
36
+
37
+ def validate_each(record, attribute, _value)
38
+ return if no_attachments?(record, attribute)
39
+
40
+ flat_options = set_flat_options(record)
41
+ @authorized_aspect_ratios = authorized_aspect_ratios_from_options(flat_options).compact
42
+ return if @authorized_aspect_ratios.empty?
43
+
44
+ validate_changed_files_from_metadata(record, attribute)
45
+ end
46
+
47
+ private
48
+
49
+ def is_valid?(record, attribute, attachable, metadata)
50
+ !image_metadata_missing?(record, attribute, attachable, metadata) &&
51
+ authorized_aspect_ratio?(record, attribute, attachable, metadata)
52
+ end
53
+
54
+ def authorized_aspect_ratio?(record, attribute, attachable, metadata)
55
+ attachable_aspect_ratio_is_authorized = @authorized_aspect_ratios.any? do |authorized_aspect_ratio|
56
+ case authorized_aspect_ratio
57
+ when :square then valid_square_aspect_ratio?(metadata)
58
+ when :portrait then valid_portrait_aspect_ratio?(metadata)
59
+ when :landscape then valid_landscape_aspect_ratio?(metadata)
60
+ when ASPECT_RATIO_REGEX then valid_regex_aspect_ratio?(authorized_aspect_ratio, metadata)
61
+ end
62
+ end
63
+
64
+ return true if attachable_aspect_ratio_is_authorized
65
+
66
+ errors_options = initialize_error_options(options, attachable)
67
+ errors_options[:aspect_ratio] = string_aspect_ratios
68
+ add_error(record, attribute, aspect_ratio_error_mapping, **errors_options)
69
+ false
70
+ end
71
+
72
+ def aspect_ratio_error_mapping
73
+ return :aspect_ratio_invalid unless @authorized_aspect_ratios.one?
74
+
75
+ aspect_ratio = @authorized_aspect_ratios.first
76
+ NAMED_ASPECT_RATIOS.include?(aspect_ratio) ? :"aspect_ratio_not_#{aspect_ratio}" : :aspect_ratio_is_not
77
+ end
78
+
79
+ def image_metadata_missing?(record, attribute, attachable, metadata)
80
+ return false if metadata[:width].to_i > 0 && metadata[:height].to_i > 0
81
+
82
+ errors_options = initialize_error_options(options, attachable)
83
+ errors_options[:aspect_ratio] = string_aspect_ratios
84
+ add_error(record, attribute, :image_metadata_missing, **errors_options)
85
+ true
86
+ end
87
+
88
+ def valid_square_aspect_ratio?(metadata)
89
+ metadata[:width] == metadata[:height]
90
+ end
91
+
92
+ def valid_portrait_aspect_ratio?(metadata)
93
+ metadata[:width] < metadata[:height]
94
+ end
95
+
96
+ def valid_landscape_aspect_ratio?(metadata)
97
+ metadata[:width] > metadata[:height]
98
+ end
99
+
100
+ def valid_regex_aspect_ratio?(aspect_ratio, metadata)
101
+ aspect_ratio =~ ASPECT_RATIO_REGEX
102
+ x = ::Regexp.last_match(1).to_i
103
+ y = ::Regexp.last_match(2).to_i
104
+
105
+ x > 0 && y > 0 && (x.to_f / y).round(PRECISION) == (metadata[:width].to_f / metadata[:height]).round(PRECISION)
106
+ end
107
+
108
+ def ensure_at_least_one_validator_option
109
+ return if AVAILABLE_CHECKS.any? { |argument| options.key?(argument) }
110
+
111
+ raise ArgumentError, 'You must pass either :with or :in to the validator'
112
+ end
113
+
114
+ def ensure_aspect_ratio_validity
115
+ return true if options[:with]&.is_a?(Proc) || options[:in]&.is_a?(Proc)
116
+
117
+ authorized_aspect_ratios_from_options(options).each do |aspect_ratio|
118
+ unless NAMED_ASPECT_RATIOS.include?(aspect_ratio) || aspect_ratio =~ ASPECT_RATIO_REGEX
119
+ raise ArgumentError, invalid_aspect_ratio_message
120
+ end
121
+ end
122
+ end
123
+
124
+ def invalid_aspect_ratio_message
125
+ <<~ERROR_MESSAGE
126
+ You must pass a valid aspect ratio to the validator
127
+ It should either be a named aspect ratio (#{NAMED_ASPECT_RATIOS.join(', ')})
128
+ Or an aspect ratio like 'is_16_9' (matching /#{ASPECT_RATIO_REGEX.source}/)
129
+ ERROR_MESSAGE
130
+ end
131
+
132
+ def authorized_aspect_ratios_from_options(flat_options)
133
+ (Array.wrap(flat_options[:with]) + Array.wrap(flat_options[:in]))
134
+ end
135
+
136
+ def string_aspect_ratios
137
+ @authorized_aspect_ratios.map do |aspect_ratio|
138
+ if NAMED_ASPECT_RATIOS.include?(aspect_ratio)
139
+ aspect_ratio
140
+ else
141
+ aspect_ratio =~ ASPECT_RATIO_REGEX
142
+ x = ::Regexp.last_match(1).to_i
143
+ y = ::Regexp.last_match(2).to_i
144
+
145
+ "#{x}:#{y}"
146
+ end
147
+ end.join(', ')
148
+ end
149
+ end
150
+ end
@@ -51,7 +51,7 @@ module ActiveStorageValidations
51
51
  end
52
52
 
53
53
  def open3_mime_type_for_io
54
- return nil if io.blank?
54
+ return nil if io.bytesize == 0
55
55
 
56
56
  Tempfile.create do |tempfile|
57
57
  tempfile.binmode
@@ -34,9 +34,11 @@ module ActiveStorageValidations
34
34
  @authorized_content_types = authorized_content_types_from_options(record)
35
35
  return if @authorized_content_types.empty?
36
36
 
37
- attachables_from_changes(record, attribute).each do |attachable|
38
- set_attachable_cached_values(attachable)
39
- is_valid?(record, attribute, attachable)
37
+ checked_files = disable_spoofing_protection? ? attached_files(record, attribute) : attachables_from_changes(record, attribute)
38
+
39
+ checked_files.each do |file|
40
+ set_attachable_cached_values(file)
41
+ is_valid?(record, attribute, file)
40
42
  end
41
43
  end
42
44
 
@@ -54,8 +56,8 @@ module ActiveStorageValidations
54
56
  end
55
57
 
56
58
  def set_attachable_cached_values(attachable)
57
- @attachable_content_type = attachable_content_type_rails_like(attachable)
58
- @attachable_filename = attachable_filename(attachable).to_s
59
+ @attachable_content_type = disable_spoofing_protection? ? attachable.blob.content_type : attachable_content_type_rails_like(attachable)
60
+ @attachable_filename = disable_spoofing_protection? ? attachable.blob.filename.to_s : attachable_filename(attachable).to_s
59
61
  end
60
62
 
61
63
  # Check if the provided content_type is authorized and not spoofed against
@@ -116,6 +118,10 @@ module ActiveStorageValidations
116
118
  Marcel::MimeType.for(declared_type: @attachable_content_type, name: @attachable_filename)
117
119
  end
118
120
 
121
+ def disable_spoofing_protection?
122
+ !enable_spoofing_protection?
123
+ end
124
+
119
125
  def enable_spoofing_protection?
120
126
  options[:spoofing_protection] == true
121
127
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "marcel"
4
+
3
5
  Marcel::MimeType.extend "application/x-rar-compressed", parents: %(application/x-rar)
4
6
  Marcel::MimeType.extend "audio/x-hx-aac-adts", parents: %(audio/x-aac)
5
7
  Marcel::MimeType.extend "audio/x-m4a", parents: %(audio/mp4)
@@ -9,8 +9,8 @@ module ActiveStorageValidations
9
9
 
10
10
  private
11
11
 
12
- # Retrieve either an ActiveStorage::Attached::One or an
13
- # ActiveStorage::Attached::Many instance depending on attribute definition
12
+ # Retrieve either an `ActiveStorage::Attached::One` or an
13
+ # `ActiveStorage::Attached::Many` instance depending on attribute definition
14
14
  def attached_files(record, attribute)
15
15
  Array.wrap(record.send(attribute))
16
16
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveStorageValidations
4
- VERSION = '1.3.5'
4
+ VERSION = '1.4.0'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_storage_validations
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.5
4
+ version: 1.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Igor Kasyanchuk
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-12-11 00:00:00.000000000 Z
11
+ date: 2024-12-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activejob