active_storage_validations 1.3.5 → 1.4.0

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