active_storage_validations 0.8.2 → 0.9.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.
@@ -0,0 +1,22 @@
1
+ tr:
2
+ errors:
3
+ messages:
4
+ content_type_invalid: "geçersiz dosya tipine sahip"
5
+ file_size_out_of_range: "dosya boyutu %{file_size} gerekli aralık dışında"
6
+ limit_out_of_range: "toplam miktar aralık dışında"
7
+ image_metadata_missing: "geçerli bir imaj değil"
8
+ dimension_min_inclusion: "%{width} x %{height} piksele eşit ya da büyük olmalı"
9
+ dimension_max_inclusion: "%{width} x %{height} piksele eşit ya da küçük olmalı"
10
+ dimension_width_inclusion: "en %{min} ve %{max} piksel aralığı dışında"
11
+ dimension_height_inclusion: "boy %{min} ve %{max} piksel aralığı dışında"
12
+ dimension_width_greater_than_or_equal_to: "en %{length} piksele eşit ya da büyük olmalı"
13
+ dimension_height_greater_than_or_equal_to: "boy %{length} piksele eşit ya da büyük olmalı"
14
+ dimension_width_less_than_or_equal_to: "en %{length} piksele eşit ya da küçük olmalı"
15
+ dimension_height_less_than_or_equal_to: "boy %{length} piksele eşit ya da küçük olmalı"
16
+ dimension_width_equal_to: "en %{length} piksele eşit olmalı"
17
+ dimension_height_equal_to: "boy %{length} piksele eşit olmalı"
18
+ aspect_ratio_not_square: "kare bir imaj olmalı"
19
+ aspect_ratio_not_portrait: "dikey bir imaj olmalı"
20
+ aspect_ratio_not_landscape: "yatay bir imaj olmalı"
21
+ aspect_ratio_is_not: "%{aspect_ratio} en boy oranına sahip olmalı"
22
+ aspect_ratio_unknown: "bilinmeyen en boy oranı"
@@ -0,0 +1,22 @@
1
+ uk:
2
+ errors:
3
+ messages:
4
+ content_type_invalid: "має неприпустимий тип вмісту"
5
+ file_size_out_of_range: "розмір %{file_size} більше необхідного"
6
+ limit_out_of_range: "кількість файлів більше необхідного"
7
+ image_metadata_missing: "не є допустимим зображенням"
8
+ dimension_min_inclusion: "мусить бути більше або дорівнювати %{width} x %{height} пікселям"
9
+ dimension_max_inclusion: "мусить бути менше або дорівнювати %{width} x %{height} пікселям"
10
+ dimension_width_inclusion: "ширина не включена між %{min} і %{max} пікселям"
11
+ dimension_height_inclusion: "висота не включена між %{min} і %{max} пікселям"
12
+ dimension_width_greater_than_or_equal_to: "ширина мусить бути більше або дорівнювати %{length} пікселям"
13
+ dimension_height_greater_than_or_equal_to: "висота мусить бути більше або дорівнювати %{length} пікселям"
14
+ dimension_width_less_than_or_equal_to: "ширина мусить бути менше або дорівнювати %{length} пікселям"
15
+ dimension_height_less_than_or_equal_to: "висота мусить бути менше або дорівнювати %{length} пікселям"
16
+ dimension_width_equal_to: "ширина мусить дорівнювати %{length} пікселям"
17
+ dimension_height_equal_to: "висота мусить дорівнювати %{length} пікселям"
18
+ aspect_ratio_not_square: "мусить бути квадратне зображення"
19
+ aspect_ratio_not_portrait: "мусить бути портретне зображення"
20
+ aspect_ratio_not_landscape: "мусить бути пейзажне зображення"
21
+ aspect_ratio_is_not: "мусить мати співвідношення сторін %{aspect_ratio}"
22
+ aspect_ratio_unknown: "має невідоме співвідношення сторін"
@@ -0,0 +1,22 @@
1
+ vi:
2
+ errors:
3
+ messages:
4
+ content_type_invalid: "tệp không hợp lệ"
5
+ file_size_out_of_range: "kích thước %{file_size} vượt giới hạn"
6
+ limit_out_of_range: "tổng số tệp vượt giới hạn"
7
+ image_metadata_missing: "không phải là ảnh"
8
+ dimension_min_inclusion: "phải lớn hơn hoặc bằng %{width} x %{height} pixel"
9
+ dimension_max_inclusion: "phải nhỏ hơn hoặc bằng %{width} x %{height} pixel"
10
+ dimension_width_inclusion: "chiều rộng không nằm trong %{min} và %{max} pixel"
11
+ dimension_height_inclusion: "chiều cao không nằm trong %{min} và %{max} pixel"
12
+ dimension_width_greater_than_or_equal_to: "chiều rộng phải lớn hơn hoặc bằng %{length} pixel"
13
+ dimension_height_greater_than_or_equal_to: "chiều cao phải lớn hơn hoặc bằng %{length} pixel"
14
+ dimension_width_less_than_or_equal_to: "chiều rộng phải nhỏ hơn hoặc bằng %{length} pixel"
15
+ dimension_height_less_than_or_equal_to: "chiều cao phải nhỏ hơn hoặc bằng %{length} pixel"
16
+ dimension_width_equal_to: "chiều rộng phải bằng %{length} pixel"
17
+ dimension_height_equal_to: "chiều cao phải bằng %{length} pixel"
18
+ aspect_ratio_not_square: "phải là ảnh hình vuông"
19
+ aspect_ratio_not_portrait: "phải là ảnh đứng"
20
+ aspect_ratio_not_landscape: "phải là ảnh ngang"
21
+ aspect_ratio_is_not: "phải có tỉ lệ ảnh %{aspect_ratio}"
22
+ aspect_ratio_unknown: "tỉ lệ ảnh không xác định"
@@ -5,7 +5,10 @@ module ActiveStorageValidations
5
5
  def validate_each(record, attribute, _value)
6
6
  return if record.send(attribute).attached?
7
7
 
8
- record.errors.add(attribute, :blank)
8
+ errors_options = {}
9
+ errors_options[:message] = options[:message] if options[:message].present?
10
+
11
+ record.errors.add(attribute, :blank, **errors_options)
9
12
  end
10
13
  end
11
14
  end
@@ -14,19 +14,28 @@ module ActiveStorageValidations
14
14
  next if is_valid?(file)
15
15
 
16
16
  errors_options[:content_type] = content_type(file)
17
- record.errors.add(attribute, :content_type_invalid, errors_options)
17
+ record.errors.add(attribute, :content_type_invalid, **errors_options)
18
18
  break
19
19
  end
20
20
  end
21
21
 
22
22
  def types
23
23
  (Array.wrap(options[:with]) + Array.wrap(options[:in])).compact.map do |type|
24
- Mime[type] || type
24
+ if type.is_a?(Regexp)
25
+ type
26
+ elsif type.is_a?(String) && type =~ %r{\A\w+/[-+.\w]+\z} # mime-type-ish string
27
+ type
28
+ else
29
+ Mime[type] || raise(ArgumentError, "content_type must be one of Regxep,"\
30
+ " supported mime types (e.g. :png, 'jpg'), or mime type String ('image/jpeg')")
31
+ end
25
32
  end
26
33
  end
27
34
 
28
35
  def types_to_human_format
29
- types.join(', ')
36
+ types
37
+ .map { |type| type.to_s.split('/').last.upcase }
38
+ .join(', ')
30
39
  end
31
40
 
32
41
  def content_type(file)
@@ -34,10 +43,9 @@ module ActiveStorageValidations
34
43
  end
35
44
 
36
45
  def is_valid?(file)
37
- if options[:with].is_a?(Regexp)
38
- options[:with].match?(content_type(file).to_s)
39
- else
40
- content_type(file).in?(types)
46
+ file_type = content_type(file)
47
+ types.any? do |type|
48
+ type == file_type || (type.is_a?(Regexp) && type.match?(file_type.to_s))
41
49
  end
42
50
  end
43
51
  end
@@ -52,7 +52,7 @@ module ActiveStorageValidations
52
52
  # Rails 5
53
53
  def validate_each(record, attribute, _value)
54
54
  return true unless record.send(attribute).attached?
55
-
55
+
56
56
  files = Array.wrap(record.send(attribute))
57
57
  files.each do |file|
58
58
  # Analyze file first if not analyzed to get all required metadata.
@@ -110,7 +110,7 @@ module ActiveStorageValidations
110
110
  else
111
111
  if file_metadata[length] != options[length]
112
112
  add_error(record, attribute, options[:message].presence || :"dimension_#{length}_equal_to", length: options[length])
113
- return false
113
+ return false
114
114
  end
115
115
  end
116
116
  end
@@ -119,11 +119,11 @@ module ActiveStorageValidations
119
119
  true # valid file
120
120
  end
121
121
 
122
- def add_error(record, attribute, type, *attrs)
122
+ def add_error(record, attribute, type, **attrs)
123
123
  key = options[:message].presence || type
124
124
  return if record.errors.added?(attribute, key)
125
- record.errors.add(attribute, key, *attrs)
126
- end
125
+ record.errors.add(attribute, key, **attrs)
126
+ end
127
127
 
128
128
  end
129
129
  end
@@ -10,18 +10,14 @@ module ActiveStorageValidations
10
10
  raise ArgumentError, 'You must pass either :max or :min to the validator'
11
11
  end
12
12
 
13
- def validate_each(record, attribute, _value)
13
+ def validate_each(record, attribute, _)
14
14
  return true unless record.send(attribute).attached?
15
15
 
16
- files = Array.wrap(record.send(attribute))
17
-
18
- errors_options = {}
19
- errors_options[:min] = options[:min]
20
- errors_options[:max] = options[:max]
16
+ files = Array.wrap(record.send(attribute)).compact.uniq
17
+ errors_options = { min: options[:min], max: options[:max] }
21
18
 
22
19
  return true if files_count_valid?(files.count)
23
-
24
- record.errors.add(attribute, options[:message].presence || :limit_out_of_range, errors_options)
20
+ record.errors.add(attribute, options[:message].presence || :limit_out_of_range, **errors_options)
25
21
  end
26
22
 
27
23
  def files_count_valid?(count)
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_storage_validations/matchers/attached_validator_matcher'
4
+ require 'active_storage_validations/matchers/content_type_validator_matcher'
5
+ require 'active_storage_validations/matchers/dimension_validator_matcher'
6
+ require 'active_storage_validations/matchers/size_validator_matcher'
7
+
8
+ module ActiveStorageValidations
9
+ module Matchers
10
+ # Helper to stub a method with either RSpec or Minitest (whatever is available)
11
+ def self.stub_method(object, method, result)
12
+ if defined?(Minitest::Mock)
13
+ object.stub(method, result) do
14
+ yield
15
+ end
16
+ elsif defined?(RSpec::Mocks)
17
+ RSpec::Mocks.allow_message(object, method) { result }
18
+ yield
19
+ else
20
+ raise 'Need either Minitest::Mock or RSpec::Mocks to run this validator matcher'
21
+ end
22
+ end
23
+
24
+ def self.mock_metadata(attachment, width, height)
25
+ if Rails::VERSION::MAJOR >= 6
26
+ # Mock the Metadata class for rails 6
27
+ mock = OpenStruct.new(metadata: { width: width, height: height })
28
+ stub_method(ActiveStorageValidations::Metadata, :new, mock) do
29
+ yield
30
+ end
31
+ else
32
+ # Stub the metadata analysis for rails 5
33
+ stub_method(attachment, :analyze, true) do
34
+ stub_method(attachment, :analyzed?, true) do
35
+ stub_method(attachment, :metadata, { width: width, height: height }) do
36
+ yield
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveStorageValidations
4
+ module Matchers
5
+ def validate_attached_of(name)
6
+ AttachedValidatorMatcher.new(name)
7
+ end
8
+
9
+ class AttachedValidatorMatcher
10
+ def initialize(attribute_name)
11
+ @attribute_name = attribute_name
12
+ end
13
+
14
+ def description
15
+ "validate #{@attribute_name} must be attached"
16
+ end
17
+
18
+ def matches?(subject)
19
+ @subject = subject.is_a?(Class) ? subject.new : subject
20
+ responds_to_methods && valid_when_attached && invalid_when_not_attached
21
+ end
22
+
23
+ def failure_message
24
+ "is expected to validate attached of #{@attribute_name}"
25
+ end
26
+
27
+ def failure_message_when_negated
28
+ "is expected to not validate attached of #{@attribute_name}"
29
+ end
30
+
31
+ private
32
+
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)
37
+ end
38
+
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)
43
+ end
44
+
45
+ def invalid_when_not_attached
46
+ @subject.public_send(@attribute_name).detach
47
+ # Unset the direct relation since `detach` on an unpersisted record does not set `attached?` to false.
48
+ @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
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,101 @@
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_content_type_matcher.rb
5
+ module ActiveStorageValidations
6
+ module Matchers
7
+ def validate_content_type_of(name)
8
+ ContentTypeValidatorMatcher.new(name)
9
+ end
10
+
11
+ class ContentTypeValidatorMatcher
12
+ def initialize(attribute_name)
13
+ @attribute_name = attribute_name
14
+ end
15
+
16
+ def description
17
+ "validate the content types allowed on attachment #{@attribute_name}"
18
+ end
19
+
20
+ def allowing(*types)
21
+ @allowed_types = types.flatten
22
+ self
23
+ end
24
+
25
+ def rejecting(*types)
26
+ @rejected_types = types.flatten
27
+ self
28
+ end
29
+
30
+ def matches?(subject)
31
+ @subject = subject.is_a?(Class) ? subject.new : subject
32
+ responds_to_methods && allowed_types_allowed? && rejected_types_rejected?
33
+ end
34
+
35
+ def failure_message
36
+ <<~MESSAGE
37
+ Expected #{@attribute_name}
38
+
39
+ Accept content types: #{allowed_types.join(", ")}
40
+ #{accepted_types_and_failures}
41
+
42
+ Reject content types: #{rejected_types.join(", ")}
43
+ #{rejected_types_and_failures}
44
+ MESSAGE
45
+ end
46
+
47
+ protected
48
+
49
+ def responds_to_methods
50
+ @subject.respond_to?(@attribute_name) &&
51
+ @subject.public_send(@attribute_name).respond_to?(:attach) &&
52
+ @subject.public_send(@attribute_name).respond_to?(:detach)
53
+ end
54
+
55
+ def allowed_types
56
+ @allowed_types || []
57
+ end
58
+
59
+ def rejected_types
60
+ @rejected_types || (Mime::LOOKUP.keys - allowed_types)
61
+ end
62
+
63
+ def allowed_types_allowed?
64
+ @missing_allowed_types ||= allowed_types.reject { |type| type_allowed?(type) }
65
+ @missing_allowed_types.none?
66
+ end
67
+
68
+ def rejected_types_rejected?
69
+ @missing_rejected_types ||= rejected_types.select { |type| type_allowed?(type) }
70
+ @missing_rejected_types.none?
71
+ end
72
+
73
+ def accepted_types_and_failures
74
+ if @missing_allowed_types.present?
75
+ "#{@missing_allowed_types.join(", ")} were rejected."
76
+ else
77
+ "All were accepted successfully."
78
+ end
79
+ end
80
+
81
+ def rejected_types_and_failures
82
+ if @missing_rejected_types.present?
83
+ "#{@missing_rejected_types.join(", ")} were accepted."
84
+ else
85
+ "All were rejected successfully."
86
+ end
87
+ end
88
+
89
+ def type_allowed?(type)
90
+ @subject.public_send(@attribute_name).attach(attachment_for(type))
91
+ @subject.validate
92
+ @subject.errors.details[@attribute_name].all? { |error| error[:error] != :content_type_invalid }
93
+ end
94
+
95
+ def attachment_for(type)
96
+ suffix = type.to_s.split('/').last
97
+ { io: Tempfile.new('.'), filename: "test.#{suffix}", content_type: type }
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveStorageValidations
4
+ module Matchers
5
+ def validate_dimensions_of(name)
6
+ DimensionValidatorMatcher.new(name)
7
+ end
8
+
9
+ class DimensionValidatorMatcher
10
+ def initialize(attribute_name)
11
+ @attribute_name = attribute_name
12
+ @width_min = @width_max = @height_min = @height_max = nil
13
+ @custom_message = nil
14
+ end
15
+
16
+ def description
17
+ "validate image dimensions of #{@attribute_name}"
18
+ end
19
+
20
+ def width_min(width)
21
+ @width_min = width
22
+ self
23
+ end
24
+
25
+ def width_max(width)
26
+ @width_max = width
27
+ self
28
+ end
29
+
30
+ def with_message(message)
31
+ @custom_message = message
32
+ self
33
+ end
34
+
35
+ def width(width)
36
+ @width_min = @width_max = width
37
+ self
38
+ end
39
+
40
+ def height_min(height)
41
+ @height_min = height
42
+ self
43
+ end
44
+
45
+ def height_max(height)
46
+ @height_max = height
47
+ self
48
+ end
49
+
50
+ def width_between(range)
51
+ @width_min, @width_max = range.first, range.last
52
+ self
53
+ end
54
+
55
+ def height_between(range)
56
+ @height_min, @height_max = range.first, range.last
57
+ self
58
+ end
59
+
60
+ def height(height)
61
+ @height_min = @height_max = height
62
+ self
63
+ end
64
+
65
+ def matches?(subject)
66
+ @subject = subject.is_a?(Class) ? subject.new : subject
67
+ responds_to_methods &&
68
+ width_smaller_than_min? && width_larger_than_min? && width_smaller_than_max? && width_larger_than_max? && width_equals? &&
69
+ height_smaller_than_min? && height_larger_than_min? && height_smaller_than_max? && height_larger_than_max? && height_equals?
70
+ end
71
+
72
+ def failure_message
73
+ <<~MESSAGE
74
+ is expected to validate dimensions of #{@attribute_name}
75
+ width between #{@width_min} and #{@width_max}
76
+ height between #{@height_min} and #{@height_max}
77
+ MESSAGE
78
+ end
79
+
80
+ protected
81
+
82
+ def responds_to_methods
83
+ @subject.respond_to?(@attribute_name) &&
84
+ @subject.public_send(@attribute_name).respond_to?(:attach) &&
85
+ @subject.public_send(@attribute_name).respond_to?(:detach)
86
+ end
87
+
88
+ def valid_width
89
+ ((@width_min || 0) + (@width_max || 2000)) / 2
90
+ end
91
+
92
+ def valid_height
93
+ ((@height_min || 0) + (@height_max || 2000)) / 2
94
+ end
95
+
96
+ def width_smaller_than_min?
97
+ @width_min.nil? || !passes_validation_with_dimensions(@width_min - 1, valid_height, 'width')
98
+ end
99
+
100
+ def width_larger_than_min?
101
+ @width_min.nil? || @width_min == @width_max || passes_validation_with_dimensions(@width_min + 1, valid_height, 'width')
102
+ end
103
+
104
+ def width_smaller_than_max?
105
+ @width_max.nil? || @width_min == @width_max || passes_validation_with_dimensions(@width_max - 1, valid_height, 'width')
106
+ end
107
+
108
+ def width_larger_than_max?
109
+ @width_max.nil? || !passes_validation_with_dimensions(@width_max + 1, valid_height, 'width')
110
+ end
111
+
112
+ def width_equals?
113
+ @width_min.nil? || @width_min != @width_max || passes_validation_with_dimensions(@width_min, valid_height, 'width')
114
+ end
115
+
116
+ def height_smaller_than_min?
117
+ @height_min.nil? || !passes_validation_with_dimensions(valid_width, @height_min - 1, 'height')
118
+ end
119
+
120
+ def height_larger_than_min?
121
+ @height_min.nil? || @height_min == @height_max || passes_validation_with_dimensions(valid_width, @height_min + 1, 'height')
122
+ end
123
+
124
+ def height_smaller_than_max?
125
+ @height_max.nil? || @height_min == @height_max || passes_validation_with_dimensions(valid_width, @height_max - 1, 'height')
126
+ end
127
+
128
+ def height_larger_than_max?
129
+ @height_max.nil? || !passes_validation_with_dimensions(valid_width, @height_max + 1, 'height')
130
+ end
131
+
132
+ def height_equals?
133
+ @height_min.nil? || @height_min != @height_max || passes_validation_with_dimensions(valid_width, @height_min, 'height')
134
+ end
135
+
136
+ def passes_validation_with_dimensions(width, height, check)
137
+ @subject.public_send(@attribute_name).attach attachment_for(width, height)
138
+
139
+ attachment = @subject.public_send(@attribute_name)
140
+ Matchers.mock_metadata(attachment, width, height) do
141
+ @subject.validate
142
+ exclude_error_message = @custom_message || "dimension_#{check}"
143
+ @subject.errors.details[@attribute_name].all? { |error| error[:error].to_s.exclude?(exclude_error_message) }
144
+ end
145
+ end
146
+
147
+ def attachment_for(width, height)
148
+ { io: Tempfile.new('Hello world!'), filename: 'test.png', content_type: 'image/png' }
149
+ end
150
+ end
151
+ end
152
+ end