active_storage_validations 1.1.3 → 1.1.4

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.
Files changed (28) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +81 -55
  3. data/lib/active_storage_validations/aspect_ratio_validator.rb +47 -22
  4. data/lib/active_storage_validations/attached_validator.rb +12 -3
  5. data/lib/active_storage_validations/concerns/errorable.rb +38 -0
  6. data/lib/active_storage_validations/concerns/symbolizable.rb +8 -6
  7. data/lib/active_storage_validations/content_type_validator.rb +41 -6
  8. data/lib/active_storage_validations/dimension_validator.rb +15 -15
  9. data/lib/active_storage_validations/limit_validator.rb +44 -7
  10. data/lib/active_storage_validations/matchers/aspect_ratio_validator_matcher.rb +128 -0
  11. data/lib/active_storage_validations/matchers/attached_validator_matcher.rb +20 -23
  12. data/lib/active_storage_validations/matchers/concerns/active_storageable.rb +17 -0
  13. data/lib/active_storage_validations/matchers/concerns/allow_blankable.rb +26 -0
  14. data/lib/active_storage_validations/matchers/concerns/contextable.rb +35 -0
  15. data/lib/active_storage_validations/matchers/concerns/messageable.rb +26 -0
  16. data/lib/active_storage_validations/matchers/concerns/rspecable.rb +25 -0
  17. data/lib/active_storage_validations/matchers/concerns/validatable.rb +5 -10
  18. data/lib/active_storage_validations/matchers/content_type_validator_matcher.rb +39 -25
  19. data/lib/active_storage_validations/matchers/dimension_validator_matcher.rb +61 -44
  20. data/lib/active_storage_validations/matchers/size_validator_matcher.rb +41 -24
  21. data/lib/active_storage_validations/matchers.rb +1 -0
  22. data/lib/active_storage_validations/metadata.rb +42 -28
  23. data/lib/active_storage_validations/processable_image_validator.rb +14 -5
  24. data/lib/active_storage_validations/size_validator.rb +7 -6
  25. data/lib/active_storage_validations/version.rb +1 -1
  26. data/lib/active_storage_validations.rb +1 -1
  27. metadata +9 -3
  28. data/lib/active_storage_validations/error_handler.rb +0 -21
@@ -1,32 +1,38 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'concerns/errorable.rb'
3
4
  require_relative 'concerns/symbolizable.rb'
4
5
 
5
6
  module ActiveStorageValidations
6
7
  class LimitValidator < ActiveModel::EachValidator # :nodoc:
7
8
  include OptionProcUnfolding
8
- include ErrorHandler
9
+ include Errorable
9
10
  include Symbolizable
10
11
 
11
12
  AVAILABLE_CHECKS = %i[max min].freeze
13
+ ERROR_TYPES = %i[
14
+ limit_out_of_range
15
+ ].freeze
12
16
 
13
17
  def check_validity!
14
- unless AVAILABLE_CHECKS.any? { |argument| options.key?(argument) }
15
- raise ArgumentError, 'You must pass either :max or :min to the validator'
16
- end
18
+ ensure_at_least_one_validator_option
19
+ ensure_arguments_validity
17
20
  end
18
21
 
19
22
  def validate_each(record, attribute, _)
20
23
  files = Array.wrap(record.send(attribute)).reject { |file| file.blank? }.compact.uniq
21
24
  flat_options = unfold_procs(record, self.options, AVAILABLE_CHECKS)
25
+
26
+ return true if files_count_valid?(files.count, flat_options)
27
+
22
28
  errors_options = initialize_error_options(options)
23
29
  errors_options[:min] = flat_options[:min]
24
30
  errors_options[:max] = flat_options[:max]
25
-
26
- return true if files_count_valid?(files.count, flat_options)
27
- add_error(record, attribute, :limit_out_of_range, **errors_options)
31
+ add_error(record, attribute, ERROR_TYPES.first, **errors_options)
28
32
  end
29
33
 
34
+ private
35
+
30
36
  def files_count_valid?(count, flat_options)
31
37
  if flat_options[:max].present? && flat_options[:min].present?
32
38
  count >= flat_options[:min] && count <= flat_options[:max]
@@ -36,5 +42,36 @@ module ActiveStorageValidations
36
42
  count >= flat_options[:min]
37
43
  end
38
44
  end
45
+
46
+ def ensure_at_least_one_validator_option
47
+ unless AVAILABLE_CHECKS.any? { |argument| options.key?(argument) }
48
+ raise ArgumentError, 'You must pass either :max or :min to the validator'
49
+ end
50
+ end
51
+
52
+ def ensure_arguments_validity
53
+ return true if min_max_are_proc? || min_or_max_is_proc_and_other_not_present?
54
+
55
+ raise ArgumentError, 'You must pass integers to :min and :max' if min_or_max_defined_and_not_integer?
56
+ raise ArgumentError, 'You must pass a higher value to :max than to :min' if min_higher_than_max?
57
+ end
58
+
59
+ def min_max_are_proc?
60
+ options[:min]&.is_a?(Proc) && options[:max]&.is_a?(Proc)
61
+ end
62
+
63
+ def min_or_max_is_proc_and_other_not_present?
64
+ (options[:min]&.is_a?(Proc) && options[:max].nil?) ||
65
+ (options[:min].nil? && options[:max]&.is_a?(Proc))
66
+ end
67
+
68
+ def min_or_max_defined_and_not_integer?
69
+ (options.key?(:min) && !options[:min].is_a?(Integer)) ||
70
+ (options.key?(:max) && !options[:max].is_a?(Integer))
71
+ end
72
+
73
+ def min_higher_than_max?
74
+ options[:min] > options[:max] if options[:min].is_a?(Integer) && options[:max].is_a?(Integer)
75
+ end
39
76
  end
40
77
  end
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'concerns/active_storageable.rb'
4
+ require_relative 'concerns/allow_blankable.rb'
5
+ require_relative 'concerns/contextable.rb'
6
+ require_relative 'concerns/messageable.rb'
7
+ require_relative 'concerns/rspecable.rb'
8
+ require_relative 'concerns/validatable.rb'
9
+
10
+ module ActiveStorageValidations
11
+ module Matchers
12
+ def validate_aspect_ratio_of(name, expected_aspect_ratio)
13
+ AspectRatioValidatorMatcher.new(name, expected_aspect_ratio)
14
+ end
15
+
16
+ class AspectRatioValidatorMatcher
17
+ include ActiveStorageable
18
+ include AllowBlankable
19
+ include Contextable
20
+ include Messageable
21
+ include Rspecable
22
+ include Validatable
23
+
24
+ def initialize(attribute_name)
25
+ initialize_allow_blankable
26
+ initialize_contextable
27
+ initialize_messageable
28
+ initialize_rspecable
29
+ @attribute_name = attribute_name
30
+ @allowed_aspect_ratios = @rejected_aspect_ratios = []
31
+ end
32
+
33
+ def description
34
+ "validate the aspect ratios allowed on :#{@attribute_name}."
35
+ end
36
+
37
+ def failure_message
38
+ "is expected to validate aspect ratio of :#{@attribute_name}"
39
+ end
40
+
41
+ def allowing(*aspect_ratios)
42
+ @allowed_aspect_ratios = aspect_ratios.flatten
43
+ self
44
+ end
45
+
46
+ def rejecting(*aspect_ratios)
47
+ @rejected_aspect_ratios = aspect_ratios.flatten
48
+ self
49
+ end
50
+
51
+ def matches?(subject)
52
+ @subject = subject.is_a?(Class) ? subject.new : subject
53
+
54
+ is_a_valid_active_storage_attribute? &&
55
+ is_context_valid? &&
56
+ is_allowing_blank? &&
57
+ is_custom_message_valid? &&
58
+ all_allowed_aspect_ratios_allowed? &&
59
+ all_rejected_aspect_ratios_rejected?
60
+ end
61
+
62
+ protected
63
+
64
+ def all_allowed_aspect_ratios_allowed?
65
+ @allowed_aspect_ratios_not_allowed ||= @allowed_aspect_ratios.reject { |aspect_ratio| aspect_ratio_allowed?(aspect_ratio) }
66
+ @allowed_aspect_ratios_not_allowed.empty?
67
+ end
68
+
69
+ def all_rejected_aspect_ratios_rejected?
70
+ @rejected_aspect_ratios_not_rejected ||= @rejected_aspect_ratios.select { |aspect_ratio| aspect_ratio_allowed?(aspect_ratio) }
71
+ @rejected_aspect_ratios_not_rejected.empty?
72
+ end
73
+
74
+ def aspect_ratio_allowed?(aspect_ratio)
75
+ width, height = valid_width_and_height_for(aspect_ratio)
76
+
77
+ mock_dimensions_for(attach_file, width, height) do
78
+ validate
79
+ is_valid?
80
+ end
81
+ end
82
+
83
+ def is_custom_message_valid?
84
+ return true unless @custom_message
85
+
86
+ mock_dimensions_for(attach_file, -1, -1) do
87
+ validate
88
+ has_an_error_message_which_is_custom_message?
89
+ end
90
+ end
91
+
92
+ def attach_file
93
+ @subject.public_send(@attribute_name).attach(dummy_file)
94
+ @subject.public_send(@attribute_name)
95
+ end
96
+
97
+ def dummy_file
98
+ {
99
+ io: Tempfile.new('Hello world!'),
100
+ filename: 'test.png',
101
+ content_type: 'image/png'
102
+ }
103
+ end
104
+
105
+ def mock_dimensions_for(attachment, width, height)
106
+ Matchers.mock_metadata(attachment, width, height) do
107
+ yield
108
+ end
109
+ end
110
+
111
+ def valid_width_and_height_for(aspect_ratio)
112
+ case aspect_ratio
113
+ when :square then [100, 100]
114
+ when :portrait then [100, 200]
115
+ when :landscape then [200, 100]
116
+ when validator_class::ASPECT_RATIO_REGEX
117
+ aspect_ratio =~ validator_class::ASPECT_RATIO_REGEX
118
+ x = Regexp.last_match(1).to_i
119
+ y = Regexp.last_match(2).to_i
120
+
121
+ [100 * x, 100 * y]
122
+ else
123
+ [-1, -1]
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
@@ -1,5 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'concerns/active_storageable.rb'
4
+ require_relative 'concerns/contextable.rb'
5
+ require_relative 'concerns/messageable.rb'
6
+ require_relative 'concerns/rspecable.rb'
3
7
  require_relative 'concerns/validatable.rb'
4
8
 
5
9
  module ActiveStorageValidations
@@ -9,46 +13,39 @@ module ActiveStorageValidations
9
13
  end
10
14
 
11
15
  class AttachedValidatorMatcher
16
+ include ActiveStorageable
17
+ include Contextable
18
+ include Messageable
19
+ include Rspecable
12
20
  include Validatable
13
21
 
14
22
  def initialize(attribute_name)
23
+ initialize_contextable
24
+ initialize_messageable
25
+ initialize_rspecable
15
26
  @attribute_name = attribute_name
16
- @custom_message = nil
17
27
  end
18
28
 
19
29
  def description
20
- "validate #{@attribute_name} must be attached"
30
+ "validate that :#{@attribute_name} must be attached"
21
31
  end
22
32
 
23
- def with_message(message)
24
- @custom_message = message
25
- self
33
+ def failure_message
34
+ "is expected to validate attachment of :#{@attribute_name}"
26
35
  end
27
36
 
28
37
  def matches?(subject)
29
38
  @subject = subject.is_a?(Class) ? subject.new : subject
30
- responds_to_methods &&
31
- is_valid_when_file_attached? &&
32
- is_invalid_when_file_not_attached? &&
33
- validate_custom_message?
34
- end
35
39
 
36
- def failure_message
37
- "is expected to validate attached of #{@attribute_name}"
38
- end
39
-
40
- def failure_message_when_negated
41
- "is expected to not validate attached of #{@attribute_name}"
40
+ is_a_valid_active_storage_attribute? &&
41
+ is_context_valid? &&
42
+ is_custom_message_valid? &&
43
+ is_valid_when_file_attached? &&
44
+ is_invalid_when_file_not_attached?
42
45
  end
43
46
 
44
47
  private
45
48
 
46
- def responds_to_methods
47
- @subject.respond_to?(@attribute_name) &&
48
- @subject.public_send(@attribute_name).respond_to?(:attach) &&
49
- @subject.public_send(@attribute_name).respond_to?(:detach)
50
- end
51
-
52
49
  def is_valid_when_file_attached?
53
50
  attach_dummy_file unless file_attached?
54
51
  validate
@@ -61,7 +58,7 @@ module ActiveStorageValidations
61
58
  !is_valid?
62
59
  end
63
60
 
64
- def validate_custom_message?
61
+ def is_custom_message_valid?
65
62
  return true unless @custom_message
66
63
 
67
64
  detach_file if file_attached?
@@ -0,0 +1,17 @@
1
+ require "active_support/concern"
2
+
3
+ module ActiveStorageValidations
4
+ module Matchers
5
+ module ActiveStorageable
6
+ extend ActiveSupport::Concern
7
+
8
+ private
9
+
10
+ def is_a_valid_active_storage_attribute?
11
+ @subject.respond_to?(@attribute_name) &&
12
+ @subject.public_send(@attribute_name).respond_to?(:attach) &&
13
+ @subject.public_send(@attribute_name).respond_to?(:detach)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,26 @@
1
+ require "active_support/concern"
2
+
3
+ module ActiveStorageValidations
4
+ module Matchers
5
+ module AllowBlankable
6
+ extend ActiveSupport::Concern
7
+
8
+ def initialize_allow_blankable
9
+ @allow_blank = nil
10
+ end
11
+
12
+ def allow_blank
13
+ @allow_blank = true
14
+ self
15
+ end
16
+
17
+ private
18
+
19
+ def is_allowing_blank?
20
+ return true unless @allow_blank
21
+
22
+ validate
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,35 @@
1
+ require "active_support/concern"
2
+
3
+ module ActiveStorageValidations
4
+ module Matchers
5
+ module Contextable
6
+ extend ActiveSupport::Concern
7
+
8
+ def initialize_contextable
9
+ @context = nil
10
+ end
11
+
12
+ def on(context)
13
+ @context = context
14
+ self
15
+ end
16
+
17
+ private
18
+
19
+ def is_context_valid?
20
+ return true if !@context && !(attribute_validator && attribute_validator.options[:on])
21
+
22
+ raise ArgumentError, "This validator matcher needs the #on option to work since its validator has one" if !@context
23
+ raise ArgumentError, "This validator matcher option only allows a symbol or an array" if !(@context.is_a?(Symbol) || @context.is_a?(Array))
24
+
25
+ if @context.is_a?(Array) && attribute_validator.options[:on].is_a?(Array)
26
+ @context.to_set == attribute_validator.options[:on].to_set
27
+ elsif @context.is_a?(Symbol) && attribute_validator.options[:on].is_a?(Symbol)
28
+ @context == attribute_validator.options[:on]
29
+ else
30
+ false
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,26 @@
1
+ require "active_support/concern"
2
+
3
+ module ActiveStorageValidations
4
+ module Matchers
5
+ module Messageable
6
+ extend ActiveSupport::Concern
7
+
8
+ def initialize_messageable
9
+ @custom_message = nil
10
+ end
11
+
12
+ def with_message(custom_message)
13
+ @custom_message = custom_message
14
+ self
15
+ end
16
+
17
+ private
18
+
19
+ def has_an_error_message_which_is_custom_message?
20
+ validator_errors_for_attribute.one? do |error|
21
+ error[:error] == @custom_message
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,25 @@
1
+ require "active_support/concern"
2
+
3
+ module ActiveStorageValidations
4
+ module Matchers
5
+ module Rspecable
6
+ extend ActiveSupport::Concern
7
+
8
+ def initialize_rspecable
9
+ @failure_message_artefacts = []
10
+ end
11
+
12
+ def description
13
+ raise NotImplementedError, "#{self.class} did not define #{__method__}"
14
+ end
15
+
16
+ def failure_message
17
+ raise NotImplementedError, "#{self.class} did not define #{__method__}"
18
+ end
19
+
20
+ def failure_message_when_negated
21
+ failure_message.sub(/is expected to validate/, 'is expected not to validate')
22
+ end
23
+ end
24
+ end
25
+ end
@@ -1,4 +1,3 @@
1
-
2
1
  require "active_support/concern"
3
2
 
4
3
  module ActiveStorageValidations
@@ -9,7 +8,7 @@ module ActiveStorageValidations
9
8
  private
10
9
 
11
10
  def validate
12
- @subject.validate
11
+ @subject.validate(@context)
13
12
  end
14
13
 
15
14
  def validator_errors_for_attribute
@@ -35,18 +34,14 @@ module ActiveStorageValidations
35
34
  self.class.name.gsub(/::Matchers|Matcher/, '').constantize
36
35
  end
37
36
 
38
- def error_from_custom_message
39
- associated_validation = @subject.class.validators_on(@attribute_name).find do |validator|
37
+ def attribute_validator
38
+ @subject.class.validators_on(@attribute_name).find do |validator|
40
39
  validator.class == validator_class
41
40
  end
42
-
43
- associated_validation.options[:message]
44
41
  end
45
42
 
46
- def has_an_error_message_which_is_custom_message?
47
- validator_errors_for_attribute.one? do |error|
48
- error[:error] == @custom_message
49
- end
43
+ def error_from_custom_message
44
+ attribute_validator.options[:message]
50
45
  end
51
46
  end
52
47
  end
@@ -3,6 +3,11 @@
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
5
 
6
+ require_relative 'concerns/active_storageable.rb'
7
+ require_relative 'concerns/allow_blankable.rb'
8
+ require_relative 'concerns/contextable.rb'
9
+ require_relative 'concerns/messageable.rb'
10
+ require_relative 'concerns/rspecable.rb'
6
11
  require_relative 'concerns/validatable.rb'
7
12
 
8
13
  module ActiveStorageValidations
@@ -12,16 +17,30 @@ module ActiveStorageValidations
12
17
  end
13
18
 
14
19
  class ContentTypeValidatorMatcher
20
+ include ActiveStorageable
21
+ include AllowBlankable
22
+ include Contextable
23
+ include Messageable
24
+ include Rspecable
15
25
  include Validatable
16
26
 
17
27
  def initialize(attribute_name)
28
+ initialize_allow_blankable
29
+ initialize_contextable
30
+ initialize_messageable
31
+ initialize_rspecable
18
32
  @attribute_name = attribute_name
19
33
  @allowed_types = @rejected_types = []
20
- @custom_message = nil
21
34
  end
22
35
 
23
36
  def description
24
- "validate the content types allowed on attachment #{@attribute_name}"
37
+ "validate the content types allowed on :#{@attribute_name}"
38
+ end
39
+
40
+ def failure_message
41
+ message = ["is expected to validate the content types of :#{@attribute_name}"]
42
+ build_failure_message(message)
43
+ message.join("\n")
25
44
  end
26
45
 
27
46
  def allowing(*types)
@@ -34,42 +53,37 @@ module ActiveStorageValidations
34
53
  self
35
54
  end
36
55
 
37
- def with_message(message)
38
- @custom_message = message
39
- self
40
- end
41
-
42
56
  def matches?(subject)
43
57
  @subject = subject.is_a?(Class) ? subject.new : subject
44
58
 
45
- responds_to_methods &&
59
+ is_a_valid_active_storage_attribute? &&
60
+ is_context_valid? &&
61
+ is_allowing_blank? &&
62
+ is_custom_message_valid? &&
46
63
  all_allowed_types_allowed? &&
47
- all_rejected_types_rejected? &&
48
- validate_custom_message?
64
+ all_rejected_types_rejected?
49
65
  end
50
66
 
51
- def failure_message
52
- message = ["Expected #{@attribute_name}"]
67
+ protected
53
68
 
69
+ def build_failure_message(message)
54
70
  if @allowed_types_not_allowed.present?
55
- message << "Accept content types: #{@allowed_types.join(", ")}"
56
- message << "#{@allowed_types_not_allowed.join(", ")} were rejected"
71
+ message << " the following content type#{'s' if @allowed_types.count > 1} should be allowed: :#{@allowed_types.join(", :")}"
72
+ message << " but #{pluralize(@allowed_types_not_allowed)} rejected"
57
73
  end
58
74
 
59
75
  if @rejected_types_not_rejected.present?
60
- message << "Reject content types: #{@rejected_types.join(", ")}"
61
- message << "#{@rejected_types_not_rejected.join(", ")} were accepted"
76
+ message << " the following content type#{'s' if @rejected_types.count > 1} should be rejected: :#{@rejected_types.join(", :")}"
77
+ message << " but #{pluralize(@rejected_types_not_rejected)} accepted"
62
78
  end
63
-
64
- message.join("\n")
65
79
  end
66
80
 
67
- protected
68
-
69
- def responds_to_methods
70
- @subject.respond_to?(@attribute_name) &&
71
- @subject.public_send(@attribute_name).respond_to?(:attach) &&
72
- @subject.public_send(@attribute_name).respond_to?(:detach)
81
+ def pluralize(types)
82
+ if types.count == 1
83
+ ":#{types[0]} was"
84
+ else
85
+ ":#{types.join(", :")} were"
86
+ end
73
87
  end
74
88
 
75
89
  def all_allowed_types_allowed?
@@ -92,7 +106,7 @@ module ActiveStorageValidations
92
106
  @subject.public_send(@attribute_name).attach(attachment_for(type))
93
107
  end
94
108
 
95
- def validate_custom_message?
109
+ def is_custom_message_valid?
96
110
  return true unless @custom_message
97
111
 
98
112
  attach_invalid_content_type_file