active_storage_validations 0.8.2 → 0.9.4

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