active_storage_validations 1.1.4 → 1.3.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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +84 -33
  3. data/config/locales/da.yml +33 -0
  4. data/config/locales/de.yml +6 -0
  5. data/config/locales/en.yml +6 -0
  6. data/config/locales/es.yml +6 -0
  7. data/config/locales/fr.yml +6 -0
  8. data/config/locales/it.yml +6 -0
  9. data/config/locales/ja.yml +6 -0
  10. data/config/locales/nl.yml +6 -0
  11. data/config/locales/pl.yml +6 -0
  12. data/config/locales/pt-BR.yml +6 -0
  13. data/config/locales/ru.yml +6 -0
  14. data/config/locales/sv.yml +11 -1
  15. data/config/locales/tr.yml +6 -0
  16. data/config/locales/uk.yml +6 -0
  17. data/config/locales/vi.yml +6 -0
  18. data/config/locales/zh-CN.yml +6 -0
  19. data/lib/active_storage_validations/aspect_ratio_validator.rb +10 -34
  20. data/lib/active_storage_validations/attached_validator.rb +6 -4
  21. data/lib/active_storage_validations/base_size_validator.rb +68 -0
  22. data/lib/active_storage_validations/concerns/active_storageable.rb +28 -0
  23. data/lib/active_storage_validations/concerns/errorable.rb +4 -5
  24. data/lib/active_storage_validations/concerns/loggable.rb +9 -0
  25. data/lib/active_storage_validations/concerns/metadatable.rb +31 -0
  26. data/lib/active_storage_validations/content_type_spoof_detector.rb +130 -0
  27. data/lib/active_storage_validations/content_type_validator.rb +56 -22
  28. data/lib/active_storage_validations/dimension_validator.rb +31 -52
  29. data/lib/active_storage_validations/limit_validator.rb +5 -3
  30. data/lib/active_storage_validations/marcel_extensor.rb +5 -0
  31. data/lib/active_storage_validations/matchers/aspect_ratio_validator_matcher.rb +6 -15
  32. data/lib/active_storage_validations/matchers/attached_validator_matcher.rb +5 -13
  33. data/lib/active_storage_validations/matchers/base_size_validator_matcher.rb +134 -0
  34. data/lib/active_storage_validations/matchers/concerns/attachable.rb +66 -0
  35. data/lib/active_storage_validations/matchers/concerns/contextable.rb +20 -8
  36. data/lib/active_storage_validations/matchers/concerns/messageable.rb +1 -1
  37. data/lib/active_storage_validations/matchers/concerns/validatable.rb +9 -3
  38. data/lib/active_storage_validations/matchers/content_type_validator_matcher.rb +12 -2
  39. data/lib/active_storage_validations/matchers/dimension_validator_matcher.rb +6 -15
  40. data/lib/active_storage_validations/matchers/limit_validator_matcher.rb +127 -0
  41. data/lib/active_storage_validations/matchers/processable_image_validator_matcher.rb +78 -0
  42. data/lib/active_storage_validations/matchers/size_validator_matcher.rb +4 -139
  43. data/lib/active_storage_validations/matchers/total_size_validator_matcher.rb +31 -0
  44. data/lib/active_storage_validations/matchers.rb +6 -15
  45. data/lib/active_storage_validations/metadata.rb +27 -12
  46. data/lib/active_storage_validations/processable_image_validator.rb +17 -32
  47. data/lib/active_storage_validations/size_validator.rb +6 -55
  48. data/lib/active_storage_validations/total_size_validator.rb +45 -0
  49. data/lib/active_storage_validations/version.rb +1 -1
  50. data/lib/active_storage_validations.rb +4 -1
  51. metadata +45 -46
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative 'concerns/active_storageable.rb'
4
4
  require_relative 'concerns/allow_blankable.rb'
5
+ require_relative 'concerns/attachable.rb'
5
6
  require_relative 'concerns/contextable.rb'
6
7
  require_relative 'concerns/messageable.rb'
7
8
  require_relative 'concerns/rspecable.rb'
@@ -9,13 +10,14 @@ require_relative 'concerns/validatable.rb'
9
10
 
10
11
  module ActiveStorageValidations
11
12
  module Matchers
12
- def validate_aspect_ratio_of(name, expected_aspect_ratio)
13
- AspectRatioValidatorMatcher.new(name, expected_aspect_ratio)
13
+ def validate_aspect_ratio_of(attribute_name)
14
+ AspectRatioValidatorMatcher.new(attribute_name)
14
15
  end
15
16
 
16
17
  class AspectRatioValidatorMatcher
17
18
  include ActiveStorageable
18
19
  include AllowBlankable
20
+ include Attachable
19
21
  include Contextable
20
22
  include Messageable
21
23
  include Rspecable
@@ -76,6 +78,7 @@ module ActiveStorageValidations
76
78
 
77
79
  mock_dimensions_for(attach_file, width, height) do
78
80
  validate
81
+ detach_file
79
82
  is_valid?
80
83
  end
81
84
  end
@@ -85,23 +88,11 @@ module ActiveStorageValidations
85
88
 
86
89
  mock_dimensions_for(attach_file, -1, -1) do
87
90
  validate
91
+ detach_file
88
92
  has_an_error_message_which_is_custom_message?
89
93
  end
90
94
  end
91
95
 
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
96
  def mock_dimensions_for(attachment, width, height)
106
97
  Matchers.mock_metadata(attachment, width, height) do
107
98
  yield
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'concerns/active_storageable.rb'
4
+ require_relative 'concerns/attachable.rb'
4
5
  require_relative 'concerns/contextable.rb'
5
6
  require_relative 'concerns/messageable.rb'
6
7
  require_relative 'concerns/rspecable.rb'
@@ -8,12 +9,13 @@ require_relative 'concerns/validatable.rb'
8
9
 
9
10
  module ActiveStorageValidations
10
11
  module Matchers
11
- def validate_attached_of(name)
12
- AttachedValidatorMatcher.new(name)
12
+ def validate_attached_of(attribute_name)
13
+ AttachedValidatorMatcher.new(attribute_name)
13
14
  end
14
15
 
15
16
  class AttachedValidatorMatcher
16
17
  include ActiveStorageable
18
+ include Attachable
17
19
  include Contextable
18
20
  include Messageable
19
21
  include Rspecable
@@ -47,7 +49,7 @@ module ActiveStorageValidations
47
49
  private
48
50
 
49
51
  def is_valid_when_file_attached?
50
- attach_dummy_file unless file_attached?
52
+ attach_file unless file_attached?
51
53
  validate
52
54
  is_valid?
53
55
  end
@@ -66,16 +68,6 @@ module ActiveStorageValidations
66
68
  has_an_error_message_which_is_custom_message?
67
69
  end
68
70
 
69
- def attach_dummy_file
70
- dummy_file = {
71
- io: Tempfile.new('.'),
72
- filename: 'dummy.txt',
73
- content_type: 'text/plain'
74
- }
75
-
76
- @subject.public_send(@attribute_name).attach(dummy_file)
77
- end
78
-
79
71
  def file_attached?
80
72
  @subject.public_send(@attribute_name).attached?
81
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,66 @@
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 attach_files(count)
12
+ return unless count.positive?
13
+
14
+ file_array = Array.new(count, dummy_file)
15
+
16
+ @subject.public_send(@attribute_name).attach(file_array)
17
+ end
18
+
19
+ def detach_file
20
+ @subject.attachment_changes.delete(@attribute_name.to_s)
21
+ end
22
+ alias :detach_files :detach_file
23
+
24
+ def file_attached?
25
+ @subject.public_send(@attribute_name).attached?
26
+ end
27
+
28
+ def dummy_blob
29
+ ActiveStorage::Blob.create_and_upload!(**dummy_file)
30
+ end
31
+
32
+ def dummy_file
33
+ {
34
+ io: io,
35
+ filename: 'test.png',
36
+ content_type: 'image/png'
37
+ }
38
+ end
39
+
40
+ def processable_image
41
+ {
42
+ io: StringIO.new(image_data),
43
+ filename: 'processable_image.png',
44
+ content_type: 'image/png'
45
+ }
46
+ end
47
+
48
+ def not_processable_image
49
+ {
50
+ io: Tempfile.new('.'),
51
+ filename: 'not_processable_image.txt',
52
+ content_type: 'text/plain'
53
+ }
54
+ end
55
+
56
+ def io
57
+ @io ||= Tempfile.new('Hello world!')
58
+ end
59
+
60
+ def image_data
61
+ # Binary data for a 1x1 transparent PNG image
62
+ "\x89PNG\r\n\x1A\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x06\x00\x00\x00\x1F\x15\xC4\x89\x00\x00\x00\nIDATx\x9Cc\x00\x01\x00\x00\x05\x00\x01\r\n\x2D\xB4\x00\x00\x00\x00IEND\xAE\x42\x60\x82"
63
+ end
64
+ end
65
+ end
66
+ end
@@ -17,19 +17,31 @@ module ActiveStorageValidations
17
17
  private
18
18
 
19
19
  def is_context_valid?
20
- return true if !@context && !(attribute_validator && attribute_validator.options[:on])
20
+ return true if !@context && attribute_validators.none? { |validator| validator.options[:on] }
21
21
 
22
- raise ArgumentError, "This validator matcher needs the #on option to work since its validator has one" if !@context
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
23
  raise ArgumentError, "This validator matcher option only allows a symbol or an array" if !(@context.is_a?(Symbol) || @context.is_a?(Array))
24
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
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
31
29
  end
32
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
33
45
  end
34
46
  end
35
47
  end
@@ -18,7 +18,7 @@ module ActiveStorageValidations
18
18
 
19
19
  def has_an_error_message_which_is_custom_message?
20
20
  validator_errors_for_attribute.one? do |error|
21
- error[:error] == @custom_message
21
+ error[:custom_message] == @custom_message
22
22
  end
23
23
  end
24
24
  end
@@ -26,7 +26,7 @@ module ActiveStorageValidations
26
26
  def available_errors
27
27
  [
28
28
  *validator_class::ERROR_TYPES,
29
- *error_from_custom_message
29
+ *errors_from_custom_messages
30
30
  ].compact
31
31
  end
32
32
 
@@ -40,8 +40,14 @@ module ActiveStorageValidations
40
40
  end
41
41
  end
42
42
 
43
- def error_from_custom_message
44
- attribute_validator.options[:message]
43
+ def attribute_validators
44
+ @subject.class.validators_on(@attribute_name).select do |validator|
45
+ validator.class == validator_class
46
+ end
47
+ end
48
+
49
+ def errors_from_custom_messages
50
+ attribute_validators.map { |validator| validator.options[:message] }
45
51
  end
46
52
  end
47
53
  end
@@ -5,6 +5,7 @@
5
5
 
6
6
  require_relative 'concerns/active_storageable.rb'
7
7
  require_relative 'concerns/allow_blankable.rb'
8
+ require_relative 'concerns/attachable.rb'
8
9
  require_relative 'concerns/contextable.rb'
9
10
  require_relative 'concerns/messageable.rb'
10
11
  require_relative 'concerns/rspecable.rb'
@@ -12,13 +13,14 @@ require_relative 'concerns/validatable.rb'
12
13
 
13
14
  module ActiveStorageValidations
14
15
  module Matchers
15
- def validate_content_type_of(name)
16
- ContentTypeValidatorMatcher.new(name)
16
+ def validate_content_type_of(attribute_name)
17
+ ContentTypeValidatorMatcher.new(attribute_name)
17
18
  end
18
19
 
19
20
  class ContentTypeValidatorMatcher
20
21
  include ActiveStorageable
21
22
  include AllowBlankable
23
+ include Attachable
22
24
  include Contextable
23
25
  include Messageable
24
26
  include Rspecable
@@ -99,6 +101,7 @@ module ActiveStorageValidations
99
101
  def type_allowed?(type)
100
102
  attach_file_of_type(type)
101
103
  validate
104
+ detach_file
102
105
  is_valid?
103
106
  end
104
107
 
@@ -127,6 +130,13 @@ module ActiveStorageValidations
127
130
  content_type: type
128
131
  }
129
132
  end
133
+
134
+ # Due to the way we build test attachments in #attachment_for
135
+ # (ie spoofed file basically), we need to ignore the error related to
136
+ # content type spoofing in our matcher to pass the tests
137
+ def validator_errors_for_attribute
138
+ super.reject { |hash| hash[:error] == :spoofed_content_type }
139
+ end
130
140
  end
131
141
  end
132
142
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative 'concerns/active_storageable.rb'
4
4
  require_relative 'concerns/allow_blankable.rb'
5
+ require_relative 'concerns/attachable'
5
6
  require_relative 'concerns/contextable.rb'
6
7
  require_relative 'concerns/messageable.rb'
7
8
  require_relative 'concerns/rspecable.rb'
@@ -9,13 +10,14 @@ require_relative 'concerns/validatable.rb'
9
10
 
10
11
  module ActiveStorageValidations
11
12
  module Matchers
12
- def validate_dimensions_of(name)
13
- DimensionValidatorMatcher.new(name)
13
+ def validate_dimensions_of(attribute_name)
14
+ DimensionValidatorMatcher.new(attribute_name)
14
15
  end
15
16
 
16
17
  class DimensionValidatorMatcher
17
18
  include ActiveStorageable
18
19
  include AllowBlankable
20
+ include Attachable
19
21
  include Contextable
20
22
  include Messageable
21
23
  include Rspecable
@@ -162,6 +164,7 @@ module ActiveStorageValidations
162
164
  def passes_validation_with_dimensions(width, height)
163
165
  mock_dimensions_for(attach_file, width, height) do
164
166
  validate
167
+ detach_file
165
168
  is_valid? || add_failure_message_artefact(width, height)
166
169
  end
167
170
  end
@@ -176,6 +179,7 @@ module ActiveStorageValidations
176
179
 
177
180
  mock_dimensions_for(attach_file, -1, -1) do
178
181
  validate
182
+ detach_file
179
183
  has_an_error_message_which_is_custom_message?
180
184
  end
181
185
  end
@@ -185,19 +189,6 @@ module ActiveStorageValidations
185
189
  yield
186
190
  end
187
191
  end
188
-
189
- def attach_file
190
- @subject.public_send(@attribute_name).attach(dummy_file)
191
- @subject.public_send(@attribute_name)
192
- end
193
-
194
- def dummy_file
195
- {
196
- io: Tempfile.new('Hello world!'),
197
- filename: 'test.png',
198
- content_type: 'image/png'
199
- }
200
- end
201
192
  end
202
193
  end
203
194
  end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'concerns/active_storageable'
4
+ require_relative 'concerns/allow_blankable'
5
+ require_relative 'concerns/attachable'
6
+ require_relative 'concerns/contextable'
7
+ require_relative 'concerns/messageable'
8
+ require_relative 'concerns/rspecable'
9
+ require_relative 'concerns/validatable'
10
+
11
+ module ActiveStorageValidations
12
+ module Matchers
13
+ def validate_limits_of(name)
14
+ LimitValidatorMatcher.new(name)
15
+ end
16
+
17
+ class LimitValidatorMatcher
18
+ include ActiveStorageable
19
+ include AllowBlankable
20
+ include Attachable
21
+ include Contextable
22
+ include Messageable
23
+ include Rspecable
24
+ include Validatable
25
+
26
+ def initialize(attribute_name)
27
+ initialize_allow_blankable
28
+ initialize_contextable
29
+ initialize_messageable
30
+ initialize_rspecable
31
+ @attribute_name = attribute_name
32
+ @min = @max = nil
33
+ end
34
+
35
+ def description
36
+ "validate the limit files of :#{@attribute_name}"
37
+ end
38
+
39
+ def failure_message
40
+ message = ["is expected to validate limit file of :#{@attribute_name}"]
41
+ build_failure_message(message)
42
+ message.join("\n")
43
+ end
44
+
45
+ def min(count)
46
+ @min = count
47
+ self
48
+ end
49
+
50
+ def max(count)
51
+ @max = count
52
+ self
53
+ end
54
+
55
+ def matches?(subject)
56
+ @subject = subject.is_a?(Class) ? subject.new : subject
57
+
58
+ is_a_valid_active_storage_attribute? &&
59
+ is_context_valid? &&
60
+ is_custom_message_valid? &&
61
+ file_count_not_smaller_than_min? &&
62
+ file_count_equal_min? &&
63
+ file_count_larger_than_min? &&
64
+ file_count_smaller_than_max? &&
65
+ file_count_equal_max? &&
66
+ file_count_not_larger_than_max?
67
+ end
68
+
69
+ private
70
+
71
+ def build_failure_message(message)
72
+ return unless @failure_message_artefacts.present?
73
+
74
+ message << " but there seem to have issues with the matcher methods you used, since:"
75
+ @failure_message_artefacts.each do |error_case|
76
+ message << " validation failed when provided with #{error_case[:count]} file(s)"
77
+ end
78
+ message << " whereas it should have passed"
79
+ end
80
+
81
+ def file_count_not_smaller_than_min?
82
+ @min.nil? || @min.zero? || !passes_validation_with_limits(@min - 1)
83
+ end
84
+
85
+ def file_count_equal_min?
86
+ @min.nil? || @min.zero? || passes_validation_with_limits(@min)
87
+ end
88
+
89
+ def file_count_larger_than_min?
90
+ @min.nil? || @min.zero? || @min == @max || passes_validation_with_limits(@min + 1)
91
+ end
92
+
93
+ def file_count_smaller_than_max?
94
+ @max.nil? || @min == @max || passes_validation_with_limits(@max - 1)
95
+ end
96
+
97
+ def file_count_equal_max?
98
+ @max.nil? || passes_validation_with_limits(@max)
99
+ end
100
+
101
+ def file_count_not_larger_than_max?
102
+ @max.nil? || !passes_validation_with_limits(@max + 1)
103
+ end
104
+
105
+ def passes_validation_with_limits(count)
106
+ attach_files(count)
107
+ validate
108
+ detach_files
109
+ is_valid? || add_failure_message_artefact(count)
110
+ end
111
+
112
+ def is_custom_message_valid?
113
+ return true if !@custom_message || (@min&.zero? && @max.nil?)
114
+
115
+ @min.nil? ? attach_files(@max + 1) : attach_files(@min - 1)
116
+ validate
117
+ detach_files
118
+ has_an_error_message_which_is_custom_message?
119
+ end
120
+
121
+ def add_failure_message_artefact(count)
122
+ @failure_message_artefacts << { count: count }
123
+ false
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'concerns/active_storageable.rb'
4
+ require_relative 'concerns/allow_blankable.rb'
5
+ require_relative 'concerns/attachable.rb'
6
+ require_relative 'concerns/contextable.rb'
7
+ require_relative 'concerns/messageable.rb'
8
+ require_relative 'concerns/rspecable.rb'
9
+ require_relative 'concerns/validatable.rb'
10
+
11
+ module ActiveStorageValidations
12
+ module Matchers
13
+ def validate_processable_image_of(name)
14
+ ProcessableImageValidatorMatcher.new(name)
15
+ end
16
+
17
+ class ProcessableImageValidatorMatcher
18
+ include ActiveStorageable
19
+ include AllowBlankable
20
+ include Attachable
21
+ include Contextable
22
+ include Messageable
23
+ include Rspecable
24
+ include Validatable
25
+
26
+ def initialize(attribute_name)
27
+ initialize_allow_blankable
28
+ initialize_contextable
29
+ initialize_messageable
30
+ initialize_rspecable
31
+ @attribute_name = attribute_name
32
+ end
33
+
34
+ def description
35
+ "validate that :#{@attribute_name} is a processable image"
36
+ end
37
+
38
+ def failure_message
39
+ "is expected to validate the processable image of :#{@attribute_name}"
40
+ end
41
+
42
+ def matches?(subject)
43
+ @subject = subject.is_a?(Class) ? subject.new : subject
44
+
45
+ is_a_valid_active_storage_attribute? &&
46
+ is_context_valid? &&
47
+ is_custom_message_valid? &&
48
+ is_valid_when_image_processable? &&
49
+ is_invalid_when_image_not_processable?
50
+ end
51
+
52
+ private
53
+
54
+ def is_valid_when_image_processable?
55
+ attach_file(processable_image)
56
+ validate
57
+ detach_file
58
+ is_valid?
59
+ end
60
+
61
+ def is_invalid_when_image_not_processable?
62
+ attach_file(not_processable_image)
63
+ validate
64
+ detach_file
65
+ !is_valid?
66
+ end
67
+
68
+ def is_custom_message_valid?
69
+ return true unless @custom_message
70
+
71
+ attach_file(not_processable_image)
72
+ validate
73
+ detach_file
74
+ has_an_error_message_which_is_custom_message?
75
+ end
76
+ end
77
+ end
78
+ end