active_storage_validations 0.9.8 → 1.0.1

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: c9b366848f85ac520038a9efe0bee0514338f4d28b8f292ef87ad06d1ee0da28
4
- data.tar.gz: 651195be6d5d23bf20c4600656e799005e3e1fc118ac21cc5fe023b24f43ae73
3
+ metadata.gz: b3a3eccb847537354b9018f239171e4b3bdf363991d68c57844d87ecd0c14fcd
4
+ data.tar.gz: 432cfb9b3162db1d57446536de58d3b511c5a641d3da8c22123da56c877fd804
5
5
  SHA512:
6
- metadata.gz: f6231454bce1c4b09bf4dba3a83238f09b3fa0593c0c04146342ac6dd035048f890edcb594d24747f9d5e55d5ef86a35eba7fa93b109f101906b0f5b4f373c06
7
- data.tar.gz: 6a5a9a0abd3f7abbddd92d8f1e5601859bbcc06e17f049046e5c9e2744760aa4fd7fa77462c22fb8d076cc4aef801e9b814a6b8663edb08e9c32a497ec041908
6
+ metadata.gz: '0697168d7d38d2974772a89e203861099bf5ac476e147dbc14826d1cee98271e3ded9789fd0ada073853d835658165141a596eeeb92af1cf52850d8c8f76c110'
7
+ data.tar.gz: d9ad210e053d90e69df85a6ce21644ace0eb096376db6ff5c5b1e4fb31a6eff2d23db6000f1ef566fba33337d242b23b52dbdc9af481b10e9870248a88c15fac
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
 
@@ -36,6 +41,7 @@ class User < ApplicationRecord
36
41
  dimension: { width: { min: 800, max: 2400 },
37
42
  height: { min: 600, max: 1800 }, message: 'is not given between dimension' }
38
43
  validates :image, attached: true,
44
+ processable_image: true,
39
45
  content_type: ['image/png', 'image/jpeg'],
40
46
  aspect_ratio: :landscape
41
47
  end
@@ -121,6 +127,18 @@ class User < ApplicationRecord
121
127
  end
122
128
  ```
123
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
+
124
142
  ## Internationalization (I18n)
125
143
 
126
144
  Active Storage Validations uses I18n for error messages. For this, add these keys in your translation file:
@@ -148,6 +166,7 @@ en:
148
166
  aspect_ratio_not_landscape: "must be a landscape image"
149
167
  aspect_ratio_is_not: "must have an aspect ratio of %{aspect_ratio}"
150
168
  aspect_ratio_unknown: "has an unknown aspect ratio"
169
+ image_not_processable: "is not a valid image"
151
170
  ```
152
171
 
153
172
  In some cases, Active Storage Validations provides variables to help you customize messages:
@@ -303,12 +322,10 @@ To run tests in root folder of gem:
303
322
  Snippet to run in console:
304
323
 
305
324
  ```
306
- BUNDLE_GEMFILE=gemfiles/rails_5_2.gemfile bundle
307
325
  BUNDLE_GEMFILE=gemfiles/rails_6_0.gemfile bundle
308
326
  BUNDLE_GEMFILE=gemfiles/rails_6_1.gemfile bundle
309
327
  BUNDLE_GEMFILE=gemfiles/rails_7_0.gemfile bundle
310
328
  BUNDLE_GEMFILE=gemfiles/rails_next.gemfile bundle
311
- BUNDLE_GEMFILE=gemfiles/rails_5_2.gemfile bundle exec rake test
312
329
  BUNDLE_GEMFILE=gemfiles/rails_6_0.gemfile bundle exec rake test
313
330
  BUNDLE_GEMFILE=gemfiles/rails_6_1.gemfile bundle exec rake test
314
331
  BUNDLE_GEMFILE=gemfiles/rails_7_0.gemfile bundle exec rake test
@@ -378,10 +395,15 @@ You are welcome to contribute.
378
395
  - https://github.com/stephensolis
379
396
  - https://github.com/kwent
380
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
381
403
 
382
404
  ## License
383
405
 
384
406
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
385
407
 
386
408
  [<img src="https://github.com/igorkasyanchuk/rails_time_travel/blob/main/docs/more_gems.png?raw=true"
387
- />](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"
@@ -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,17 +4,14 @@ 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
- super(options)
12
- end
13
-
14
-
15
12
  def check_validity!
16
13
  return true if AVAILABLE_CHECKS.any? { |argument| options.key?(argument) }
17
- raise ArgumentError, 'You must pass "aspect_ratio: :OPTION" option to the validator'
14
+ raise ArgumentError, 'You must pass :with to the validator'
18
15
  end
19
16
 
20
17
  if Rails.gem_version >= Gem::Version.new('6.0.0')
@@ -55,26 +52,27 @@ module ActiveStorageValidations
55
52
 
56
53
 
57
54
  def is_valid?(record, attribute, metadata)
55
+ flat_options = unfold_procs(record, self.options, AVAILABLE_CHECKS)
58
56
  if metadata[:width].to_i <= 0 || metadata[:height].to_i <= 0
59
- add_error(record, attribute, options[:message].presence || :image_metadata_missing)
57
+ add_error(record, attribute, :image_metadata_missing, flat_options[:with])
60
58
  return false
61
59
  end
62
60
 
63
- case options[:with]
61
+ case flat_options[:with]
64
62
  when :square
65
63
  return true if metadata[:width] == metadata[:height]
66
- add_error(record, attribute, :aspect_ratio_not_square)
64
+ add_error(record, attribute, :aspect_ratio_not_square, flat_options[:with])
67
65
 
68
66
  when :portrait
69
67
  return true if metadata[:height] > metadata[:width]
70
- add_error(record, attribute, :aspect_ratio_not_portrait)
68
+ add_error(record, attribute, :aspect_ratio_not_portrait, flat_options[:with])
71
69
 
72
70
  when :landscape
73
71
  return true if metadata[:width] > metadata[:height]
74
- add_error(record, attribute, :aspect_ratio_not_landscape)
72
+ add_error(record, attribute, :aspect_ratio_not_landscape, flat_options[:with])
75
73
 
76
74
  else
77
- if options[:with] =~ /is\_(\d*)\_(\d*)/
75
+ if flat_options[:with] =~ /is_(\d*)_(\d*)/
78
76
  x = $1.to_i
79
77
  y = $2.to_i
80
78
 
@@ -82,17 +80,17 @@ module ActiveStorageValidations
82
80
 
83
81
  add_error(record, attribute, :aspect_ratio_is_not, "#{x}x#{y}")
84
82
  else
85
- add_error(record, attribute, :aspect_ratio_unknown)
83
+ add_error(record, attribute, :aspect_ratio_unknown, flat_options[:with])
86
84
  end
87
85
  end
88
86
  false
89
87
  end
90
88
 
91
89
 
92
- def add_error(record, attribute, type, interpolate = options[:with])
93
- key = options[:message].presence || type
94
- return if record.errors.added?(attribute, key)
95
- 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)
96
94
  end
97
95
 
98
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,25 +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)
11
+ def process_options(record)
12
+ flat_options = unfold_procs(record, self.options, AVAILABLE_CHECKS)
13
+
10
14
  [:width, :height].each do |length|
11
- if options[length] and options[length].is_a?(Hash)
12
- if range = options[length][:in]
15
+ if flat_options[length] and flat_options[length].is_a?(Hash)
16
+ if (range = flat_options[length][:in])
13
17
  raise ArgumentError, ":in must be a Range" unless range.is_a?(Range)
14
- options[length][:min], options[length][:max] = range.min, range.max
18
+ flat_options[length][:min], flat_options[length][:max] = range.min, range.max
15
19
  end
16
20
  end
17
21
  end
18
22
  [:min, :max].each do |dim|
19
- if range = options[dim]
23
+ if (range = flat_options[dim])
20
24
  raise ArgumentError, ":#{dim} must be a Range (width..height)" unless range.is_a?(Range)
21
- options[:width] = { dim => range.first }
22
- options[:height] = { dim => range.last }
25
+ flat_options[:width] = { dim => range.first }
26
+ flat_options[:height] = { dim => range.last }
23
27
  end
24
28
  end
25
- super
29
+
30
+ flat_options
26
31
  end
27
32
 
28
33
 
@@ -64,26 +69,27 @@ module ActiveStorageValidations
64
69
 
65
70
 
66
71
  def is_valid?(record, attribute, file_metadata)
72
+ flat_options = process_options(record)
67
73
  # Validation fails unless file metadata contains valid width and height.
68
74
  if file_metadata[:width].to_i <= 0 || file_metadata[:height].to_i <= 0
69
- add_error(record, attribute, options[:message].presence || :image_metadata_missing)
75
+ add_error(record, attribute, :image_metadata_missing)
70
76
  return false
71
77
  end
72
78
 
73
79
  # Validation based on checks :min and :max (:min, :max has higher priority to :width, :height).
74
- if options[:min] || options[:max]
75
- if options[:min] && (
76
- (options[:width][:min] && file_metadata[:width] < options[:width][:min]) ||
77
- (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])
78
84
  )
79
- 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])
80
86
  return false
81
87
  end
82
- if options[:max] && (
83
- (options[:width][:max] && file_metadata[:width] > options[:width][:max]) ||
84
- (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])
85
91
  )
86
- 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])
87
93
  return false
88
94
  end
89
95
 
@@ -91,24 +97,24 @@ module ActiveStorageValidations
91
97
  else
92
98
  width_or_height_invalid = false
93
99
  [:width, :height].each do |length|
94
- next unless options[length]
95
- if options[length].is_a?(Hash)
96
- if options[length][:in] && (file_metadata[length] < options[length][:min] || file_metadata[length] > options[length][:max])
97
- add_error(record, attribute, options[:message].presence || :"dimension_#{length}_inclusion", min: options[length][:min], max: options[length][:max])
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])
98
104
  width_or_height_invalid = true
99
105
  else
100
- if options[length][:min] && file_metadata[length] < options[length][:min]
101
- add_error(record, attribute, options[:message].presence || :"dimension_#{length}_greater_than_or_equal_to", length: options[length][:min])
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])
102
108
  width_or_height_invalid = true
103
109
  end
104
- if options[length][:max] && file_metadata[length] > options[length][:max]
105
- add_error(record, attribute, options[:message].presence || :"dimension_#{length}_less_than_or_equal_to", length: options[length][:max])
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])
106
112
  width_or_height_invalid = true
107
113
  end
108
114
  end
109
115
  else
110
- if file_metadata[length] != options[length]
111
- add_error(record, attribute, options[:message].presence || :"dimension_#{length}_equal_to", length: options[length])
116
+ if file_metadata[length] != flat_options[length]
117
+ add_error(record, attribute, :"dimension_#{length}_equal_to", length: flat_options[length])
112
118
  width_or_height_invalid = true
113
119
  end
114
120
  end
@@ -120,10 +126,10 @@ module ActiveStorageValidations
120
126
  true # valid file
121
127
  end
122
128
 
123
- def add_error(record, attribute, type, **attrs)
124
- key = options[:message].presence || type
125
- return if record.errors.added?(attribute, key)
126
- 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)
127
133
  end
128
134
 
129
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
@@ -19,11 +19,13 @@ module ActiveStorageValidations
19
19
 
20
20
  def allowing(*types)
21
21
  @allowed_types = types.flatten
22
+ either_allowing_or_rejecting
22
23
  self
23
24
  end
24
25
 
25
26
  def rejecting(*types)
26
27
  @rejected_types = types.flatten
28
+ either_allowing_or_rejecting
27
29
  self
28
30
  end
29
31
 
@@ -33,19 +35,29 @@ module ActiveStorageValidations
33
35
  end
34
36
 
35
37
  def failure_message
36
- <<~MESSAGE
37
- Expected #{@attribute_name}
38
+ message = ["Expected #{@attribute_name}"]
38
39
 
39
- Accept content types: #{allowed_types.join(", ")}
40
- #{accepted_types_and_failures}
40
+ if @allowed_types
41
+ message << "Accept content types: #{allowed_types.join(", ")}"
42
+ message << "#{@missing_allowed_types.join(", ")} were rejected"
43
+ end
44
+
45
+ if @rejected_types
46
+ message << "Reject content types: #{rejected_types.join(", ")}"
47
+ message << "#{@missing_rejected_types.join(", ")} were accepted"
48
+ end
41
49
 
42
- Reject content types: #{rejected_types.join(", ")}
43
- #{rejected_types_and_failures}
44
- MESSAGE
50
+ message.join("\n")
45
51
  end
46
52
 
47
53
  protected
48
54
 
55
+ def either_allowing_or_rejecting
56
+ if @allowed_types && @rejected_types
57
+ raise ArgumentError, "You must specify either allowing or rejecting"
58
+ end
59
+ end
60
+
49
61
  def responds_to_methods
50
62
  @subject.respond_to?(@attribute_name) &&
51
63
  @subject.public_send(@attribute_name).respond_to?(:attach) &&
@@ -57,7 +69,7 @@ module ActiveStorageValidations
57
69
  end
58
70
 
59
71
  def rejected_types
60
- @rejected_types || (content_type_keys - allowed_types)
72
+ @rejected_types || []
61
73
  end
62
74
 
63
75
  def allowed_types_allowed?
@@ -70,22 +82,6 @@ module ActiveStorageValidations
70
82
  @missing_rejected_types.none?
71
83
  end
72
84
 
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
85
  def type_allowed?(type)
90
86
  @subject.public_send(@attribute_name).attach(attachment_for(type))
91
87
  @subject.validate
@@ -96,16 +92,6 @@ module ActiveStorageValidations
96
92
  suffix = type.to_s.split('/').last
97
93
  { io: Tempfile.new('.'), filename: "test.#{suffix}", content_type: type }
98
94
  end
99
-
100
- private
101
-
102
- def content_type_keys
103
- if Rails.gem_version < Gem::Version.new('6.1.0')
104
- Mime::LOOKUP.keys
105
- else
106
- Marcel::TYPES.keys
107
- end
108
- end
109
95
  end
110
96
  end
111
97
  end
@@ -1,5 +1,7 @@
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)
@@ -10,7 +12,7 @@ module ActiveStorageValidations
10
12
  def image_processor
11
13
  Rails.application.config.active_storage.variant_processor
12
14
  end
13
-
15
+
14
16
  def exception_class
15
17
  if image_processor == :vips && defined?(Vips)
16
18
  Vips::Error
@@ -35,6 +37,16 @@ module ActiveStorageValidations
35
37
  { width: image.width, height: image.height }
36
38
  end
37
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
38
50
  end
39
51
 
40
52
  private
@@ -76,12 +88,9 @@ module ActiveStorageValidations
76
88
  end
77
89
  end
78
90
 
79
- if image && valid_image?(image)
80
- yield image
81
- else
82
- logger.info "Skipping image analysis because ImageMagick or Vips doesn't support the file"
83
- {}
84
- end
91
+
92
+ raise InvalidImageError unless valid_image?(image)
93
+ yield image
85
94
  rescue LoadError, NameError
86
95
  logger.info "Skipping image analysis because the mini_magick or ruby-vips gem isn't installed"
87
96
  {}
@@ -93,6 +102,8 @@ module ActiveStorageValidations
93
102
  end
94
103
 
95
104
  def valid_image?(image)
105
+ return false unless image
106
+
96
107
  image_processor == :vips && image.is_a?(Vips::Image) ? image.avg : image.valid?
97
108
  rescue exception_class
98
109
  false
@@ -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,39 +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)
28
- errors_options[:min_size] = number_to_human_size(min_size)
29
- errors_options[:max_size] = number_to_human_size(max_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))
30
32
 
31
33
  record.errors.add(attribute, :file_size_out_of_range, **errors_options)
32
34
  break
33
35
  end
34
36
  end
35
37
 
36
- def content_size_valid?(file_size)
37
- if options[:between].present?
38
- options[:between].include?(file_size)
39
- elsif options[:less_than].present?
40
- file_size < options[:less_than]
41
- elsif options[:less_than_or_equal_to].present?
42
- file_size <= options[:less_than_or_equal_to]
43
- elsif options[:greater_than].present?
44
- file_size > options[:greater_than]
45
- elsif options[:greater_than_or_equal_to].present?
46
- 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]
47
49
  end
48
50
  end
49
51
 
50
- def min_size
51
- options[:between]&.min || options[:greater_than] || options[:greater_than_or_equal_to]
52
+ def min_size(flat_options)
53
+ flat_options[:between]&.min || flat_options[:greater_than] || flat_options[:greater_than_or_equal_to]
52
54
  end
53
55
 
54
- def max_size
55
- options[:between]&.max || options[:less_than] || options[:less_than_or_equal_to]
56
+ def max_size(flat_options)
57
+ flat_options[:between]&.max || flat_options[:less_than] || flat_options[:less_than_or_equal_to]
56
58
  end
57
59
  end
58
60
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveStorageValidations
4
- VERSION = '0.9.8'
4
+ VERSION = '1.0.1'
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,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_storage_validations
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.8
4
+ version: 1.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Igor Kasyanchuk
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-04-17 00:00:00.000000000 Z
11
+ date: 2022-10-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activejob
@@ -164,6 +164,20 @@ dependencies:
164
164
  - - ">="
165
165
  - !ruby/object:Gem::Version
166
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'
167
181
  description: Validations for Active Storage (presence)
168
182
  email:
169
183
  - igorkasyanchuk@gmail.com
@@ -187,6 +201,7 @@ files:
187
201
  - config/locales/tr.yml
188
202
  - config/locales/uk.yml
189
203
  - config/locales/vi.yml
204
+ - config/locales/zh-CN.yml
190
205
  - lib/active_storage_validations.rb
191
206
  - lib/active_storage_validations/aspect_ratio_validator.rb
192
207
  - lib/active_storage_validations/attached_validator.rb
@@ -200,6 +215,8 @@ files:
200
215
  - lib/active_storage_validations/matchers/dimension_validator_matcher.rb
201
216
  - lib/active_storage_validations/matchers/size_validator_matcher.rb
202
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
203
220
  - lib/active_storage_validations/railtie.rb
204
221
  - lib/active_storage_validations/size_validator.rb
205
222
  - lib/active_storage_validations/version.rb