active_storage_validations 1.0.4 → 1.1.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +126 -48
  3. data/config/locales/de.yml +6 -1
  4. data/config/locales/en.yml +5 -1
  5. data/config/locales/es.yml +6 -1
  6. data/config/locales/fr.yml +6 -1
  7. data/config/locales/it.yml +6 -1
  8. data/config/locales/ja.yml +6 -1
  9. data/config/locales/nl.yml +6 -1
  10. data/config/locales/pl.yml +6 -1
  11. data/config/locales/pt-BR.yml +6 -1
  12. data/config/locales/ru.yml +7 -2
  13. data/config/locales/sv.yml +23 -0
  14. data/config/locales/tr.yml +6 -1
  15. data/config/locales/uk.yml +6 -1
  16. data/config/locales/vi.yml +6 -1
  17. data/config/locales/zh-CN.yml +6 -1
  18. data/lib/active_storage_validations/aspect_ratio_validator.rb +57 -27
  19. data/lib/active_storage_validations/attached_validator.rb +20 -5
  20. data/lib/active_storage_validations/concerns/errorable.rb +38 -0
  21. data/lib/active_storage_validations/concerns/symbolizable.rb +12 -0
  22. data/lib/active_storage_validations/content_type_validator.rb +47 -7
  23. data/lib/active_storage_validations/dimension_validator.rb +61 -30
  24. data/lib/active_storage_validations/limit_validator.rb +49 -5
  25. data/lib/active_storage_validations/matchers/aspect_ratio_validator_matcher.rb +128 -0
  26. data/lib/active_storage_validations/matchers/attached_validator_matcher.rb +54 -23
  27. data/lib/active_storage_validations/matchers/concerns/active_storageable.rb +17 -0
  28. data/lib/active_storage_validations/matchers/concerns/allow_blankable.rb +26 -0
  29. data/lib/active_storage_validations/matchers/concerns/contextable.rb +35 -0
  30. data/lib/active_storage_validations/matchers/concerns/messageable.rb +26 -0
  31. data/lib/active_storage_validations/matchers/concerns/rspecable.rb +25 -0
  32. data/lib/active_storage_validations/matchers/concerns/validatable.rb +48 -0
  33. data/lib/active_storage_validations/matchers/content_type_validator_matcher.rb +75 -32
  34. data/lib/active_storage_validations/matchers/dimension_validator_matcher.rb +99 -52
  35. data/lib/active_storage_validations/matchers/size_validator_matcher.rb +96 -31
  36. data/lib/active_storage_validations/matchers.rb +1 -0
  37. data/lib/active_storage_validations/metadata.rb +42 -28
  38. data/lib/active_storage_validations/processable_image_validator.rb +16 -10
  39. data/lib/active_storage_validations/size_validator.rb +32 -9
  40. data/lib/active_storage_validations/version.rb +1 -1
  41. data/lib/active_storage_validations.rb +3 -0
  42. metadata +29 -4
@@ -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,11 @@
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'
7
+ require_relative 'concerns/validatable.rb'
8
+
3
9
  module ActiveStorageValidations
4
10
  module Matchers
5
11
  def validate_attached_of(name)
@@ -7,52 +13,77 @@ module ActiveStorageValidations
7
13
  end
8
14
 
9
15
  class AttachedValidatorMatcher
16
+ include ActiveStorageable
17
+ include Contextable
18
+ include Messageable
19
+ include Rspecable
20
+ include Validatable
21
+
10
22
  def initialize(attribute_name)
23
+ initialize_contextable
24
+ initialize_messageable
25
+ initialize_rspecable
11
26
  @attribute_name = attribute_name
12
27
  end
13
28
 
14
29
  def description
15
- "validate #{@attribute_name} must be attached"
30
+ "validate that :#{@attribute_name} must be attached"
31
+ end
32
+
33
+ def failure_message
34
+ "is expected to validate attachment of :#{@attribute_name}"
16
35
  end
17
36
 
18
37
  def matches?(subject)
19
38
  @subject = subject.is_a?(Class) ? subject.new : subject
20
- responds_to_methods && valid_when_attached && invalid_when_not_attached
39
+
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?
21
45
  end
22
46
 
23
- def failure_message
24
- "is expected to validate attached of #{@attribute_name}"
47
+ private
48
+
49
+ def is_valid_when_file_attached?
50
+ attach_dummy_file unless file_attached?
51
+ validate
52
+ is_valid?
25
53
  end
26
54
 
27
- def failure_message_when_negated
28
- "is expected to not validate attached of #{@attribute_name}"
55
+ def is_invalid_when_file_not_attached?
56
+ detach_file if file_attached?
57
+ validate
58
+ !is_valid?
29
59
  end
30
60
 
31
- private
61
+ def is_custom_message_valid?
62
+ return true unless @custom_message
63
+
64
+ detach_file if file_attached?
65
+ validate
66
+ has_an_error_message_which_is_custom_message?
67
+ end
32
68
 
33
- def responds_to_methods
34
- @subject.respond_to?(@attribute_name) &&
35
- @subject.public_send(@attribute_name).respond_to?(:attach) &&
36
- @subject.public_send(@attribute_name).respond_to?(:detach)
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)
37
77
  end
38
78
 
39
- def valid_when_attached
40
- @subject.public_send(@attribute_name).attach(attachable) unless @subject.public_send(@attribute_name).attached?
41
- @subject.validate
42
- @subject.errors.details[@attribute_name].exclude?(error: :blank)
79
+ def file_attached?
80
+ @subject.public_send(@attribute_name).attached?
43
81
  end
44
82
 
45
- def invalid_when_not_attached
83
+ def detach_file
46
84
  @subject.public_send(@attribute_name).detach
47
85
  # Unset the direct relation since `detach` on an unpersisted record does not set `attached?` to false.
48
86
  @subject.public_send("#{@attribute_name}=", nil)
49
-
50
- @subject.validate
51
- @subject.errors.details[@attribute_name].include?(error: :blank)
52
- end
53
-
54
- def attachable
55
- { io: Tempfile.new('.'), filename: 'dummy.txt', content_type: 'text/plain' }
56
87
  end
57
88
  end
58
89
  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,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
@@ -0,0 +1,48 @@
1
+ require "active_support/concern"
2
+
3
+ module ActiveStorageValidations
4
+ module Matchers
5
+ module Validatable
6
+ extend ActiveSupport::Concern
7
+
8
+ private
9
+
10
+ def validate
11
+ @subject.validate(@context)
12
+ end
13
+
14
+ def validator_errors_for_attribute
15
+ @subject.errors.details[@attribute_name].select do |error|
16
+ error[:validator_type] == validator_class.to_sym
17
+ end
18
+ end
19
+
20
+ def is_valid?
21
+ validator_errors_for_attribute.none? do |error|
22
+ error[:error].in?(available_errors)
23
+ end
24
+ end
25
+
26
+ def available_errors
27
+ [
28
+ *validator_class::ERROR_TYPES,
29
+ *error_from_custom_message
30
+ ].compact
31
+ end
32
+
33
+ def validator_class
34
+ self.class.name.gsub(/::Matchers|Matcher/, '').constantize
35
+ end
36
+
37
+ def attribute_validator
38
+ @subject.class.validators_on(@attribute_name).find do |validator|
39
+ validator.class == validator_class
40
+ end
41
+ end
42
+
43
+ def error_from_custom_message
44
+ attribute_validator.options[:message]
45
+ end
46
+ end
47
+ end
48
+ end
@@ -2,6 +2,14 @@
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/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'
11
+ require_relative 'concerns/validatable.rb'
12
+
5
13
  module ActiveStorageValidations
6
14
  module Matchers
7
15
  def validate_content_type_of(name)
@@ -9,12 +17,30 @@ module ActiveStorageValidations
9
17
  end
10
18
 
11
19
  class ContentTypeValidatorMatcher
20
+ include ActiveStorageable
21
+ include AllowBlankable
22
+ include Contextable
23
+ include Messageable
24
+ include Rspecable
25
+ include Validatable
26
+
12
27
  def initialize(attribute_name)
28
+ initialize_allow_blankable
29
+ initialize_contextable
30
+ initialize_messageable
31
+ initialize_rspecable
13
32
  @attribute_name = attribute_name
33
+ @allowed_types = @rejected_types = []
14
34
  end
15
35
 
16
36
  def description
17
- "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")
18
44
  end
19
45
 
20
46
  def allowing(*types)
@@ -29,60 +55,77 @@ module ActiveStorageValidations
29
55
 
30
56
  def matches?(subject)
31
57
  @subject = subject.is_a?(Class) ? subject.new : subject
32
- responds_to_methods && allowed_types_allowed? && rejected_types_rejected?
58
+
59
+ is_a_valid_active_storage_attribute? &&
60
+ is_context_valid? &&
61
+ is_allowing_blank? &&
62
+ is_custom_message_valid? &&
63
+ all_allowed_types_allowed? &&
64
+ all_rejected_types_rejected?
33
65
  end
34
66
 
35
- def failure_message
36
- message = ["Expected #{@attribute_name}"]
67
+ protected
37
68
 
38
- if @allowed_types
39
- message << "Accept content types: #{allowed_types.join(", ")}"
40
- message << "#{@missing_allowed_types.join(", ")} were rejected"
69
+ def build_failure_message(message)
70
+ if @allowed_types_not_allowed.present?
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"
41
73
  end
42
74
 
43
- if @rejected_types
44
- message << "Reject content types: #{rejected_types.join(", ")}"
45
- message << "#{@missing_rejected_types.join(", ")} were accepted"
75
+ if @rejected_types_not_rejected.present?
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"
46
78
  end
47
-
48
- message.join("\n")
49
79
  end
50
80
 
51
- protected
81
+ def pluralize(types)
82
+ if types.count == 1
83
+ ":#{types[0]} was"
84
+ else
85
+ ":#{types.join(", :")} were"
86
+ end
87
+ end
52
88
 
53
- def responds_to_methods
54
- @subject.respond_to?(@attribute_name) &&
55
- @subject.public_send(@attribute_name).respond_to?(:attach) &&
56
- @subject.public_send(@attribute_name).respond_to?(:detach)
89
+ def all_allowed_types_allowed?
90
+ @allowed_types_not_allowed ||= @allowed_types.reject { |type| type_allowed?(type) }
91
+ @allowed_types_not_allowed.empty?
57
92
  end
58
93
 
59
- def allowed_types
60
- @allowed_types || []
94
+ def all_rejected_types_rejected?
95
+ @rejected_types_not_rejected ||= @rejected_types.select { |type| type_allowed?(type) }
96
+ @rejected_types_not_rejected.empty?
61
97
  end
62
98
 
63
- def rejected_types
64
- @rejected_types || []
99
+ def type_allowed?(type)
100
+ attach_file_of_type(type)
101
+ validate
102
+ is_valid?
65
103
  end
66
104
 
67
- def allowed_types_allowed?
68
- @missing_allowed_types ||= allowed_types.reject { |type| type_allowed?(type) }
69
- @missing_allowed_types.none?
105
+ def attach_file_of_type(type)
106
+ @subject.public_send(@attribute_name).attach(attachment_for(type))
70
107
  end
71
108
 
72
- def rejected_types_rejected?
73
- @missing_rejected_types ||= rejected_types.select { |type| type_allowed?(type) }
74
- @missing_rejected_types.none?
109
+ def is_custom_message_valid?
110
+ return true unless @custom_message
111
+
112
+ attach_invalid_content_type_file
113
+ validate
114
+ has_an_error_message_which_is_custom_message?
75
115
  end
76
116
 
77
- def type_allowed?(type)
78
- @subject.public_send(@attribute_name).attach(attachment_for(type))
79
- @subject.validate
80
- @subject.errors.details[@attribute_name].all? { |error| error[:error] != :content_type_invalid }
117
+ def attach_invalid_content_type_file
118
+ @subject.public_send(@attribute_name).attach(attachment_for('fake/fake'))
81
119
  end
82
120
 
83
121
  def attachment_for(type)
84
122
  suffix = type.to_s.split('/').last
85
- { io: Tempfile.new('.'), filename: "test.#{suffix}", content_type: type }
123
+
124
+ {
125
+ io: Tempfile.new('.'),
126
+ filename: "test.#{suffix}",
127
+ content_type: type
128
+ }
86
129
  end
87
130
  end
88
131
  end