active_storage_validations 0.9.5 → 1.0.3

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: efa69aeeea1b171286c6ca464e38146dedef79d22853c35ec6ea0ce0ca089812
4
- data.tar.gz: e186561942afb6b295839565323d0bbed704de9e413b79ecc1fb194ada643ca5
3
+ metadata.gz: 267c083e1e37ff53853d928bf3df0a69b8c9cf0662c3cf744f99cde3d5809125
4
+ data.tar.gz: 36554892e5c2df7ad812119b6acfae985e4adbe1dd8f7c29699f8f9ba69f0506
5
5
  SHA512:
6
- metadata.gz: c31ec657e1121427f19d7617d5efbeab639c6ebd87c0333c36510064e7b4d8125ffd8e4f5afa86121e626dd608d2ad1b859a3515c8bc7683e0793291a30c7fd2
7
- data.tar.gz: 3141abc08b6b33d90f9874a74e93f6aaddb0ebbd52bdd36ee292a1e0ff7d37cf65a8ced082d316f48cc8e0132320c4f83304d9823971906526096264129c0145
6
+ metadata.gz: 6cbe5d7521afc8680a9df584968c4e3da7c5e1602efc30f60dc6890a54bb9d609daf7e7eaa0924d6d7971f13e9c763e4063c228d14645b8c5261f1702c4a21c9
7
+ data.tar.gz: 989e058c095e07790f647af2582355dddd715101b6dce10d4b521865362c5857f8df869270ae88e5eb58d20fa698ed470f4b181517ac016439d45f0e30049e21
data/README.md CHANGED
@@ -1,3 +1,6 @@
1
+ [<img src="https://github.com/igorkasyanchuk/rails_time_travel/blob/main/docs/more_gems.png?raw=true"
2
+ />](https://www.railsjazz.com/?utm_source=github&utm_medium=top&utm_campaign=active_storage_validations)
3
+
1
4
  # Active Storage Validations
2
5
 
3
6
  [![MiniTest](https://github.com/igorkasyanchuk/active_storage_validations/workflows/MiniTest/badge.svg)](https://github.com/igorkasyanchuk/active_storage_validations/actions)
@@ -16,7 +19,9 @@ This gems doing it for you. Just use `attached: true` or `content_type: 'image/p
16
19
  * validates dimension of images/videos
17
20
  * validates number of uploaded files (min/max required)
18
21
  * validates aspect ratio (if square, portrait, landscape, is_16_9, ...)
22
+ * validates if file can be processed by MiniMagick or Vips
19
23
  * custom error messages
24
+ * allow procs for dynamic determination of values
20
25
 
21
26
  ## Usage
22
27
 
@@ -26,16 +31,18 @@ For example you have a model like this and you want to add validation.
26
31
  class User < ApplicationRecord
27
32
  has_one_attached :avatar
28
33
  has_many_attached :photos
34
+ has_one_attached :image
29
35
 
30
36
  validates :name, presence: true
31
37
 
32
38
  validates :avatar, attached: true, content_type: 'image/png',
33
39
  dimension: { width: 200, height: 200 }
34
- validates :photos, attached: true, content_type: ['image/png', 'image/jpg', 'image/jpeg'],
40
+ validates :photos, attached: true, content_type: ['image/png', 'image/jpeg'],
35
41
  dimension: { width: { min: 800, max: 2400 },
36
42
  height: { min: 600, max: 1800 }, message: 'is not given between dimension' }
37
43
  validates :image, attached: true,
38
- content_type: ['image/png', 'image/jpg'],
44
+ processable_image: true,
45
+ content_type: ['image/png', 'image/jpeg'],
39
46
  aspect_ratio: :landscape
40
47
  end
41
48
  ```
@@ -44,13 +51,15 @@ or
44
51
 
45
52
  ```ruby
46
53
  class Project < ApplicationRecord
54
+ has_one_attached :logo
47
55
  has_one_attached :preview
48
56
  has_one_attached :attachment
49
57
  has_many_attached :documents
50
58
 
51
59
  validates :title, presence: true
52
60
 
53
- validates :preview, attached: true, size: { less_than: 100.megabytes , message: 'is not given between size' }
61
+ validates :logo, attached: true, size: { less_than: 100.megabytes , message: 'is too large' }
62
+ validates :preview, attached: true, size: { between: 1.kilobyte..100.megabytes , message: 'is not given between size' }
54
63
  validates :attachment, attached: true, content_type: { in: 'application/pdf', message: 'is not a PDF' }
55
64
  validates :documents, limit: { min: 1, max: 3 }
56
65
  end
@@ -58,14 +67,15 @@ end
58
67
 
59
68
  ### More examples
60
69
 
61
- - Content type validation using symbols. In order to infer the correct mime type from the symbol, the types must be registered with `MimeMagic::EXTENSIONS`.
70
+ - Content type validation using symbols. In order to infer the correct mime type from the symbol, the types must be registered with `Marcel::EXTENSIONS` (`MimeMagic::EXTENSIONS` for Rails <= 6.1.3).
62
71
 
63
72
  ```ruby
64
73
  class User < ApplicationRecord
65
74
  has_one_attached :avatar
66
75
  has_many_attached :photos
67
76
 
68
- validates :avatar, attached: true, content_type: :png # MimeMagic.by_extension(:png).to_s => 'image/png'
77
+ validates :avatar, attached: true, content_type: :png # Marcel::Magic.by_extension(:png).to_s => 'image/png'
78
+ # Rails <= 6.1.3; MimeMagic.by_extension(:png).to_s => 'image/png'
69
79
  # or
70
80
  validates :photos, attached: true, content_type: [:png, :jpg, :jpeg]
71
81
  # or
@@ -117,6 +127,18 @@ class User < ApplicationRecord
117
127
  end
118
128
  ```
119
129
 
130
+ - Proc Usage:
131
+
132
+ Procs can be used instead of values in all the above examples. They will be called on every validation.
133
+ ```ruby
134
+ class User < ApplicationRecord
135
+ has_many_attached :proc_files
136
+
137
+ validates :proc_files, limit: { max: -> (record) { record.admin? ? 100 : 10 } }
138
+ end
139
+
140
+ ```
141
+
120
142
  ## Internationalization (I18n)
121
143
 
122
144
  Active Storage Validations uses I18n for error messages. For this, add these keys in your translation file:
@@ -144,6 +166,7 @@ en:
144
166
  aspect_ratio_not_landscape: "must be a landscape image"
145
167
  aspect_ratio_is_not: "must have an aspect ratio of %{aspect_ratio}"
146
168
  aspect_ratio_unknown: "has an unknown aspect ratio"
169
+ image_not_processable: "is not a valid image"
147
170
  ```
148
171
 
149
172
  In some cases, Active Storage Validations provides variables to help you customize messages:
@@ -176,6 +199,8 @@ gem 'active_storage_validations'
176
199
 
177
200
  # Optional, to use :dimension validator or :aspect_ratio validator
178
201
  gem 'mini_magick', '>= 4.9.5'
202
+ # Or
203
+ gem 'ruby-vips', '>= 2.1.0'
179
204
  ```
180
205
 
181
206
  And then execute:
@@ -289,9 +314,23 @@ end
289
314
 
290
315
  To run tests in root folder of gem:
291
316
 
292
- * `BUNDLE_GEMFILE=gemfiles/rails_5_2.gemfile bundle exec rake test` to run for Rails 5.2
293
317
  * `BUNDLE_GEMFILE=gemfiles/rails_6_0.gemfile bundle exec rake test` to run for Rails 6.0
294
318
  * `BUNDLE_GEMFILE=gemfiles/rails_6_1.gemfile bundle exec rake test` to run for Rails 6.1
319
+ * `BUNDLE_GEMFILE=gemfiles/rails_7_0.gemfile bundle exec rake test` to run for Rails 7.0
320
+ * `BUNDLE_GEMFILE=gemfiles/rails_next.gemfile bundle exec rake test` to run for Rails main branch
321
+
322
+ Snippet to run in console:
323
+
324
+ ```
325
+ BUNDLE_GEMFILE=gemfiles/rails_6_0.gemfile bundle
326
+ BUNDLE_GEMFILE=gemfiles/rails_6_1.gemfile bundle
327
+ BUNDLE_GEMFILE=gemfiles/rails_7_0.gemfile bundle
328
+ BUNDLE_GEMFILE=gemfiles/rails_next.gemfile bundle
329
+ BUNDLE_GEMFILE=gemfiles/rails_6_0.gemfile bundle exec rake test
330
+ BUNDLE_GEMFILE=gemfiles/rails_6_1.gemfile bundle exec rake test
331
+ BUNDLE_GEMFILE=gemfiles/rails_7_0.gemfile bundle exec rake test
332
+ BUNDLE_GEMFILE=gemfiles/rails_next.gemfile bundle exec rake test
333
+ ```
295
334
 
296
335
  ## Known issues
297
336
 
@@ -344,11 +383,27 @@ You are welcome to contribute.
344
383
  - https://github.com/jayshepherd
345
384
  - https://github.com/ohbarye
346
385
  - https://github.com/randsina
347
- - https://github.com/...
386
+ - https://github.com/vietqhoang
387
+ - https://github.com/kemenaran
388
+ - https://github.com/jrmhaig
389
+ - https://github.com/tagliala
390
+ - https://github.com/evedovelli
391
+ - https://github.com/JuanVqz
392
+ - https://github.com/luiseugenio
393
+ - https://github.com/equivalent
394
+ - https://github.com/NARKOZ
395
+ - https://github.com/stephensolis
396
+ - https://github.com/kwent
397
+ = https://github.com/Animesh-Ghosh
398
+ = https://github.com/gr8bit
399
+ = https://github.com/codegeek319
400
+ = https://github.com/clwy-cn
401
+ = https://github.com/kukicola
402
+ = https://github.com/sobrinho
348
403
 
349
404
  ## License
350
405
 
351
406
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
352
407
 
353
408
  [<img src="https://github.com/igorkasyanchuk/rails_time_travel/blob/main/docs/more_gems.png?raw=true"
354
- />](https://www.railsjazz.com/)
409
+ />](https://www.railsjazz.com/?utm_source=github&utm_medium=bottom&utm_campaign=active_storage_validations)
@@ -20,3 +20,4 @@ en:
20
20
  aspect_ratio_not_landscape: "must be a landscape image"
21
21
  aspect_ratio_is_not: "must have an aspect ratio of %{aspect_ratio}"
22
22
  aspect_ratio_unknown: "has an unknown aspect ratio"
23
+ image_not_processable: "is not a valid image"
@@ -2,21 +2,21 @@ pt-BR:
2
2
  errors:
3
3
  messages:
4
4
  content_type_invalid: "tem um tipo de arquivo inválido"
5
- file_size_out_of_range: "tamanho %{file_size} está fora da faixa de tamanho válida"
6
- limit_out_of_range: "número total está fora do limite"
5
+ file_size_out_of_range: "tem tamanho %{file_size} e está fora da faixa de tamanho válida"
6
+ limit_out_of_range: "o número total está fora do limite"
7
7
  image_metadata_missing: "não é uma imagem válida"
8
- dimension_min_inclusion: "deve ser maior ou igual a %{width} x %{height} pixel"
9
- dimension_max_inclusion: "deve ser menor ou igual a %{width} x %{height} pixel"
10
- dimension_width_inclusion: "largura não está entre %{min} e %{max} pixel"
11
- dimension_height_inclusion: "altura não está entre %{min} e %{max} pixel"
12
- dimension_width_greater_than_or_equal_to: "largura deve ser maior ou igual a %{length} pixel"
13
- dimension_height_greater_than_or_equal_to: "altura deve ser maior ou igual a %{length} pixel"
14
- dimension_width_less_than_or_equal_to: "largura deve ser menor ou igual a %{length} pixel"
15
- dimension_height_less_than_or_equal_to: "altura deve ser menor ou igual a %{length} pixel"
16
- dimension_width_equal_to: "largura deve ser igual a %{length} pixel"
17
- dimension_height_equal_to: "altura deve ser igual a %{length} pixel"
8
+ dimension_min_inclusion: "deve ser maior ou igual a %{width} x %{height} pixels"
9
+ dimension_max_inclusion: "deve ser menor ou igual a %{width} x %{height} pixels"
10
+ dimension_width_inclusion: "deve ter largura entre %{min} e %{max} pixels"
11
+ dimension_height_inclusion: "deve ter altura entre %{min} e %{max} pixels"
12
+ dimension_width_greater_than_or_equal_to: "deve ter largura maior ou igual a %{length} pixels"
13
+ dimension_height_greater_than_or_equal_to: "deve ter altura maior ou igual a %{length} pixels"
14
+ dimension_width_less_than_or_equal_to: "deve ter largura menor ou igual a %{length} pixels"
15
+ dimension_height_less_than_or_equal_to: "deve ter altura menor ou igual a %{length} pixels"
16
+ dimension_width_equal_to: "deve ter largura igual a %{length} pixels"
17
+ dimension_height_equal_to: "deve ter altura igual a %{length} pixels"
18
18
  aspect_ratio_not_square: "não é uma imagem quadrada"
19
- aspect_ratio_not_portrait: "não contém uma imagem no formato retrato"
20
- aspect_ratio_not_landscape: "não contém uma imagem no formato paisagem"
19
+ aspect_ratio_not_portrait: "não está no formato retrato"
20
+ aspect_ratio_not_landscape: "não está no formato paisagem"
21
21
  aspect_ratio_is_not: "não contém uma proporção de %{aspect_ratio}"
22
22
  aspect_ratio_unknown: "não tem uma proporção definida"
@@ -0,0 +1,22 @@
1
+ zh-CN:
2
+ errors:
3
+ messages:
4
+ content_type_invalid: "文件类型错误"
5
+ file_size_out_of_range: "文件大小 %{file_size} 超出限定范围"
6
+ limit_out_of_range: "文件数超出限定范围"
7
+ image_metadata_missing: "不是有效的图像"
8
+ dimension_min_inclusion: "必须大于或等于 %{width} x %{height} 像素"
9
+ dimension_max_inclusion: "必须小于或等于 %{width} x %{height} 像素"
10
+ dimension_width_inclusion: "宽度不在 %{min} 和 %{max} 像素之间"
11
+ dimension_height_inclusion: "高度不在 %{min} 和 %{max} 像素之间"
12
+ dimension_width_greater_than_or_equal_to: "宽度必须大于或等于 %{length} 像素"
13
+ dimension_height_greater_than_or_equal_to: "高度必须大于或等于 %{length} 像素"
14
+ dimension_width_less_than_or_equal_to: "宽度必须小于或等于 %{length} 像素"
15
+ dimension_height_less_than_or_equal_to: "高度必须小于或等于 %{length} 像素"
16
+ dimension_width_equal_to: "宽度必须等于 %{length} 像素"
17
+ dimension_height_equal_to: "高度必须等于 %{length} 像素"
18
+ aspect_ratio_not_square: "必须是方形图片"
19
+ aspect_ratio_not_portrait: "必须是竖屏图片"
20
+ aspect_ratio_not_landscape: "必须是横屏图片"
21
+ aspect_ratio_is_not: "纵横比必须是 %{aspect_ratio}"
22
+ aspect_ratio_unknown: "未知的纵横比"
@@ -4,21 +4,17 @@ require_relative 'metadata.rb'
4
4
 
5
5
  module ActiveStorageValidations
6
6
  class AspectRatioValidator < ActiveModel::EachValidator # :nodoc
7
+ include OptionProcUnfolding
8
+
7
9
  AVAILABLE_CHECKS = %i[with].freeze
8
10
  PRECISION = 3
9
11
 
10
- def initialize(options)
11
- require 'mini_magick' unless defined?(MiniMagick)
12
- super(options)
13
- end
14
-
15
-
16
12
  def check_validity!
17
13
  return true if AVAILABLE_CHECKS.any? { |argument| options.key?(argument) }
18
- raise ArgumentError, 'You must pass "aspect_ratio: :OPTION" option to the validator'
14
+ raise ArgumentError, 'You must pass :with to the validator'
19
15
  end
20
16
 
21
- if Rails::VERSION::MAJOR >= 6
17
+ if Rails.gem_version >= Gem::Version.new('6.0.0')
22
18
  def validate_each(record, attribute, _value)
23
19
  return true unless record.send(attribute).attached?
24
20
 
@@ -37,14 +33,14 @@ module ActiveStorageValidations
37
33
  # Rails 5
38
34
  def validate_each(record, attribute, _value)
39
35
  return true unless record.send(attribute).attached?
40
-
36
+
41
37
  files = Array.wrap(record.send(attribute))
42
-
38
+
43
39
  files.each do |file|
44
40
  # Analyze file first if not analyzed to get all required metadata.
45
41
  file.analyze; file.reload unless file.analyzed?
46
42
  metadata = file.metadata
47
-
43
+
48
44
  next if is_valid?(record, attribute, metadata)
49
45
  break
50
46
  end
@@ -56,26 +52,27 @@ module ActiveStorageValidations
56
52
 
57
53
 
58
54
  def is_valid?(record, attribute, metadata)
55
+ flat_options = unfold_procs(record, self.options, AVAILABLE_CHECKS)
59
56
  if metadata[:width].to_i <= 0 || metadata[:height].to_i <= 0
60
- add_error(record, attribute, options[:message].presence || :image_metadata_missing)
57
+ add_error(record, attribute, :image_metadata_missing, flat_options[:with])
61
58
  return false
62
59
  end
63
60
 
64
- case options[:with]
61
+ case flat_options[:with]
65
62
  when :square
66
63
  return true if metadata[:width] == metadata[:height]
67
- add_error(record, attribute, :aspect_ratio_not_square)
64
+ add_error(record, attribute, :aspect_ratio_not_square, flat_options[:with])
68
65
 
69
66
  when :portrait
70
67
  return true if metadata[:height] > metadata[:width]
71
- add_error(record, attribute, :aspect_ratio_not_portrait)
68
+ add_error(record, attribute, :aspect_ratio_not_portrait, flat_options[:with])
72
69
 
73
70
  when :landscape
74
71
  return true if metadata[:width] > metadata[:height]
75
- add_error(record, attribute, :aspect_ratio_not_landscape)
72
+ add_error(record, attribute, :aspect_ratio_not_landscape, flat_options[:with])
76
73
 
77
74
  else
78
- if options[:with] =~ /is\_(\d*)\_(\d*)/
75
+ if flat_options[:with] =~ /is_(\d*)_(\d*)/
79
76
  x = $1.to_i
80
77
  y = $2.to_i
81
78
 
@@ -83,17 +80,17 @@ module ActiveStorageValidations
83
80
 
84
81
  add_error(record, attribute, :aspect_ratio_is_not, "#{x}x#{y}")
85
82
  else
86
- add_error(record, attribute, :aspect_ratio_unknown)
83
+ add_error(record, attribute, :aspect_ratio_unknown, flat_options[:with])
87
84
  end
88
85
  end
89
86
  false
90
87
  end
91
88
 
92
89
 
93
- def add_error(record, attribute, type, interpolate = options[:with])
94
- key = options[:message].presence || type
95
- return if record.errors.added?(attribute, key)
96
- record.errors.add(attribute, key, aspect_ratio: interpolate)
90
+ def add_error(record, attribute, default_message, interpolate)
91
+ message = options[:message].presence || default_message
92
+ return if record.errors.added?(attribute, message)
93
+ record.errors.add(attribute, message, aspect_ratio: interpolate)
97
94
  end
98
95
 
99
96
  end
@@ -2,16 +2,23 @@
2
2
 
3
3
  module ActiveStorageValidations
4
4
  class ContentTypeValidator < ActiveModel::EachValidator # :nodoc:
5
+ include OptionProcUnfolding
6
+
7
+ AVAILABLE_CHECKS = %i[with in].freeze
8
+
5
9
  def validate_each(record, attribute, _value)
6
- return true if !record.send(attribute).attached? || types.empty?
10
+ return true unless record.send(attribute).attached?
7
11
 
12
+ types = authorized_types(record)
13
+ return true if types.empty?
14
+
8
15
  files = Array.wrap(record.send(attribute))
9
16
 
10
- errors_options = { authorized_types: types_to_human_format }
17
+ errors_options = { authorized_types: types_to_human_format(types) }
11
18
  errors_options[:message] = options[:message] if options[:message].present?
12
19
 
13
20
  files.each do |file|
14
- next if is_valid?(file)
21
+ next if is_valid?(file, types)
15
22
 
16
23
  errors_options[:content_type] = content_type(file)
17
24
  record.errors.add(attribute, :content_type_invalid, **errors_options)
@@ -19,10 +26,9 @@ module ActiveStorageValidations
19
26
  end
20
27
  end
21
28
 
22
- def types
23
- return @types if defined? @types
24
-
25
- @types = (Array.wrap(options[:with]) + Array.wrap(options[:in])).compact.map do |type|
29
+ def authorized_types(record)
30
+ flat_options = unfold_procs(record, self.options, AVAILABLE_CHECKS)
31
+ (Array.wrap(flat_options[:with]) + Array.wrap(flat_options[:in])).compact.map do |type|
26
32
  if type.is_a?(Regexp)
27
33
  type
28
34
  else
@@ -31,7 +37,7 @@ module ActiveStorageValidations
31
37
  end
32
38
  end
33
39
 
34
- def types_to_human_format
40
+ def types_to_human_format(types)
35
41
  types
36
42
  .map { |type| type.to_s.split('/').last.upcase }
37
43
  .join(', ')
@@ -41,7 +47,7 @@ module ActiveStorageValidations
41
47
  file.blob.present? && file.blob.content_type
42
48
  end
43
49
 
44
- def is_valid?(file)
50
+ def is_valid?(file, types)
45
51
  file_type = content_type(file)
46
52
  types.any? do |type|
47
53
  type == file_type || (type.is_a?(Regexp) && type.match?(file_type.to_s))
@@ -4,27 +4,30 @@ require_relative 'metadata.rb'
4
4
 
5
5
  module ActiveStorageValidations
6
6
  class DimensionValidator < ActiveModel::EachValidator # :nodoc
7
+ include OptionProcUnfolding
8
+
7
9
  AVAILABLE_CHECKS = %i[width height min max].freeze
8
10
 
9
- def initialize(options)
10
- require 'mini_magick' unless defined?(MiniMagick)
11
+ def process_options(record)
12
+ flat_options = unfold_procs(record, self.options, AVAILABLE_CHECKS)
11
13
 
12
14
  [:width, :height].each do |length|
13
- if options[length] and options[length].is_a?(Hash)
14
- if range = options[length][:in]
15
+ if flat_options[length] and flat_options[length].is_a?(Hash)
16
+ if (range = flat_options[length][:in])
15
17
  raise ArgumentError, ":in must be a Range" unless range.is_a?(Range)
16
- options[length][:min], options[length][:max] = range.min, range.max
18
+ flat_options[length][:min], flat_options[length][:max] = range.min, range.max
17
19
  end
18
20
  end
19
21
  end
20
22
  [:min, :max].each do |dim|
21
- if range = options[dim]
23
+ if (range = flat_options[dim])
22
24
  raise ArgumentError, ":#{dim} must be a Range (width..height)" unless range.is_a?(Range)
23
- options[:width] = { dim => range.first }
24
- options[:height] = { dim => range.last }
25
+ flat_options[:width] = { dim => range.first }
26
+ flat_options[:height] = { dim => range.last }
25
27
  end
26
28
  end
27
- super
29
+
30
+ flat_options
28
31
  end
29
32
 
30
33
 
@@ -34,7 +37,7 @@ module ActiveStorageValidations
34
37
  end
35
38
 
36
39
 
37
- if Rails::VERSION::MAJOR >= 6
40
+ if Rails.gem_version >= Gem::Version.new('6.0.0')
38
41
  def validate_each(record, attribute, _value)
39
42
  return true unless record.send(attribute).attached?
40
43
 
@@ -66,63 +69,67 @@ module ActiveStorageValidations
66
69
 
67
70
 
68
71
  def is_valid?(record, attribute, file_metadata)
72
+ flat_options = process_options(record)
69
73
  # Validation fails unless file metadata contains valid width and height.
70
74
  if file_metadata[:width].to_i <= 0 || file_metadata[:height].to_i <= 0
71
- add_error(record, attribute, options[:message].presence || :image_metadata_missing)
75
+ add_error(record, attribute, :image_metadata_missing)
72
76
  return false
73
77
  end
74
78
 
75
79
  # Validation based on checks :min and :max (:min, :max has higher priority to :width, :height).
76
- if options[:min] || options[:max]
77
- if options[:min] && (
78
- (options[:width][:min] && file_metadata[:width] < options[:width][:min]) ||
79
- (options[:height][:min] && file_metadata[:height] < options[:height][:min])
80
+ if flat_options[:min] || flat_options[:max]
81
+ if flat_options[:min] && (
82
+ (flat_options[:width][:min] && file_metadata[:width] < flat_options[:width][:min]) ||
83
+ (flat_options[:height][:min] && file_metadata[:height] < flat_options[:height][:min])
80
84
  )
81
- add_error(record, attribute, options[:message].presence || :"dimension_min_inclusion", width: options[:width][:min], height: options[:height][:min])
85
+ add_error(record, attribute, :dimension_min_inclusion, width: flat_options[:width][:min], height: flat_options[:height][:min])
82
86
  return false
83
87
  end
84
- if options[:max] && (
85
- (options[:width][:max] && file_metadata[:width] > options[:width][:max]) ||
86
- (options[:height][:max] && file_metadata[:height] > options[:height][:max])
88
+ if flat_options[:max] && (
89
+ (flat_options[:width][:max] && file_metadata[:width] > flat_options[:width][:max]) ||
90
+ (flat_options[:height][:max] && file_metadata[:height] > flat_options[:height][:max])
87
91
  )
88
- add_error(record, attribute, options[:message].presence || :"dimension_max_inclusion", width: options[:width][:max], height: options[:height][:max])
92
+ add_error(record, attribute, :dimension_max_inclusion, width: flat_options[:width][:max], height: flat_options[:height][:max])
89
93
  return false
90
94
  end
91
95
 
92
96
  # Validation based on checks :width and :height.
93
97
  else
98
+ width_or_height_invalid = false
94
99
  [:width, :height].each do |length|
95
- next unless options[length]
96
- if options[length].is_a?(Hash)
97
- if options[length][:in] && (file_metadata[length] < options[length][:min] || file_metadata[length] > options[length][:max])
98
- add_error(record, attribute, options[:message].presence || :"dimension_#{length}_inclusion", min: options[length][:min], max: options[length][:max])
99
- return false
100
+ next unless flat_options[length]
101
+ if flat_options[length].is_a?(Hash)
102
+ if flat_options[length][:in] && (file_metadata[length] < flat_options[length][:min] || file_metadata[length] > flat_options[length][:max])
103
+ add_error(record, attribute, :"dimension_#{length}_inclusion", min: flat_options[length][:min], max: flat_options[length][:max])
104
+ width_or_height_invalid = true
100
105
  else
101
- if options[length][:min] && file_metadata[length] < options[length][:min]
102
- add_error(record, attribute, options[:message].presence || :"dimension_#{length}_greater_than_or_equal_to", length: options[length][:min])
103
- return false
106
+ if flat_options[length][:min] && file_metadata[length] < flat_options[length][:min]
107
+ add_error(record, attribute, :"dimension_#{length}_greater_than_or_equal_to", length: flat_options[length][:min])
108
+ width_or_height_invalid = true
104
109
  end
105
- if options[length][:max] && file_metadata[length] > options[length][:max]
106
- add_error(record, attribute, options[:message].presence || :"dimension_#{length}_less_than_or_equal_to", length: options[length][:max])
107
- return false
110
+ if flat_options[length][:max] && file_metadata[length] > flat_options[length][:max]
111
+ add_error(record, attribute, :"dimension_#{length}_less_than_or_equal_to", length: flat_options[length][:max])
112
+ width_or_height_invalid = true
108
113
  end
109
114
  end
110
115
  else
111
- if file_metadata[length] != options[length]
112
- add_error(record, attribute, options[:message].presence || :"dimension_#{length}_equal_to", length: options[length])
113
- return false
116
+ if file_metadata[length] != flat_options[length]
117
+ add_error(record, attribute, :"dimension_#{length}_equal_to", length: flat_options[length])
118
+ width_or_height_invalid = true
114
119
  end
115
120
  end
116
121
  end
122
+
123
+ return false if width_or_height_invalid
117
124
  end
118
125
 
119
126
  true # valid file
120
127
  end
121
128
 
122
- def add_error(record, attribute, type, **attrs)
123
- key = options[:message].presence || type
124
- return if record.errors.added?(attribute, key)
125
- record.errors.add(attribute, key, **attrs)
129
+ def add_error(record, attribute, default_message, **attrs)
130
+ message = options[:message].presence || default_message
131
+ return if record.errors.added?(attribute, message)
132
+ record.errors.add(attribute, message, **attrs)
126
133
  end
127
134
 
128
135
  end
@@ -2,11 +2,12 @@
2
2
 
3
3
  module ActiveStorageValidations
4
4
  class LimitValidator < ActiveModel::EachValidator # :nodoc:
5
+ include OptionProcUnfolding
6
+
5
7
  AVAILABLE_CHECKS = %i[max min].freeze
6
8
 
7
9
  def check_validity!
8
10
  return true if AVAILABLE_CHECKS.any? { |argument| options.key?(argument) }
9
-
10
11
  raise ArgumentError, 'You must pass either :max or :min to the validator'
11
12
  end
12
13
 
@@ -14,19 +15,20 @@ module ActiveStorageValidations
14
15
  return true unless record.send(attribute).attached?
15
16
 
16
17
  files = Array.wrap(record.send(attribute)).compact.uniq
17
- errors_options = { min: options[:min], max: options[:max] }
18
+ flat_options = unfold_procs(record, self.options, AVAILABLE_CHECKS)
19
+ errors_options = { min: flat_options[:min], max: flat_options[:max] }
18
20
 
19
- return true if files_count_valid?(files.count)
21
+ return true if files_count_valid?(files.count, flat_options)
20
22
  record.errors.add(attribute, options[:message].presence || :limit_out_of_range, **errors_options)
21
23
  end
22
24
 
23
- def files_count_valid?(count)
24
- if options[:max].present? && options[:min].present?
25
- count >= options[:min] && count <= options[:max]
26
- elsif options[:max].present?
27
- count <= options[:max]
28
- elsif options[:min].present?
29
- count >= options[:min]
25
+ def files_count_valid?(count, flat_options)
26
+ if flat_options[:max].present? && flat_options[:min].present?
27
+ count >= flat_options[:min] && count <= flat_options[:max]
28
+ elsif flat_options[:max].present?
29
+ count <= flat_options[:max]
30
+ elsif flat_options[:min].present?
31
+ count >= flat_options[:min]
30
32
  end
31
33
  end
32
34
  end
@@ -33,15 +33,19 @@ module ActiveStorageValidations
33
33
  end
34
34
 
35
35
  def failure_message
36
- <<~MESSAGE
37
- Expected #{@attribute_name}
38
-
39
- Accept content types: #{allowed_types.join(", ")}
40
- #{accepted_types_and_failures}
41
-
42
- Reject content types: #{rejected_types.join(", ")}
43
- #{rejected_types_and_failures}
44
- MESSAGE
36
+ message = ["Expected #{@attribute_name}"]
37
+
38
+ if @allowed_types
39
+ message << "Accept content types: #{allowed_types.join(", ")}"
40
+ message << "#{@missing_allowed_types.join(", ")} were rejected"
41
+ end
42
+
43
+ if @rejected_types
44
+ message << "Reject content types: #{rejected_types.join(", ")}"
45
+ message << "#{@missing_rejected_types.join(", ")} were accepted"
46
+ end
47
+
48
+ message.join("\n")
45
49
  end
46
50
 
47
51
  protected
@@ -57,7 +61,7 @@ module ActiveStorageValidations
57
61
  end
58
62
 
59
63
  def rejected_types
60
- @rejected_types || (Marcel::TYPES.keys - allowed_types)
64
+ @rejected_types || []
61
65
  end
62
66
 
63
67
  def allowed_types_allowed?
@@ -70,22 +74,6 @@ module ActiveStorageValidations
70
74
  @missing_rejected_types.none?
71
75
  end
72
76
 
73
- def accepted_types_and_failures
74
- if @missing_allowed_types.present?
75
- "#{@missing_allowed_types.join(", ")} were rejected."
76
- else
77
- "All were accepted successfully."
78
- end
79
- end
80
-
81
- def rejected_types_and_failures
82
- if @missing_rejected_types.present?
83
- "#{@missing_rejected_types.join(", ")} were accepted."
84
- else
85
- "All were rejected successfully."
86
- end
87
- end
88
-
89
77
  def type_allowed?(type)
90
78
  @subject.public_send(@attribute_name).attach(attachment_for(type))
91
79
  @subject.validate
@@ -22,7 +22,7 @@ module ActiveStorageValidations
22
22
  end
23
23
 
24
24
  def self.mock_metadata(attachment, width, height)
25
- if Rails::VERSION::MAJOR >= 6
25
+ if Rails.gem_version >= Gem::Version.new('6.0.0')
26
26
  # Mock the Metadata class for rails 6
27
27
  mock = OpenStruct.new(metadata: { width: width, height: height })
28
28
  stub_method(ActiveStorageValidations::Metadata, :new, mock) do
@@ -1,11 +1,34 @@
1
1
  module ActiveStorageValidations
2
2
  class Metadata
3
+ class InvalidImageError < StandardError; end
4
+
3
5
  attr_reader :file
4
6
 
5
7
  def initialize(file)
8
+ require_image_processor
6
9
  @file = file
7
10
  end
8
11
 
12
+ def image_processor
13
+ Rails.application.config.active_storage.variant_processor
14
+ end
15
+
16
+ def exception_class
17
+ if image_processor == :vips && defined?(Vips)
18
+ Vips::Error
19
+ elsif defined?(MiniMagick)
20
+ MiniMagick::Error
21
+ end
22
+ end
23
+
24
+ def require_image_processor
25
+ if image_processor == :vips
26
+ require 'vips' unless defined?(Vips)
27
+ else
28
+ require 'mini_magick' unless defined?(MiniMagick)
29
+ end
30
+ end
31
+
9
32
  def metadata
10
33
  read_image do |image|
11
34
  if rotated_image?(image)
@@ -14,6 +37,16 @@ module ActiveStorageValidations
14
37
  { width: image.width, height: image.height }
15
38
  end
16
39
  end
40
+ rescue InvalidImageError
41
+ logger.info "Skipping image analysis because ImageMagick or Vips doesn't support the file"
42
+ {}
43
+ end
44
+
45
+ def valid?
46
+ read_image
47
+ true
48
+ rescue InvalidImageError
49
+ false
17
50
  end
18
51
 
19
52
  private
@@ -21,17 +54,16 @@ module ActiveStorageValidations
21
54
  def read_image
22
55
  is_string = file.is_a?(String)
23
56
  if is_string || file.is_a?(ActiveStorage::Blob)
24
- if is_string
25
- # If Rails 5.2 or 6.0, use `find_signed`
26
- if Rails::VERSION::MAJOR < 6 || (Rails::VERSION::MAJOR == 6 && Rails::VERSION::MINOR == 0)
27
- blob = ActiveStorage::Blob.find_signed(file)
28
- # If Rails 6.1 or higher, use `find_signed!`
29
- elsif Rails::VERSION::MAJOR > 6 || (Rails::VERSION::MAJOR == 6 && Rails::VERSION::MINOR >= 1)
30
- blob = ActiveStorage::Blob.find_signed!(file)
57
+ blob =
58
+ if is_string
59
+ if Rails.gem_version < Gem::Version.new('6.1.0')
60
+ ActiveStorage::Blob.find_signed(file)
61
+ else
62
+ ActiveStorage::Blob.find_signed!(file)
63
+ end
64
+ else
65
+ file
31
66
  end
32
- else
33
- blob = file
34
- end
35
67
 
36
68
  tempfile = Tempfile.new(["ActiveStorage-#{blob.id}-", blob.filename.extension_with_delimiter])
37
69
  tempfile.binmode
@@ -43,29 +75,49 @@ module ActiveStorageValidations
43
75
  tempfile.flush
44
76
  tempfile.rewind
45
77
 
46
- image = MiniMagick::Image.new(tempfile.path)
78
+ image = if image_processor == :vips && defined?(Vips) && Vips::get_suffixes.include?(File.extname(tempfile.path).downcase)
79
+ Vips::Image.new_from_file(tempfile.path)
80
+ elsif defined?(MiniMagick)
81
+ MiniMagick::Image.new(tempfile.path)
82
+ end
47
83
  else
48
- image = MiniMagick::Image.new(read_file_path)
84
+ image = if image_processor == :vips && defined?(Vips) && Vips::get_suffixes.include?(File.extname(read_file_path).downcase)
85
+ Vips::Image.new_from_file(read_file_path)
86
+ elsif defined?(MiniMagick)
87
+ MiniMagick::Image.new(read_file_path)
88
+ end
49
89
  end
50
90
 
51
- if image.valid?
52
- yield image
53
- else
54
- logger.info "Skipping image analysis because ImageMagick doesn't support the file"
55
- {}
56
- end
57
- rescue LoadError
58
- logger.info "Skipping image analysis because the mini_magick gem isn't installed"
91
+
92
+ raise InvalidImageError unless valid_image?(image)
93
+ yield image if block_given?
94
+ rescue LoadError, NameError
95
+ logger.info "Skipping image analysis because the mini_magick or ruby-vips gem isn't installed"
59
96
  {}
60
- rescue MiniMagick::Error => error
61
- logger.error "Skipping image analysis due to an ImageMagick error: #{error.message}"
97
+ rescue exception_class => error
98
+ logger.error "Skipping image analysis due to an #{exception_class.name.split('::').map(&:downcase).join(' ').capitalize} error: #{error.message}"
62
99
  {}
63
100
  ensure
64
101
  image = nil
65
102
  end
66
103
 
104
+ def valid_image?(image)
105
+ return false unless image
106
+
107
+ image_processor == :vips && image.is_a?(Vips::Image) ? image.avg : image.valid?
108
+ rescue exception_class
109
+ false
110
+ end
111
+
67
112
  def rotated_image?(image)
68
- %w[ RightTop LeftBottom ].include?(image["%[orientation]"])
113
+ if image_processor == :vips && image.is_a?(Vips::Image)
114
+ image.get('exif-ifd0-Orientation').include?('Right-top') ||
115
+ image.get('exif-ifd0-Orientation').include?('Left-bottom')
116
+ else
117
+ %w[ RightTop LeftBottom ].include?(image["%[orientation]"])
118
+ end
119
+ rescue exception_class # field "exif-ifd0-Orientation" not found
120
+ false
69
121
  end
70
122
 
71
123
  def read_file_path
@@ -0,0 +1,16 @@
1
+ module ActiveStorageValidations
2
+ module OptionProcUnfolding
3
+
4
+ def unfold_procs(record, object, only_keys)
5
+ case object
6
+ when Hash
7
+ object.merge(object) { |key, value| only_keys&.exclude?(key) ? {} : unfold_procs(record, value, nil) }
8
+ when Array
9
+ object.map { |o| unfold_procs(record, o, only_keys) }
10
+ else
11
+ object.is_a?(Proc) ? object.call(record) : object
12
+ end
13
+ end
14
+
15
+ end
16
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'metadata.rb'
4
+
5
+ module ActiveStorageValidations
6
+ class ProcessableImageValidator < ActiveModel::EachValidator # :nodoc
7
+ include OptionProcUnfolding
8
+
9
+ if Rails.gem_version >= Gem::Version.new('6.0.0')
10
+ def validate_each(record, attribute, _value)
11
+ return true unless record.send(attribute).attached?
12
+
13
+ changes = record.attachment_changes[attribute.to_s]
14
+ return true if changes.blank?
15
+
16
+ files = Array.wrap(changes.is_a?(ActiveStorage::Attached::Changes::CreateMany) ? changes.attachables : changes.attachable)
17
+
18
+ files.each do |file|
19
+ add_error(record, attribute, :image_not_processable) unless Metadata.new(file).valid?
20
+ end
21
+ end
22
+ else
23
+ # Rails 5
24
+ def validate_each(record, attribute, _value)
25
+ return true unless record.send(attribute).attached?
26
+
27
+ files = Array.wrap(record.send(attribute))
28
+
29
+ files.each do |file|
30
+ add_error(record, attribute, :image_not_processable) unless Metadata.new(file).valid?
31
+ end
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def add_error(record, attribute, default_message)
38
+ message = options[:message].presence || default_message
39
+ return if record.errors.added?(attribute, message)
40
+ record.errors.add(attribute, message)
41
+ end
42
+ end
43
+ end
@@ -2,14 +2,15 @@
2
2
 
3
3
  module ActiveStorageValidations
4
4
  class SizeValidator < ActiveModel::EachValidator # :nodoc:
5
+ include OptionProcUnfolding
6
+
5
7
  delegate :number_to_human_size, to: ActiveSupport::NumberHelper
6
8
 
7
9
  AVAILABLE_CHECKS = %i[less_than less_than_or_equal_to greater_than greater_than_or_equal_to between].freeze
8
10
 
9
11
  def check_validity!
10
12
  return true if AVAILABLE_CHECKS.any? { |argument| options.key?(argument) }
11
-
12
- raise ArgumentError, 'You must pass either :less_than, :greater_than, or :between to the validator'
13
+ raise ArgumentError, 'You must pass either :less_than(_or_equal_to), :greater_than(_or_equal_to), or :between to the validator'
13
14
  end
14
15
 
15
16
  def validate_each(record, attribute, _value)
@@ -20,28 +21,40 @@ module ActiveStorageValidations
20
21
 
21
22
  errors_options = {}
22
23
  errors_options[:message] = options[:message] if options[:message].present?
24
+ flat_options = unfold_procs(record, self.options, AVAILABLE_CHECKS)
23
25
 
24
26
  files.each do |file|
25
- next if content_size_valid?(file.blob.byte_size)
27
+ next if content_size_valid?(file.blob.byte_size, flat_options)
26
28
 
27
29
  errors_options[:file_size] = number_to_human_size(file.blob.byte_size)
30
+ errors_options[:min_size] = number_to_human_size(min_size(flat_options))
31
+ errors_options[:max_size] = number_to_human_size(max_size(flat_options))
32
+
28
33
  record.errors.add(attribute, :file_size_out_of_range, **errors_options)
29
34
  break
30
35
  end
31
36
  end
32
37
 
33
- def content_size_valid?(file_size)
34
- if options[:between].present?
35
- options[:between].include?(file_size)
36
- elsif options[:less_than].present?
37
- file_size < options[:less_than]
38
- elsif options[:less_than_or_equal_to].present?
39
- file_size <= options[:less_than_or_equal_to]
40
- elsif options[:greater_than].present?
41
- file_size > options[:greater_than]
42
- elsif options[:greater_than_or_equal_to].present?
43
- file_size >= options[:greater_than_or_equal_to]
38
+ def content_size_valid?(file_size, flat_options)
39
+ if flat_options[:between].present?
40
+ flat_options[:between].include?(file_size)
41
+ elsif flat_options[:less_than].present?
42
+ file_size < flat_options[:less_than]
43
+ elsif flat_options[:less_than_or_equal_to].present?
44
+ file_size <= flat_options[:less_than_or_equal_to]
45
+ elsif flat_options[:greater_than].present?
46
+ file_size > flat_options[:greater_than]
47
+ elsif flat_options[:greater_than_or_equal_to].present?
48
+ file_size >= flat_options[:greater_than_or_equal_to]
44
49
  end
45
50
  end
51
+
52
+ def min_size(flat_options)
53
+ flat_options[:between]&.min || flat_options[:greater_than] || flat_options[:greater_than_or_equal_to]
54
+ end
55
+
56
+ def max_size(flat_options)
57
+ flat_options[:between]&.max || flat_options[:less_than] || flat_options[:less_than_or_equal_to]
58
+ end
46
59
  end
47
60
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveStorageValidations
4
- VERSION = '0.9.5'
4
+ VERSION = '1.0.3'
5
5
  end
@@ -2,12 +2,14 @@
2
2
 
3
3
  require 'active_storage_validations/railtie'
4
4
  require 'active_storage_validations/engine'
5
+ require 'active_storage_validations/option_proc_unfolding'
5
6
  require 'active_storage_validations/attached_validator'
6
7
  require 'active_storage_validations/content_type_validator'
7
8
  require 'active_storage_validations/size_validator'
8
9
  require 'active_storage_validations/limit_validator'
9
10
  require 'active_storage_validations/dimension_validator'
10
11
  require 'active_storage_validations/aspect_ratio_validator'
12
+ require 'active_storage_validations/processable_image_validator'
11
13
 
12
14
  ActiveSupport.on_load(:active_record) do
13
15
  send :include, ActiveStorageValidations
metadata CHANGED
@@ -1,17 +1,59 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_storage_validations
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.5
4
+ version: 1.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Igor Kasyanchuk
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-06-26 00:00:00.000000000 Z
11
+ date: 2022-10-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: rails
14
+ name: activejob
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 5.2.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 5.2.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: activemodel
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 5.2.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: 5.2.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: activestorage
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: 5.2.0
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: 5.2.0
55
+ - !ruby/object:Gem::Dependency
56
+ name: activesupport
15
57
  requirement: !ruby/object:Gem::Requirement
16
58
  requirements:
17
59
  - - ">="
@@ -52,6 +94,20 @@ dependencies:
52
94
  - - ">="
53
95
  - !ruby/object:Gem::Version
54
96
  version: 4.9.5
97
+ - !ruby/object:Gem::Dependency
98
+ name: ruby-vips
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: 2.1.0
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: 2.1.0
55
111
  - !ruby/object:Gem::Dependency
56
112
  name: pry
57
113
  requirement: !ruby/object:Gem::Requirement
@@ -94,6 +150,34 @@ dependencies:
94
150
  - - ">="
95
151
  - !ruby/object:Gem::Version
96
152
  version: '0'
153
+ - !ruby/object:Gem::Dependency
154
+ name: marcel
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
167
+ - !ruby/object:Gem::Dependency
168
+ name: globalid
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - ">="
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - ">="
179
+ - !ruby/object:Gem::Version
180
+ version: '0'
97
181
  description: Validations for Active Storage (presence)
98
182
  email:
99
183
  - igorkasyanchuk@gmail.com
@@ -117,6 +201,7 @@ files:
117
201
  - config/locales/tr.yml
118
202
  - config/locales/uk.yml
119
203
  - config/locales/vi.yml
204
+ - config/locales/zh-CN.yml
120
205
  - lib/active_storage_validations.rb
121
206
  - lib/active_storage_validations/aspect_ratio_validator.rb
122
207
  - lib/active_storage_validations/attached_validator.rb
@@ -130,6 +215,8 @@ files:
130
215
  - lib/active_storage_validations/matchers/dimension_validator_matcher.rb
131
216
  - lib/active_storage_validations/matchers/size_validator_matcher.rb
132
217
  - lib/active_storage_validations/metadata.rb
218
+ - lib/active_storage_validations/option_proc_unfolding.rb
219
+ - lib/active_storage_validations/processable_image_validator.rb
133
220
  - lib/active_storage_validations/railtie.rb
134
221
  - lib/active_storage_validations/size_validator.rb
135
222
  - lib/active_storage_validations/version.rb
@@ -138,7 +225,7 @@ homepage: https://github.com/igorkasyanchuk/active_storage_validations
138
225
  licenses:
139
226
  - MIT
140
227
  metadata: {}
141
- post_install_message:
228
+ post_install_message:
142
229
  rdoc_options: []
143
230
  require_paths:
144
231
  - lib
@@ -153,8 +240,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
153
240
  - !ruby/object:Gem::Version
154
241
  version: '0'
155
242
  requirements: []
156
- rubygems_version: 3.0.3
157
- signing_key:
243
+ rubygems_version: 3.2.3
244
+ signing_key:
158
245
  specification_version: 4
159
246
  summary: Validations for Active Storage
160
247
  test_files: []