active_storage_validations 1.1.3 → 1.2.0

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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +133 -69
  3. data/config/locales/da.yml +33 -0
  4. data/config/locales/de.yml +5 -0
  5. data/config/locales/en.yml +5 -0
  6. data/config/locales/es.yml +5 -0
  7. data/config/locales/fr.yml +5 -0
  8. data/config/locales/it.yml +5 -0
  9. data/config/locales/ja.yml +5 -0
  10. data/config/locales/nl.yml +5 -0
  11. data/config/locales/pl.yml +5 -0
  12. data/config/locales/pt-BR.yml +5 -0
  13. data/config/locales/ru.yml +5 -0
  14. data/config/locales/sv.yml +10 -1
  15. data/config/locales/tr.yml +5 -0
  16. data/config/locales/uk.yml +5 -0
  17. data/config/locales/vi.yml +5 -0
  18. data/config/locales/zh-CN.yml +5 -0
  19. data/lib/active_storage_validations/aspect_ratio_validator.rb +47 -22
  20. data/lib/active_storage_validations/attached_validator.rb +12 -3
  21. data/lib/active_storage_validations/base_size_validator.rb +66 -0
  22. data/lib/active_storage_validations/concerns/errorable.rb +38 -0
  23. data/lib/active_storage_validations/concerns/symbolizable.rb +8 -6
  24. data/lib/active_storage_validations/content_type_validator.rb +41 -6
  25. data/lib/active_storage_validations/dimension_validator.rb +15 -15
  26. data/lib/active_storage_validations/limit_validator.rb +44 -7
  27. data/lib/active_storage_validations/matchers/aspect_ratio_validator_matcher.rb +119 -0
  28. data/lib/active_storage_validations/matchers/attached_validator_matcher.rb +25 -36
  29. data/lib/active_storage_validations/matchers/base_size_validator_matcher.rb +134 -0
  30. data/lib/active_storage_validations/matchers/concerns/active_storageable.rb +17 -0
  31. data/lib/active_storage_validations/matchers/concerns/allow_blankable.rb +26 -0
  32. data/lib/active_storage_validations/matchers/concerns/attachable.rb +48 -0
  33. data/lib/active_storage_validations/matchers/concerns/contextable.rb +47 -0
  34. data/lib/active_storage_validations/matchers/concerns/messageable.rb +26 -0
  35. data/lib/active_storage_validations/matchers/concerns/rspecable.rb +25 -0
  36. data/lib/active_storage_validations/matchers/concerns/validatable.rb +11 -10
  37. data/lib/active_storage_validations/matchers/content_type_validator_matcher.rb +44 -27
  38. data/lib/active_storage_validations/matchers/dimension_validator_matcher.rb +67 -59
  39. data/lib/active_storage_validations/matchers/processable_image_validator_matcher.rb +78 -0
  40. data/lib/active_storage_validations/matchers/size_validator_matcher.rb +8 -126
  41. data/lib/active_storage_validations/matchers/total_size_validator_matcher.rb +40 -0
  42. data/lib/active_storage_validations/matchers.rb +3 -0
  43. data/lib/active_storage_validations/metadata.rb +60 -28
  44. data/lib/active_storage_validations/processable_image_validator.rb +14 -5
  45. data/lib/active_storage_validations/size_validator.rb +7 -51
  46. data/lib/active_storage_validations/total_size_validator.rb +49 -0
  47. data/lib/active_storage_validations/version.rb +1 -1
  48. data/lib/active_storage_validations.rb +3 -2
  49. metadata +38 -39
  50. data/lib/active_storage_validations/error_handler.rb +0 -21
@@ -1,56 +1,55 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'concerns/active_storageable.rb'
4
+ require_relative 'concerns/attachable.rb'
5
+ require_relative 'concerns/contextable.rb'
6
+ require_relative 'concerns/messageable.rb'
7
+ require_relative 'concerns/rspecable.rb'
3
8
  require_relative 'concerns/validatable.rb'
4
9
 
5
10
  module ActiveStorageValidations
6
11
  module Matchers
7
- def validate_attached_of(name)
8
- AttachedValidatorMatcher.new(name)
12
+ def validate_attached_of(attribute_name)
13
+ AttachedValidatorMatcher.new(attribute_name)
9
14
  end
10
15
 
11
16
  class AttachedValidatorMatcher
17
+ include ActiveStorageable
18
+ include Attachable
19
+ include Contextable
20
+ include Messageable
21
+ include Rspecable
12
22
  include Validatable
13
23
 
14
24
  def initialize(attribute_name)
25
+ initialize_contextable
26
+ initialize_messageable
27
+ initialize_rspecable
15
28
  @attribute_name = attribute_name
16
- @custom_message = nil
17
29
  end
18
30
 
19
31
  def description
20
- "validate #{@attribute_name} must be attached"
32
+ "validate that :#{@attribute_name} must be attached"
21
33
  end
22
34
 
23
- def with_message(message)
24
- @custom_message = message
25
- self
35
+ def failure_message
36
+ "is expected to validate attachment of :#{@attribute_name}"
26
37
  end
27
38
 
28
39
  def matches?(subject)
29
40
  @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
-
36
- def failure_message
37
- "is expected to validate attached of #{@attribute_name}"
38
- end
39
41
 
40
- def failure_message_when_negated
41
- "is expected to not validate attached of #{@attribute_name}"
42
+ is_a_valid_active_storage_attribute? &&
43
+ is_context_valid? &&
44
+ is_custom_message_valid? &&
45
+ is_valid_when_file_attached? &&
46
+ is_invalid_when_file_not_attached?
42
47
  end
43
48
 
44
49
  private
45
50
 
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
51
  def is_valid_when_file_attached?
53
- attach_dummy_file unless file_attached?
52
+ attach_file unless file_attached?
54
53
  validate
55
54
  is_valid?
56
55
  end
@@ -61,7 +60,7 @@ module ActiveStorageValidations
61
60
  !is_valid?
62
61
  end
63
62
 
64
- def validate_custom_message?
63
+ def is_custom_message_valid?
65
64
  return true unless @custom_message
66
65
 
67
66
  detach_file if file_attached?
@@ -69,16 +68,6 @@ module ActiveStorageValidations
69
68
  has_an_error_message_which_is_custom_message?
70
69
  end
71
70
 
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
71
  def file_attached?
83
72
  @subject.public_send(@attribute_name).attached?
84
73
  end
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Big thank you to the paperclip validation matchers:
4
+ # https://github.com/thoughtbot/paperclip/blob/v6.1.0/lib/paperclip/matchers/validate_attachment_size_matcher.rb
5
+
6
+ require_relative 'concerns/active_storageable.rb'
7
+ require_relative 'concerns/allow_blankable.rb'
8
+ require_relative 'concerns/attachable.rb'
9
+ require_relative 'concerns/contextable.rb'
10
+ require_relative 'concerns/messageable.rb'
11
+ require_relative 'concerns/rspecable.rb'
12
+ require_relative 'concerns/validatable.rb'
13
+
14
+ module ActiveStorageValidations
15
+ module Matchers
16
+ class BaseSizeValidatorMatcher
17
+ # BaseSizeValidatorMatcher is an abstract class and shouldn't be instantiated directly.
18
+
19
+ include ActiveStorageable
20
+ include AllowBlankable
21
+ include Attachable
22
+ include Contextable
23
+ include Messageable
24
+ include Rspecable
25
+ include Validatable
26
+
27
+ def initialize(attribute_name)
28
+ initialize_allow_blankable
29
+ initialize_contextable
30
+ initialize_messageable
31
+ initialize_rspecable
32
+ @attribute_name = attribute_name
33
+ @min = @max = nil
34
+ end
35
+
36
+ def less_than(size)
37
+ @max = size - 1.byte
38
+ self
39
+ end
40
+
41
+ def less_than_or_equal_to(size)
42
+ @max = size
43
+ self
44
+ end
45
+
46
+ def greater_than(size)
47
+ @min = size + 1.byte
48
+ self
49
+ end
50
+
51
+ def greater_than_or_equal_to(size)
52
+ @min = size
53
+ self
54
+ end
55
+
56
+ def between(range)
57
+ @min, @max = range.first, range.last
58
+ self
59
+ end
60
+
61
+ def matches?(subject)
62
+ @subject = subject.is_a?(Class) ? subject.new : subject
63
+
64
+ is_a_valid_active_storage_attribute? &&
65
+ is_context_valid? &&
66
+ is_allowing_blank? &&
67
+ is_custom_message_valid? &&
68
+ not_lower_than_min? &&
69
+ higher_than_min? &&
70
+ lower_than_max? &&
71
+ not_higher_than_max?
72
+ end
73
+
74
+ protected
75
+
76
+ def build_failure_message(message)
77
+ return unless @failure_message_artefacts.present?
78
+
79
+ message << " but there seem to have issues with the matcher methods you used, since:"
80
+ @failure_message_artefacts.each do |error_case|
81
+ message << " validation failed when provided with a #{error_case[:size]} bytes test file"
82
+ end
83
+ message << " whereas it should have passed"
84
+ end
85
+
86
+ def not_lower_than_min?
87
+ @min.nil? || !passes_validation_with_size(@min - 1)
88
+ end
89
+
90
+ def higher_than_min?
91
+ @min.nil? || passes_validation_with_size(@min + 1)
92
+ end
93
+
94
+ def lower_than_max?
95
+ @max.nil? || @max == Float::INFINITY || passes_validation_with_size(@max - 1)
96
+ end
97
+
98
+ def not_higher_than_max?
99
+ @max.nil? || @max == Float::INFINITY || !passes_validation_with_size(@max + 1)
100
+ end
101
+
102
+ def passes_validation_with_size(size)
103
+ mock_size_for(io, size) do
104
+ attach_file
105
+ validate
106
+ detach_file
107
+ is_valid? || add_failure_message_artefact(size)
108
+ end
109
+ end
110
+
111
+ def add_failure_message_artefact(size)
112
+ @failure_message_artefacts << { size: size }
113
+ false
114
+ end
115
+
116
+ def is_custom_message_valid?
117
+ return true unless @custom_message
118
+
119
+ mock_size_for(io, -1.kilobytes) do
120
+ attach_file
121
+ validate
122
+ detach_file
123
+ has_an_error_message_which_is_custom_message?
124
+ end
125
+ end
126
+
127
+ def mock_size_for(io, size)
128
+ Matchers.stub_method(io, :size, size) do
129
+ yield
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
@@ -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,48 @@
1
+ module ActiveStorageValidations
2
+ module Matchers
3
+ module Attachable
4
+ private
5
+
6
+ def attach_file(file = dummy_file)
7
+ @subject.public_send(@attribute_name).attach(file)
8
+ @subject.public_send(@attribute_name)
9
+ end
10
+
11
+ def dummy_file
12
+ {
13
+ io: io,
14
+ filename: 'test.png',
15
+ content_type: 'image/png'
16
+ }
17
+ end
18
+
19
+ def processable_image
20
+ {
21
+ io: File.open(Rails.root.join('public', 'image_1920x1080.png')),
22
+ filename: 'image_1920x1080_file.png',
23
+ content_type: 'image/png'
24
+ }
25
+ end
26
+
27
+ def not_processable_image
28
+ {
29
+ io: Tempfile.new('.'),
30
+ filename: 'processable.txt',
31
+ content_type: 'text/plain'
32
+ }
33
+ end
34
+
35
+ def io
36
+ @io ||= Tempfile.new('Hello world!')
37
+ end
38
+
39
+ def detach_file
40
+ @subject.attachment_changes.delete(@attribute_name.to_s)
41
+ end
42
+
43
+ def file_attached?
44
+ @subject.public_send(@attribute_name).attached?
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,47 @@
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_validators.none? { |validator| validator.options[:on] }
21
+
22
+ raise ArgumentError, "This validator matcher needs the #on option to work since its validator has one" if !@context && attribute_validators.all? { |validator| validator.options[:on] }
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)
26
+ (validator_contexts & @context.map(&:to_s)) == validator_contexts || raise_context_not_listed_error
27
+ elsif @context.is_a?(Symbol)
28
+ validator_contexts.include?(@context.to_s) || raise_context_not_listed_error
29
+ end
30
+ end
31
+
32
+ def validator_contexts
33
+ attribute_validators.map do |validator|
34
+ case validator.options[:on]
35
+ when Array then validator.options[:on].map { |context| context.to_s }
36
+ when NilClass then nil
37
+ else validator.options[:on].to_s
38
+ end
39
+ end.flatten.compact
40
+ end
41
+
42
+ def raise_context_not_listed_error
43
+ raise ArgumentError, "One of the provided contexts to the #on method is not found in any of the listed contexts for this attribute"
44
+ end
45
+ end
46
+ end
47
+ 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
@@ -27,7 +26,7 @@ module ActiveStorageValidations
27
26
  def available_errors
28
27
  [
29
28
  *validator_class::ERROR_TYPES,
30
- *error_from_custom_message
29
+ *errors_from_custom_messages
31
30
  ].compact
32
31
  end
33
32
 
@@ -35,19 +34,21 @@ 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
43
+ def attribute_validators
44
+ @subject.class.validators_on(@attribute_name).select do |validator|
45
+ validator.class == validator_class
49
46
  end
50
47
  end
48
+
49
+ def errors_from_custom_messages
50
+ attribute_validators.map { |validator| validator.options[:message] }
51
+ end
51
52
  end
52
53
  end
53
54
  end
@@ -3,25 +3,46 @@
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/attachable.rb'
9
+ require_relative 'concerns/contextable.rb'
10
+ require_relative 'concerns/messageable.rb'
11
+ require_relative 'concerns/rspecable.rb'
6
12
  require_relative 'concerns/validatable.rb'
7
13
 
8
14
  module ActiveStorageValidations
9
15
  module Matchers
10
- def validate_content_type_of(name)
11
- ContentTypeValidatorMatcher.new(name)
16
+ def validate_content_type_of(attribute_name)
17
+ ContentTypeValidatorMatcher.new(attribute_name)
12
18
  end
13
19
 
14
20
  class ContentTypeValidatorMatcher
21
+ include ActiveStorageable
22
+ include AllowBlankable
23
+ include Attachable
24
+ include Contextable
25
+ include Messageable
26
+ include Rspecable
15
27
  include Validatable
16
28
 
17
29
  def initialize(attribute_name)
30
+ initialize_allow_blankable
31
+ initialize_contextable
32
+ initialize_messageable
33
+ initialize_rspecable
18
34
  @attribute_name = attribute_name
19
35
  @allowed_types = @rejected_types = []
20
- @custom_message = nil
21
36
  end
22
37
 
23
38
  def description
24
- "validate the content types allowed on attachment #{@attribute_name}"
39
+ "validate the content types allowed on :#{@attribute_name}"
40
+ end
41
+
42
+ def failure_message
43
+ message = ["is expected to validate the content types of :#{@attribute_name}"]
44
+ build_failure_message(message)
45
+ message.join("\n")
25
46
  end
26
47
 
27
48
  def allowing(*types)
@@ -34,42 +55,37 @@ module ActiveStorageValidations
34
55
  self
35
56
  end
36
57
 
37
- def with_message(message)
38
- @custom_message = message
39
- self
40
- end
41
-
42
58
  def matches?(subject)
43
59
  @subject = subject.is_a?(Class) ? subject.new : subject
44
60
 
45
- responds_to_methods &&
61
+ is_a_valid_active_storage_attribute? &&
62
+ is_context_valid? &&
63
+ is_allowing_blank? &&
64
+ is_custom_message_valid? &&
46
65
  all_allowed_types_allowed? &&
47
- all_rejected_types_rejected? &&
48
- validate_custom_message?
66
+ all_rejected_types_rejected?
49
67
  end
50
68
 
51
- def failure_message
52
- message = ["Expected #{@attribute_name}"]
69
+ protected
53
70
 
71
+ def build_failure_message(message)
54
72
  if @allowed_types_not_allowed.present?
55
- message << "Accept content types: #{@allowed_types.join(", ")}"
56
- message << "#{@allowed_types_not_allowed.join(", ")} were rejected"
73
+ message << " the following content type#{'s' if @allowed_types.count > 1} should be allowed: :#{@allowed_types.join(", :")}"
74
+ message << " but #{pluralize(@allowed_types_not_allowed)} rejected"
57
75
  end
58
76
 
59
77
  if @rejected_types_not_rejected.present?
60
- message << "Reject content types: #{@rejected_types.join(", ")}"
61
- message << "#{@rejected_types_not_rejected.join(", ")} were accepted"
78
+ message << " the following content type#{'s' if @rejected_types.count > 1} should be rejected: :#{@rejected_types.join(", :")}"
79
+ message << " but #{pluralize(@rejected_types_not_rejected)} accepted"
62
80
  end
63
-
64
- message.join("\n")
65
81
  end
66
82
 
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)
83
+ def pluralize(types)
84
+ if types.count == 1
85
+ ":#{types[0]} was"
86
+ else
87
+ ":#{types.join(", :")} were"
88
+ end
73
89
  end
74
90
 
75
91
  def all_allowed_types_allowed?
@@ -85,6 +101,7 @@ module ActiveStorageValidations
85
101
  def type_allowed?(type)
86
102
  attach_file_of_type(type)
87
103
  validate
104
+ detach_file
88
105
  is_valid?
89
106
  end
90
107
 
@@ -92,7 +109,7 @@ module ActiveStorageValidations
92
109
  @subject.public_send(@attribute_name).attach(attachment_for(type))
93
110
  end
94
111
 
95
- def validate_custom_message?
112
+ def is_custom_message_valid?
96
113
  return true unless @custom_message
97
114
 
98
115
  attach_invalid_content_type_file