active_storage_validations 1.1.3 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
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