active_storage_validations 1.1.1 → 1.1.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (30) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +7 -0
  3. data/config/locales/de.yml +1 -0
  4. data/config/locales/es.yml +1 -0
  5. data/config/locales/fr.yml +1 -0
  6. data/config/locales/it.yml +1 -0
  7. data/config/locales/ja.yml +1 -0
  8. data/config/locales/nl.yml +1 -0
  9. data/config/locales/pl.yml +1 -0
  10. data/config/locales/ru.yml +2 -1
  11. data/config/locales/tr.yml +1 -0
  12. data/config/locales/uk.yml +1 -0
  13. data/config/locales/vi.yml +1 -0
  14. data/config/locales/zh-CN.yml +1 -0
  15. data/lib/active_storage_validations/aspect_ratio_validator.rb +2 -0
  16. data/lib/active_storage_validations/attached_validator.rb +6 -1
  17. data/lib/active_storage_validations/concerns/symbolizable.rb +10 -0
  18. data/lib/active_storage_validations/content_type_validator.rb +6 -2
  19. data/lib/active_storage_validations/dimension_validator.rb +15 -0
  20. data/lib/active_storage_validations/error_handler.rb +9 -6
  21. data/lib/active_storage_validations/limit_validator.rb +4 -1
  22. data/lib/active_storage_validations/matchers/attached_validator_matcher.rb +39 -15
  23. data/lib/active_storage_validations/matchers/concerns/validatable.rb +46 -0
  24. data/lib/active_storage_validations/matchers/content_type_validator_matcher.rb +39 -32
  25. data/lib/active_storage_validations/matchers/dimension_validator_matcher.rb +43 -13
  26. data/lib/active_storage_validations/matchers/size_validator_matcher.rb +49 -10
  27. data/lib/active_storage_validations/processable_image_validator.rb +2 -0
  28. data/lib/active_storage_validations/size_validator.rb +21 -2
  29. data/lib/active_storage_validations/version.rb +1 -1
  30. metadata +20 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3b512ecda0ff1fa0f06649f78b7b4f58433ca7f108abd729ce08cd0eea2b1baf
4
- data.tar.gz: 1ef72d050d31ed0d4052672370849c5d66538df5b06e5a477ebc3575fc110e40
3
+ metadata.gz: 288f21f798861bd7e44ad225052620bfcfd2d58efe73f0aeda8f4964ace98f56
4
+ data.tar.gz: b19cce430aae6c1eed3c2d29cbafa6d07b36423f122157603d4391a892267d64
5
5
  SHA512:
6
- metadata.gz: dc0392b8725f2a567df3f99b61c5053ab0870e877ba16f3085871842a70ca7f5db328b63b3275912017831d4ed29697a66106cdad4e7cfbb779a1985b7d20488
7
- data.tar.gz: 8a6883ac4f1cbf0e703c41d1e8ddf6136397fb3973997cc37c83aef3f0456ab5278b5475518ce3ddb8a776a41c8e8f7774160850ddcb49b5ece6e9c846cab8db
6
+ metadata.gz: e7d6e8ac601a1bd25c2c4f4a199782e7b8dd5821ce99a2bcf14a60c0d8dd2f5e55c46d1eeae758438f8e9da34ae2f9366bf4690345dde242af897cf405dc1624
7
+ data.tar.gz: a4335b501b1298517053f45ec6f1192d053cd73fd682bc19127f5891aa7704e2116659991e29c3d2c4bd6ff9acf0847b91389b6bbfafb52d2c45a941dbb9dd35
data/README.md CHANGED
@@ -368,6 +368,11 @@ BUNDLE_GEMFILE=gemfiles/rails_7_0.gemfile bundle exec rake test
368
368
  BUNDLE_GEMFILE=gemfiles/rails_next.gemfile bundle exec rake test
369
369
  ```
370
370
 
371
+ Tips:
372
+ - To focus a specific test, use the `focus` class method provided by [minitest-focus](https://github.com/minitest/minitest-focus)
373
+ - To focus a specific file, use the TEST option provided by minitest, e.g. to only run size_validator_test.rb file you will execute the following command: `bundle exec rake test TEST=test/validators/size_validator_test.rb`
374
+
375
+
371
376
  ## Known issues
372
377
 
373
378
  - There is an issue in Rails which it possible to get if you have added a validation and generating for example an image preview of attachments. It can be fixed with this:
@@ -446,6 +451,8 @@ You are welcome to contribute.
446
451
  - https://github.com/technicalpickles
447
452
  - https://github.com/ricsdeol
448
453
  - https://github.com/Fonsan
454
+ - https://github.com/tagliala
455
+ - https://github.com/ocarreterom
449
456
 
450
457
 
451
458
  ## License
@@ -24,3 +24,4 @@ de:
24
24
  aspect_ratio_not_landscape: "muss Querformat sein"
25
25
  aspect_ratio_is_not: "muss ein Bildseitenverhältnis von %{aspect_ratio} haben"
26
26
  aspect_ratio_unknown: "hat ein unbekanntes Bildseitenverhältnis"
27
+ image_not_processable: "ist kein gültiges Bild"
@@ -24,3 +24,4 @@ es:
24
24
  aspect_ratio_not_landscape: "debe ser una imagen apaisada"
25
25
  aspect_ratio_is_not: "debe tener una relación de aspecto de %{aspect_ratio}"
26
26
  aspect_ratio_unknown: "tiene una relación de aspecto desconocida"
27
+ image_not_processable: "no es una imagen válida"
@@ -24,3 +24,4 @@ fr:
24
24
  aspect_ratio_not_landscape: "doit être une image en format paysage"
25
25
  aspect_ratio_is_not: "doit avoir un rapport hauteur / largeur de %{aspect_ratio}"
26
26
  aspect_ratio_unknown: "a un rapport d'aspect inconnu"
27
+ image_not_processable: "n'est pas une image valide"
@@ -24,3 +24,4 @@ it:
24
24
  aspect_ratio_not_landscape: "l’orientamento dell’immagine deve essere orizzontale"
25
25
  aspect_ratio_is_not: "deve avere un rapporto altezza / larghezza di %{aspect_ratio}"
26
26
  aspect_ratio_unknown: "ha un rapporto altezza / larghezza sconosciuto"
27
+ image_not_processable: "non è un'immagine valida"
@@ -24,3 +24,4 @@ ja:
24
24
  aspect_ratio_not_landscape: "は横長にしてください"
25
25
  aspect_ratio_is_not: "のアスペクト比は %{aspect_ratio} にしてください"
26
26
  aspect_ratio_unknown: "のアスペクト比を取得できませんでした"
27
+ image_not_processable: "は不正な画像です"
@@ -24,3 +24,4 @@ nl:
24
24
  aspect_ratio_not_landscape: "moet een liggende afbeelding zijn"
25
25
  aspect_ratio_is_not: "moet een beeldverhouding hebben van %{aspect_ratio}"
26
26
  aspect_ratio_unknown: "heeft een onbekende beeldverhouding"
27
+ image_not_processable: "is geen geldige afbeelding"
@@ -24,3 +24,4 @@ pl:
24
24
  aspect_ratio_not_landscape: "musi mieć proporcje pejzażu"
25
25
  aspect_ratio_is_not: "musi mieć proporcje %{aspect_ratio}"
26
26
  aspect_ratio_unknown: "ma nieokreślone proporcje"
27
+ image_not_processable: "nie jest prawidłowym obrazem"
@@ -23,4 +23,5 @@ ru:
23
23
  aspect_ratio_not_portrait: "должно быть портретное изображение"
24
24
  aspect_ratio_not_landscape: "должно быть пейзажное изображение"
25
25
  aspect_ratio_is_not: "должен иметь соотношение сторон %{aspect_ratio}"
26
- aspect_ratio_unknown: "имеет неизвестное соотношение сторон"
26
+ aspect_ratio_unknown: "имеет неизвестное соотношение сторон"
27
+ image_not_processable: "не является допустимым изображением"
@@ -24,3 +24,4 @@ tr:
24
24
  aspect_ratio_not_landscape: "yatay bir imaj olmalı"
25
25
  aspect_ratio_is_not: "%{aspect_ratio} en boy oranına sahip olmalı"
26
26
  aspect_ratio_unknown: "bilinmeyen en boy oranı"
27
+ image_not_processable: "geçerli bir imaj değil"
@@ -24,3 +24,4 @@ uk:
24
24
  aspect_ratio_not_landscape: "мусить бути пейзажне зображення"
25
25
  aspect_ratio_is_not: "мусить мати співвідношення сторін %{aspect_ratio}"
26
26
  aspect_ratio_unknown: "має невідоме співвідношення сторін"
27
+ image_not_processable: "не є допустимим зображенням"
@@ -24,3 +24,4 @@ vi:
24
24
  aspect_ratio_not_landscape: "phải là ảnh ngang"
25
25
  aspect_ratio_is_not: "phải có tỉ lệ ảnh %{aspect_ratio}"
26
26
  aspect_ratio_unknown: "tỉ lệ ảnh không xác định"
27
+ image_not_processable: "không phải là ảnh"
@@ -24,3 +24,4 @@ zh-CN:
24
24
  aspect_ratio_not_landscape: "必须是横屏图片"
25
25
  aspect_ratio_is_not: "纵横比必须是 %{aspect_ratio}"
26
26
  aspect_ratio_unknown: "未知的纵横比"
27
+ image_not_processable: "不是有效的图像"
@@ -1,11 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'concerns/symbolizable.rb'
3
4
  require_relative 'metadata.rb'
4
5
 
5
6
  module ActiveStorageValidations
6
7
  class AspectRatioValidator < ActiveModel::EachValidator # :nodoc
7
8
  include OptionProcUnfolding
8
9
  include ErrorHandler
10
+ include Symbolizable
9
11
 
10
12
  AVAILABLE_CHECKS = %i[with].freeze
11
13
  PRECISION = 3
@@ -1,15 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'concerns/symbolizable.rb'
4
+
3
5
  module ActiveStorageValidations
4
6
  class AttachedValidator < ActiveModel::EachValidator # :nodoc:
5
7
  include ErrorHandler
8
+ include Symbolizable
9
+
10
+ ERROR_TYPES = %i[blank].freeze
6
11
 
7
12
  def validate_each(record, attribute, _value)
8
13
  return if record.send(attribute).attached?
9
14
 
10
15
  errors_options = initialize_error_options(options)
11
16
 
12
- add_error(record, attribute, :blank, **errors_options)
17
+ add_error(record, attribute, ERROR_TYPES.first, **errors_options)
13
18
  end
14
19
  end
15
20
  end
@@ -0,0 +1,10 @@
1
+ module Symbolizable
2
+ extend ActiveSupport::Concern
3
+
4
+ class_methods do
5
+ def to_sym
6
+ validator_class = self.name.split("::").last
7
+ validator_class.sub(/Validator/, '').underscore.to_sym
8
+ end
9
+ end
10
+ end
@@ -1,12 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'concerns/symbolizable.rb'
4
+
3
5
  module ActiveStorageValidations
4
6
  class ContentTypeValidator < ActiveModel::EachValidator # :nodoc:
5
7
  include OptionProcUnfolding
6
8
  include ErrorHandler
9
+ include Symbolizable
7
10
 
8
11
  AVAILABLE_CHECKS = %i[with in].freeze
9
-
12
+ ERROR_TYPES = %i[content_type_invalid].freeze
13
+
10
14
  def validate_each(record, attribute, _value)
11
15
  return true unless record.send(attribute).attached?
12
16
 
@@ -22,7 +26,7 @@ module ActiveStorageValidations
22
26
  next if is_valid?(file, types)
23
27
 
24
28
  errors_options[:content_type] = content_type(file)
25
- add_error(record, attribute, :content_type_invalid, **errors_options)
29
+ add_error(record, attribute, ERROR_TYPES.first, **errors_options)
26
30
  break
27
31
  end
28
32
  end
@@ -1,13 +1,28 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'concerns/symbolizable.rb'
3
4
  require_relative 'metadata.rb'
4
5
 
5
6
  module ActiveStorageValidations
6
7
  class DimensionValidator < ActiveModel::EachValidator # :nodoc
7
8
  include OptionProcUnfolding
8
9
  include ErrorHandler
10
+ include Symbolizable
9
11
 
10
12
  AVAILABLE_CHECKS = %i[width height min max].freeze
13
+ ERROR_TYPES = %i[
14
+ image_metadata_missing
15
+ dimension_min_inclusion
16
+ dimension_max_inclusion
17
+ dimension_width_inclusion
18
+ dimension_height_inclusion
19
+ dimension_width_greater_than_or_equal_to
20
+ dimension_height_greater_than_or_equal_to
21
+ dimension_width_less_than_or_equal_to
22
+ dimension_height_less_than_or_equal_to
23
+ dimension_width_equal_to
24
+ dimension_height_equal_to
25
+ ].freeze
11
26
 
12
27
  def process_options(record)
13
28
  flat_options = unfold_procs(record, self.options, AVAILABLE_CHECKS)
@@ -3,15 +3,18 @@ module ActiveStorageValidations
3
3
 
4
4
  def initialize_error_options(options)
5
5
  {
6
- message: (options[:message] if options[:message].present?)
7
- }
6
+ validator_type: self.class.to_sym,
7
+ custom_message: (options[:message] if options[:message].present?)
8
+ }.compact
8
9
  end
9
10
 
10
- def add_error(record, attribute, default_message, **errors_options)
11
- message = errors_options[:message].presence || default_message
12
- return if record.errors.added?(attribute, message)
11
+ def add_error(record, attribute, error_type, **errors_options)
12
+ type = errors_options[:custom_message].presence || error_type
13
+ return if record.errors.added?(attribute, type)
13
14
 
14
- record.errors.add(attribute, message, **errors_options)
15
+ # You can read https://api.rubyonrails.org/classes/ActiveModel/Errors.html#method-i-add
16
+ # to better understand how Rails model errors work
17
+ record.errors.add(attribute, type, **errors_options)
15
18
  end
16
19
 
17
20
  end
@@ -1,9 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'concerns/symbolizable.rb'
4
+
3
5
  module ActiveStorageValidations
4
6
  class LimitValidator < ActiveModel::EachValidator # :nodoc:
5
7
  include OptionProcUnfolding
6
8
  include ErrorHandler
9
+ include Symbolizable
7
10
 
8
11
  AVAILABLE_CHECKS = %i[max min].freeze
9
12
 
@@ -14,7 +17,7 @@ module ActiveStorageValidations
14
17
  end
15
18
 
16
19
  def validate_each(record, attribute, _)
17
- files = Array.wrap(record.send(attribute)).compact.uniq
20
+ files = Array.wrap(record.send(attribute)).reject { |file| file.blank? }.compact.uniq
18
21
  flat_options = unfold_procs(record, self.options, AVAILABLE_CHECKS)
19
22
  errors_options = initialize_error_options(options)
20
23
  errors_options[:min] = flat_options[:min]
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'concerns/validatable.rb'
4
+
3
5
  module ActiveStorageValidations
4
6
  module Matchers
5
7
  def validate_attached_of(name)
@@ -7,6 +9,8 @@ module ActiveStorageValidations
7
9
  end
8
10
 
9
11
  class AttachedValidatorMatcher
12
+ include Validatable
13
+
10
14
  def initialize(attribute_name)
11
15
  @attribute_name = attribute_name
12
16
  @custom_message = nil
@@ -23,7 +27,10 @@ module ActiveStorageValidations
23
27
 
24
28
  def matches?(subject)
25
29
  @subject = subject.is_a?(Class) ? subject.new : subject
26
- responds_to_methods && valid_when_attached && invalid_when_not_attached
30
+ responds_to_methods &&
31
+ is_valid_when_file_attached? &&
32
+ is_invalid_when_file_not_attached? &&
33
+ validate_custom_message?
27
34
  end
28
35
 
29
36
  def failure_message
@@ -42,27 +49,44 @@ module ActiveStorageValidations
42
49
  @subject.public_send(@attribute_name).respond_to?(:detach)
43
50
  end
44
51
 
45
- def valid_when_attached
46
- @subject.public_send(@attribute_name).attach(attachable) unless @subject.public_send(@attribute_name).attached?
47
- @subject.validate
48
- @subject.errors.details[@attribute_name].exclude?(error: error_message)
52
+ def is_valid_when_file_attached?
53
+ attach_dummy_file unless file_attached?
54
+ validate
55
+ is_valid?
49
56
  end
50
57
 
51
- def invalid_when_not_attached
52
- @subject.public_send(@attribute_name).detach
53
- # Unset the direct relation since `detach` on an unpersisted record does not set `attached?` to false.
54
- @subject.public_send("#{@attribute_name}=", nil)
58
+ def is_invalid_when_file_not_attached?
59
+ detach_file if file_attached?
60
+ validate
61
+ !is_valid?
62
+ end
63
+
64
+ def validate_custom_message?
65
+ return true unless @custom_message
55
66
 
56
- @subject.validate
57
- @subject.errors.details[@attribute_name].include?(error: error_message)
67
+ detach_file if file_attached?
68
+ validate
69
+ has_an_error_message_which_is_custom_message?
58
70
  end
59
71
 
60
- def error_message
61
- @custom_message || :blank
72
+ def attach_dummy_file
73
+ dummy_file = {
74
+ io: Tempfile.new('.'),
75
+ filename: 'dummy.txt',
76
+ content_type: 'text/plain'
77
+ }
78
+
79
+ @subject.public_send(@attribute_name).attach(dummy_file)
80
+ end
81
+
82
+ def file_attached?
83
+ @subject.public_send(@attribute_name).attached?
62
84
  end
63
85
 
64
- def attachable
65
- { io: Tempfile.new('.'), filename: 'dummy.txt', content_type: 'text/plain' }
86
+ def detach_file
87
+ @subject.public_send(@attribute_name).detach
88
+ # Unset the direct relation since `detach` on an unpersisted record does not set `attached?` to false.
89
+ @subject.public_send("#{@attribute_name}=", nil)
66
90
  end
67
91
  end
68
92
  end
@@ -0,0 +1,46 @@
1
+ module Validatable
2
+ extend ActiveSupport::Concern
3
+
4
+ private
5
+
6
+ def validate
7
+ @subject.validate
8
+ end
9
+
10
+ def validator_errors_for_attribute
11
+ @subject.errors.details[@attribute_name].select do |error|
12
+ error[:validator_type] == validator_class.to_sym
13
+ end
14
+ end
15
+
16
+ def is_valid?
17
+ validator_errors_for_attribute.none? do |error|
18
+ error[:error].in?(available_errors)
19
+ end
20
+ end
21
+
22
+ def available_errors
23
+ [
24
+ *validator_class::ERROR_TYPES,
25
+ *error_from_custom_message
26
+ ].compact
27
+ end
28
+
29
+ def validator_class
30
+ self.class.name.gsub(/::Matchers|Matcher/, '').constantize
31
+ end
32
+
33
+ def error_from_custom_message
34
+ associated_validation = @subject.class.validators_on(@attribute_name).find do |validator|
35
+ validator.class == validator_class
36
+ end
37
+
38
+ associated_validation.options[:message]
39
+ end
40
+
41
+ def has_an_error_message_which_is_custom_message?
42
+ validator_errors_for_attribute.one? do |error|
43
+ error[:error] == @custom_message
44
+ end
45
+ end
46
+ end
@@ -2,6 +2,9 @@
2
2
 
3
3
  # Big thank you to the paperclip validation matchers:
4
4
  # https://github.com/thoughtbot/paperclip/blob/v6.1.0/lib/paperclip/matchers/validate_attachment_content_type_matcher.rb
5
+
6
+ require_relative 'concerns/validatable.rb'
7
+
5
8
  module ActiveStorageValidations
6
9
  module Matchers
7
10
  def validate_content_type_of(name)
@@ -9,8 +12,11 @@ module ActiveStorageValidations
9
12
  end
10
13
 
11
14
  class ContentTypeValidatorMatcher
15
+ include Validatable
16
+
12
17
  def initialize(attribute_name)
13
18
  @attribute_name = attribute_name
19
+ @allowed_types = @rejected_types = []
14
20
  @custom_message = nil
15
21
  end
16
22
 
@@ -35,20 +41,24 @@ module ActiveStorageValidations
35
41
 
36
42
  def matches?(subject)
37
43
  @subject = subject.is_a?(Class) ? subject.new : subject
38
- responds_to_methods && allowed_types_allowed? && rejected_types_rejected? && validate_custom_message?
44
+
45
+ responds_to_methods &&
46
+ all_allowed_types_allowed? &&
47
+ all_rejected_types_rejected? &&
48
+ validate_custom_message?
39
49
  end
40
50
 
41
51
  def failure_message
42
52
  message = ["Expected #{@attribute_name}"]
43
53
 
44
- if @allowed_types
45
- message << "Accept content types: #{allowed_types.join(", ")}"
46
- message << "#{@missing_allowed_types.join(", ")} were rejected"
54
+ if @allowed_types_not_allowed.present?
55
+ message << "Accept content types: #{@allowed_types.join(", ")}"
56
+ message << "#{@allowed_types_not_allowed.join(", ")} were rejected"
47
57
  end
48
58
 
49
- if @rejected_types
50
- message << "Reject content types: #{rejected_types.join(", ")}"
51
- message << "#{@missing_rejected_types.join(", ")} were accepted"
59
+ if @rejected_types_not_rejected.present?
60
+ message << "Reject content types: #{@rejected_types.join(", ")}"
61
+ message << "#{@rejected_types_not_rejected.join(", ")} were accepted"
52
62
  end
53
63
 
54
64
  message.join("\n")
@@ -62,49 +72,46 @@ module ActiveStorageValidations
62
72
  @subject.public_send(@attribute_name).respond_to?(:detach)
63
73
  end
64
74
 
65
- def allowed_types
66
- @allowed_types || []
75
+ def all_allowed_types_allowed?
76
+ @allowed_types_not_allowed ||= @allowed_types.reject { |type| type_allowed?(type) }
77
+ @allowed_types_not_allowed.empty?
67
78
  end
68
79
 
69
- def rejected_types
70
- @rejected_types || []
80
+ def all_rejected_types_rejected?
81
+ @rejected_types_not_rejected ||= @rejected_types.select { |type| type_allowed?(type) }
82
+ @rejected_types_not_rejected.empty?
71
83
  end
72
84
 
73
- def allowed_types_allowed?
74
- @missing_allowed_types ||= allowed_types.reject { |type| type_allowed?(type) }
75
- @missing_allowed_types.none?
76
- end
77
-
78
- def rejected_types_rejected?
79
- @missing_rejected_types ||= rejected_types.select { |type| type_allowed?(type) }
80
- @missing_rejected_types.none?
85
+ def type_allowed?(type)
86
+ attach_file_of_type(type)
87
+ validate
88
+ is_valid?
81
89
  end
82
90
 
83
- def type_allowed?(type)
91
+ def attach_file_of_type(type)
84
92
  @subject.public_send(@attribute_name).attach(attachment_for(type))
85
- @subject.validate
86
- @subject.errors.details[@attribute_name].none? do |error|
87
- error[:error].to_s.include?(error_message)
88
- end
89
93
  end
90
94
 
91
95
  def validate_custom_message?
92
96
  return true unless @custom_message
93
97
 
94
- @subject.public_send(@attribute_name).attach(attachment_for('fake/fake'))
95
- @subject.validate
96
- @subject.errors.details[@attribute_name].select{|error| error[:content_type]}.all? do |error|
97
- error[:error].to_s.include?(error_message)
98
- end
98
+ attach_invalid_content_type_file
99
+ validate
100
+ has_an_error_message_which_is_custom_message?
99
101
  end
100
102
 
101
- def error_message
102
- @custom_message || :content_type_invalid.to_s
103
+ def attach_invalid_content_type_file
104
+ @subject.public_send(@attribute_name).attach(attachment_for('fake/fake'))
103
105
  end
104
106
 
105
107
  def attachment_for(type)
106
108
  suffix = type.to_s.split('/').last
107
- { io: Tempfile.new('.'), filename: "test.#{suffix}", content_type: type }
109
+
110
+ {
111
+ io: Tempfile.new('.'),
112
+ filename: "test.#{suffix}",
113
+ content_type: type
114
+ }
108
115
  end
109
116
  end
110
117
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'concerns/validatable.rb'
4
+
3
5
  module ActiveStorageValidations
4
6
  module Matchers
5
7
  def validate_dimensions_of(name)
@@ -7,6 +9,8 @@ module ActiveStorageValidations
7
9
  end
8
10
 
9
11
  class DimensionValidatorMatcher
12
+ include Validatable
13
+
10
14
  def initialize(attribute_name)
11
15
  @attribute_name = attribute_name
12
16
  @width_min = @width_max = @height_min = @height_max = nil
@@ -64,9 +68,19 @@ module ActiveStorageValidations
64
68
 
65
69
  def matches?(subject)
66
70
  @subject = subject.is_a?(Class) ? subject.new : subject
71
+
67
72
  responds_to_methods &&
68
- width_not_smaller_than_min? && width_larger_than_min? && width_smaller_than_max? && width_not_larger_than_max? && width_equals? &&
69
- height_not_smaller_than_min? && height_larger_than_min? && height_smaller_than_max? && height_not_larger_than_max? && height_equals?
73
+ width_not_smaller_than_min? &&
74
+ width_larger_than_min? &&
75
+ width_smaller_than_max? &&
76
+ width_not_larger_than_max? &&
77
+ width_equals? &&
78
+ height_not_smaller_than_min? &&
79
+ height_larger_than_min? &&
80
+ height_smaller_than_max? &&
81
+ height_not_larger_than_max? &&
82
+ height_equals? &&
83
+ validate_custom_message?
70
84
  end
71
85
 
72
86
  def failure_message
@@ -134,22 +148,38 @@ module ActiveStorageValidations
134
148
  end
135
149
 
136
150
  def passes_validation_with_dimensions(width, height, check)
137
- @subject.public_send(@attribute_name).attach attachment_for(width, height)
151
+ mock_dimensions_for(attach_file, width, height) do
152
+ validate
153
+ is_valid?
154
+ end
155
+ end
156
+
157
+ def validate_custom_message?
158
+ return true unless @custom_message
138
159
 
139
- attachment = @subject.public_send(@attribute_name)
160
+ mock_dimensions_for(attach_file, -1, -1) do
161
+ validate
162
+ has_an_error_message_which_is_custom_message?
163
+ end
164
+ end
165
+
166
+ def mock_dimensions_for(attachment, width, height)
140
167
  Matchers.mock_metadata(attachment, width, height) do
141
- @subject.validate
142
- exclude_error_message = @custom_message || "dimension_#{check}"
143
- @subject.errors.details[@attribute_name].none? do |error|
144
- error[:error].to_s.include?(exclude_error_message) ||
145
- error[:error].to_s.include?("dimension_min") ||
146
- error[:error].to_s.include?("dimension_max")
147
- end
168
+ yield
148
169
  end
149
170
  end
150
171
 
151
- def attachment_for(width, height)
152
- { io: Tempfile.new('Hello world!'), filename: 'test.png', content_type: 'image/png' }
172
+ def attach_file
173
+ @subject.public_send(@attribute_name).attach(dummy_file)
174
+ @subject.public_send(@attribute_name)
175
+ end
176
+
177
+ def dummy_file
178
+ {
179
+ io: Tempfile.new('Hello world!'),
180
+ filename: 'test.png',
181
+ content_type: 'image/png'
182
+ }
153
183
  end
154
184
  end
155
185
  end
@@ -2,6 +2,9 @@
2
2
 
3
3
  # Big thank you to the paperclip validation matchers:
4
4
  # https://github.com/thoughtbot/paperclip/blob/v6.1.0/lib/paperclip/matchers/validate_attachment_size_matcher.rb
5
+
6
+ require_relative 'concerns/validatable.rb'
7
+
5
8
  module ActiveStorageValidations
6
9
  module Matchers
7
10
  def validate_size_of(name)
@@ -9,6 +12,8 @@ module ActiveStorageValidations
9
12
  end
10
13
 
11
14
  class SizeValidatorMatcher
15
+ include Validatable
16
+
12
17
  def initialize(attribute_name)
13
18
  @attribute_name = attribute_name
14
19
  @min = @max = nil
@@ -51,7 +56,13 @@ module ActiveStorageValidations
51
56
 
52
57
  def matches?(subject)
53
58
  @subject = subject.is_a?(Class) ? subject.new : subject
54
- responds_to_methods && not_lower_than_min? && higher_than_min? && lower_than_max? && not_higher_than_max?
59
+
60
+ responds_to_methods &&
61
+ not_lower_than_min? &&
62
+ higher_than_min? &&
63
+ lower_than_max? &&
64
+ not_higher_than_max? &&
65
+ validate_custom_message?
55
66
  end
56
67
 
57
68
  def failure_message
@@ -86,17 +97,45 @@ module ActiveStorageValidations
86
97
  @max.nil? || @max == Float::INFINITY || !passes_validation_with_size(@max + 1)
87
98
  end
88
99
 
89
- def passes_validation_with_size(new_size)
90
- io = Tempfile.new('Hello world!')
91
- Matchers.stub_method(io, :size, new_size) do
92
- @subject.public_send(@attribute_name).attach(io: io, filename: 'test.png', content_type: 'image/pg')
93
- @subject.validate
94
- exclude_error_message = @custom_message || "file_size_not_"
95
- @subject.errors.details[@attribute_name].none? do |error|
96
- error[:error].to_s.include?(exclude_error_message)
97
- end
100
+ def passes_validation_with_size(size)
101
+ mock_size_for(io, size) do
102
+ attach_file
103
+ validate
104
+ is_valid?
98
105
  end
99
106
  end
107
+
108
+ def validate_custom_message?
109
+ return true unless @custom_message
110
+
111
+ mock_size_for(io, -1.kilobytes) do
112
+ attach_file
113
+ validate
114
+ has_an_error_message_which_is_custom_message?
115
+ end
116
+ end
117
+
118
+ def mock_size_for(io, size)
119
+ Matchers.stub_method(io, :size, size) do
120
+ yield
121
+ end
122
+ end
123
+
124
+ def attach_file
125
+ @subject.public_send(@attribute_name).attach(dummy_file)
126
+ end
127
+
128
+ def dummy_file
129
+ {
130
+ io: io,
131
+ filename: 'test.png',
132
+ content_type: 'image/png'
133
+ }
134
+ end
135
+
136
+ def io
137
+ @io ||= Tempfile.new('Hello world!')
138
+ end
100
139
  end
101
140
  end
102
141
  end
@@ -1,11 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'concerns/symbolizable.rb'
3
4
  require_relative 'metadata.rb'
4
5
 
5
6
  module ActiveStorageValidations
6
7
  class ProcessableImageValidator < ActiveModel::EachValidator # :nodoc
7
8
  include OptionProcUnfolding
8
9
  include ErrorHandler
10
+ include Symbolizable
9
11
 
10
12
  if Rails.gem_version >= Gem::Version.new('6.0.0')
11
13
  def validate_each(record, attribute, _value)
@@ -1,13 +1,29 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'concerns/symbolizable.rb'
4
+
3
5
  module ActiveStorageValidations
4
6
  class SizeValidator < ActiveModel::EachValidator # :nodoc:
5
7
  include OptionProcUnfolding
6
8
  include ErrorHandler
9
+ include Symbolizable
7
10
 
8
11
  delegate :number_to_human_size, to: ActiveSupport::NumberHelper
9
12
 
10
- AVAILABLE_CHECKS = %i[less_than less_than_or_equal_to greater_than greater_than_or_equal_to between].freeze
13
+ AVAILABLE_CHECKS = %i[
14
+ less_than
15
+ less_than_or_equal_to
16
+ greater_than
17
+ greater_than_or_equal_to
18
+ between
19
+ ].freeze
20
+ ERROR_TYPES = %i[
21
+ file_size_not_less_than
22
+ file_size_not_less_than_or_equal_to
23
+ file_size_not_greater_than
24
+ file_size_not_greater_than_or_equal_to
25
+ file_size_not_between
26
+ ].freeze
11
27
 
12
28
  def check_validity!
13
29
  unless AVAILABLE_CHECKS.one? { |argument| options.key?(argument) }
@@ -31,7 +47,8 @@ module ActiveStorageValidations
31
47
  errors_options[:file_size] = number_to_human_size(file.blob.byte_size)
32
48
  errors_options[:min_size] = number_to_human_size(min_size(flat_options))
33
49
  errors_options[:max_size] = number_to_human_size(max_size(flat_options))
34
- error_type = "file_size_not_#{flat_options.keys.first}".to_sym
50
+ keys = AVAILABLE_CHECKS & flat_options.keys
51
+ error_type = "file_size_not_#{keys.first}".to_sym
35
52
 
36
53
  add_error(record, attribute, error_type, **errors_options)
37
54
  break
@@ -39,6 +56,8 @@ module ActiveStorageValidations
39
56
  end
40
57
 
41
58
  def content_size_valid?(file_size, flat_options)
59
+ return false if file_size < 0
60
+
42
61
  if flat_options[:between].present?
43
62
  flat_options[:between].include?(file_size)
44
63
  elsif flat_options[:less_than].present?
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveStorageValidations
4
- VERSION = '1.1.1'
4
+ VERSION = '1.1.2'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_storage_validations
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.1
4
+ version: 1.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Igor Kasyanchuk
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-10-26 00:00:00.000000000 Z
11
+ date: 2023-11-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activejob
@@ -66,6 +66,20 @@ dependencies:
66
66
  - - ">="
67
67
  - !ruby/object:Gem::Version
68
68
  version: 5.2.0
69
+ - !ruby/object:Gem::Dependency
70
+ name: minitest-focus
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '1.4'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '1.4'
69
83
  - !ruby/object:Gem::Dependency
70
84
  name: combustion
71
85
  requirement: !ruby/object:Gem::Requirement
@@ -220,6 +234,7 @@ files:
220
234
  - lib/active_storage_validations.rb
221
235
  - lib/active_storage_validations/aspect_ratio_validator.rb
222
236
  - lib/active_storage_validations/attached_validator.rb
237
+ - lib/active_storage_validations/concerns/symbolizable.rb
223
238
  - lib/active_storage_validations/content_type_validator.rb
224
239
  - lib/active_storage_validations/dimension_validator.rb
225
240
  - lib/active_storage_validations/engine.rb
@@ -227,6 +242,7 @@ files:
227
242
  - lib/active_storage_validations/limit_validator.rb
228
243
  - lib/active_storage_validations/matchers.rb
229
244
  - lib/active_storage_validations/matchers/attached_validator_matcher.rb
245
+ - lib/active_storage_validations/matchers/concerns/validatable.rb
230
246
  - lib/active_storage_validations/matchers/content_type_validator_matcher.rb
231
247
  - lib/active_storage_validations/matchers/dimension_validator_matcher.rb
232
248
  - lib/active_storage_validations/matchers/size_validator_matcher.rb
@@ -240,7 +256,8 @@ files:
240
256
  homepage: https://github.com/igorkasyanchuk/active_storage_validations
241
257
  licenses:
242
258
  - MIT
243
- metadata: {}
259
+ metadata:
260
+ rubygems_mfa_required: 'true'
244
261
  post_install_message:
245
262
  rdoc_options: []
246
263
  require_paths: