active_storage_validations 0.9.5 → 1.0.3

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: 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: []