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.
- checksums.yaml +4 -4
- data/README.md +133 -69
- data/config/locales/da.yml +33 -0
- data/config/locales/de.yml +5 -0
- data/config/locales/en.yml +5 -0
- data/config/locales/es.yml +5 -0
- data/config/locales/fr.yml +5 -0
- data/config/locales/it.yml +5 -0
- data/config/locales/ja.yml +5 -0
- data/config/locales/nl.yml +5 -0
- data/config/locales/pl.yml +5 -0
- data/config/locales/pt-BR.yml +5 -0
- data/config/locales/ru.yml +5 -0
- data/config/locales/sv.yml +10 -1
- data/config/locales/tr.yml +5 -0
- data/config/locales/uk.yml +5 -0
- data/config/locales/vi.yml +5 -0
- data/config/locales/zh-CN.yml +5 -0
- data/lib/active_storage_validations/aspect_ratio_validator.rb +47 -22
- data/lib/active_storage_validations/attached_validator.rb +12 -3
- data/lib/active_storage_validations/base_size_validator.rb +66 -0
- data/lib/active_storage_validations/concerns/errorable.rb +38 -0
- data/lib/active_storage_validations/concerns/symbolizable.rb +8 -6
- data/lib/active_storage_validations/content_type_validator.rb +41 -6
- data/lib/active_storage_validations/dimension_validator.rb +15 -15
- data/lib/active_storage_validations/limit_validator.rb +44 -7
- data/lib/active_storage_validations/matchers/aspect_ratio_validator_matcher.rb +119 -0
- data/lib/active_storage_validations/matchers/attached_validator_matcher.rb +25 -36
- data/lib/active_storage_validations/matchers/base_size_validator_matcher.rb +134 -0
- data/lib/active_storage_validations/matchers/concerns/active_storageable.rb +17 -0
- data/lib/active_storage_validations/matchers/concerns/allow_blankable.rb +26 -0
- data/lib/active_storage_validations/matchers/concerns/attachable.rb +48 -0
- data/lib/active_storage_validations/matchers/concerns/contextable.rb +47 -0
- data/lib/active_storage_validations/matchers/concerns/messageable.rb +26 -0
- data/lib/active_storage_validations/matchers/concerns/rspecable.rb +25 -0
- data/lib/active_storage_validations/matchers/concerns/validatable.rb +11 -10
- data/lib/active_storage_validations/matchers/content_type_validator_matcher.rb +44 -27
- data/lib/active_storage_validations/matchers/dimension_validator_matcher.rb +67 -59
- data/lib/active_storage_validations/matchers/processable_image_validator_matcher.rb +78 -0
- data/lib/active_storage_validations/matchers/size_validator_matcher.rb +8 -126
- data/lib/active_storage_validations/matchers/total_size_validator_matcher.rb +40 -0
- data/lib/active_storage_validations/matchers.rb +3 -0
- data/lib/active_storage_validations/metadata.rb +60 -28
- data/lib/active_storage_validations/processable_image_validator.rb +14 -5
- data/lib/active_storage_validations/size_validator.rb +7 -51
- data/lib/active_storage_validations/total_size_validator.rb +49 -0
- data/lib/active_storage_validations/version.rb +1 -1
- data/lib/active_storage_validations.rb +3 -2
- metadata +38 -39
- data/lib/active_storage_validations/error_handler.rb +0 -21
@@ -1,24 +1,50 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative 'concerns/active_storageable.rb'
|
4
|
+
require_relative 'concerns/allow_blankable.rb'
|
5
|
+
require_relative 'concerns/attachable'
|
6
|
+
require_relative 'concerns/contextable.rb'
|
7
|
+
require_relative 'concerns/messageable.rb'
|
8
|
+
require_relative 'concerns/rspecable.rb'
|
3
9
|
require_relative 'concerns/validatable.rb'
|
4
10
|
|
5
11
|
module ActiveStorageValidations
|
6
12
|
module Matchers
|
7
|
-
def validate_dimensions_of(
|
8
|
-
DimensionValidatorMatcher.new(
|
13
|
+
def validate_dimensions_of(attribute_name)
|
14
|
+
DimensionValidatorMatcher.new(attribute_name)
|
9
15
|
end
|
10
16
|
|
11
17
|
class DimensionValidatorMatcher
|
18
|
+
include ActiveStorageable
|
19
|
+
include AllowBlankable
|
20
|
+
include Attachable
|
21
|
+
include Contextable
|
22
|
+
include Messageable
|
23
|
+
include Rspecable
|
12
24
|
include Validatable
|
13
25
|
|
14
26
|
def initialize(attribute_name)
|
27
|
+
initialize_allow_blankable
|
28
|
+
initialize_contextable
|
29
|
+
initialize_messageable
|
30
|
+
initialize_rspecable
|
15
31
|
@attribute_name = attribute_name
|
16
32
|
@width_min = @width_max = @height_min = @height_max = nil
|
17
|
-
@custom_message = nil
|
18
33
|
end
|
19
34
|
|
20
35
|
def description
|
21
|
-
"validate image dimensions of
|
36
|
+
"validate the image dimensions of :#{@attribute_name}"
|
37
|
+
end
|
38
|
+
|
39
|
+
def failure_message
|
40
|
+
message = ["is expected to validate dimensions of :#{@attribute_name}"]
|
41
|
+
build_failure_message(message)
|
42
|
+
message.join("\n")
|
43
|
+
end
|
44
|
+
|
45
|
+
def width(width)
|
46
|
+
@width_min = @width_max = width
|
47
|
+
self
|
22
48
|
end
|
23
49
|
|
24
50
|
def width_min(width)
|
@@ -31,13 +57,13 @@ module ActiveStorageValidations
|
|
31
57
|
self
|
32
58
|
end
|
33
59
|
|
34
|
-
def
|
35
|
-
@
|
60
|
+
def width_between(range)
|
61
|
+
@width_min, @width_max = range.first, range.last
|
36
62
|
self
|
37
63
|
end
|
38
64
|
|
39
|
-
def
|
40
|
-
@
|
65
|
+
def height(height)
|
66
|
+
@height_min = @height_max = height
|
41
67
|
self
|
42
68
|
end
|
43
69
|
|
@@ -51,25 +77,18 @@ module ActiveStorageValidations
|
|
51
77
|
self
|
52
78
|
end
|
53
79
|
|
54
|
-
def width_between(range)
|
55
|
-
@width_min, @width_max = range.first, range.last
|
56
|
-
self
|
57
|
-
end
|
58
|
-
|
59
80
|
def height_between(range)
|
60
81
|
@height_min, @height_max = range.first, range.last
|
61
82
|
self
|
62
83
|
end
|
63
84
|
|
64
|
-
def height(height)
|
65
|
-
@height_min = @height_max = height
|
66
|
-
self
|
67
|
-
end
|
68
|
-
|
69
85
|
def matches?(subject)
|
70
86
|
@subject = subject.is_a?(Class) ? subject.new : subject
|
71
87
|
|
72
|
-
|
88
|
+
is_a_valid_active_storage_attribute? &&
|
89
|
+
is_context_valid? &&
|
90
|
+
is_allowing_blank? &&
|
91
|
+
is_custom_message_valid? &&
|
73
92
|
width_not_smaller_than_min? &&
|
74
93
|
width_larger_than_min? &&
|
75
94
|
width_smaller_than_max? &&
|
@@ -79,24 +98,19 @@ module ActiveStorageValidations
|
|
79
98
|
height_larger_than_min? &&
|
80
99
|
height_smaller_than_max? &&
|
81
100
|
height_not_larger_than_max? &&
|
82
|
-
height_equals?
|
83
|
-
validate_custom_message?
|
84
|
-
end
|
85
|
-
|
86
|
-
def failure_message
|
87
|
-
<<~MESSAGE
|
88
|
-
is expected to validate dimensions of #{@attribute_name}
|
89
|
-
width between #{@width_min} and #{@width_max}
|
90
|
-
height between #{@height_min} and #{@height_max}
|
91
|
-
MESSAGE
|
101
|
+
height_equals?
|
92
102
|
end
|
93
103
|
|
94
104
|
protected
|
95
105
|
|
96
|
-
def
|
97
|
-
@
|
98
|
-
|
99
|
-
|
106
|
+
def build_failure_message(message)
|
107
|
+
return unless @failure_message_artefacts.present?
|
108
|
+
|
109
|
+
message << " but there seem to have issues with the matcher methods you used, since:"
|
110
|
+
@failure_message_artefacts.each do |error_case|
|
111
|
+
message << " validation failed when provided with a #{error_case[:width]}x#{error_case[:height]}px test image"
|
112
|
+
end
|
113
|
+
message << " whereas it should have passed"
|
100
114
|
end
|
101
115
|
|
102
116
|
def valid_width
|
@@ -108,57 +122,64 @@ module ActiveStorageValidations
|
|
108
122
|
end
|
109
123
|
|
110
124
|
def width_not_smaller_than_min?
|
111
|
-
@width_min.nil? || !passes_validation_with_dimensions(@width_min - 1, valid_height
|
125
|
+
@width_min.nil? || !passes_validation_with_dimensions(@width_min - 1, valid_height)
|
112
126
|
end
|
113
127
|
|
114
128
|
def width_larger_than_min?
|
115
|
-
@width_min.nil? || @width_min == @width_max || passes_validation_with_dimensions(@width_min + 1, valid_height
|
129
|
+
@width_min.nil? || @width_min == @width_max || passes_validation_with_dimensions(@width_min + 1, valid_height)
|
116
130
|
end
|
117
131
|
|
118
132
|
def width_smaller_than_max?
|
119
|
-
@width_max.nil? || @width_min == @width_max || passes_validation_with_dimensions(@width_max - 1, valid_height
|
133
|
+
@width_max.nil? || @width_min == @width_max || passes_validation_with_dimensions(@width_max - 1, valid_height)
|
120
134
|
end
|
121
135
|
|
122
136
|
def width_not_larger_than_max?
|
123
|
-
@width_max.nil? || !passes_validation_with_dimensions(@width_max + 1, valid_height
|
137
|
+
@width_max.nil? || !passes_validation_with_dimensions(@width_max + 1, valid_height)
|
124
138
|
end
|
125
139
|
|
126
140
|
def width_equals?
|
127
|
-
@width_min.nil? || @width_min != @width_max || passes_validation_with_dimensions(@width_min, valid_height
|
141
|
+
@width_min.nil? || @width_min != @width_max || passes_validation_with_dimensions(@width_min, valid_height)
|
128
142
|
end
|
129
143
|
|
130
144
|
def height_not_smaller_than_min?
|
131
|
-
@height_min.nil? || !passes_validation_with_dimensions(valid_width, @height_min - 1
|
145
|
+
@height_min.nil? || !passes_validation_with_dimensions(valid_width, @height_min - 1)
|
132
146
|
end
|
133
147
|
|
134
148
|
def height_larger_than_min?
|
135
|
-
@height_min.nil? || @height_min == @height_max || passes_validation_with_dimensions(valid_width, @height_min + 1
|
149
|
+
@height_min.nil? || @height_min == @height_max || passes_validation_with_dimensions(valid_width, @height_min + 1)
|
136
150
|
end
|
137
151
|
|
138
152
|
def height_smaller_than_max?
|
139
|
-
@height_max.nil? || @height_min == @height_max || passes_validation_with_dimensions(valid_width, @height_max - 1
|
153
|
+
@height_max.nil? || @height_min == @height_max || passes_validation_with_dimensions(valid_width, @height_max - 1)
|
140
154
|
end
|
141
155
|
|
142
156
|
def height_not_larger_than_max?
|
143
|
-
@height_max.nil? || !passes_validation_with_dimensions(valid_width, @height_max + 1
|
157
|
+
@height_max.nil? || !passes_validation_with_dimensions(valid_width, @height_max + 1)
|
144
158
|
end
|
145
159
|
|
146
160
|
def height_equals?
|
147
|
-
@height_min.nil? || @height_min != @height_max || passes_validation_with_dimensions(valid_width, @height_min
|
161
|
+
@height_min.nil? || @height_min != @height_max || passes_validation_with_dimensions(valid_width, @height_min)
|
148
162
|
end
|
149
163
|
|
150
|
-
def passes_validation_with_dimensions(width, height
|
164
|
+
def passes_validation_with_dimensions(width, height)
|
151
165
|
mock_dimensions_for(attach_file, width, height) do
|
152
166
|
validate
|
153
|
-
|
167
|
+
detach_file
|
168
|
+
is_valid? || add_failure_message_artefact(width, height)
|
154
169
|
end
|
155
170
|
end
|
156
171
|
|
157
|
-
def
|
172
|
+
def add_failure_message_artefact(width, height)
|
173
|
+
@failure_message_artefacts << { width: width, height: height }
|
174
|
+
false
|
175
|
+
end
|
176
|
+
|
177
|
+
def is_custom_message_valid?
|
158
178
|
return true unless @custom_message
|
159
179
|
|
160
180
|
mock_dimensions_for(attach_file, -1, -1) do
|
161
181
|
validate
|
182
|
+
detach_file
|
162
183
|
has_an_error_message_which_is_custom_message?
|
163
184
|
end
|
164
185
|
end
|
@@ -168,19 +189,6 @@ module ActiveStorageValidations
|
|
168
189
|
yield
|
169
190
|
end
|
170
191
|
end
|
171
|
-
|
172
|
-
def attach_file
|
173
|
-
@subject.public_send(@attribute_name).attach(dummy_file)
|
174
|
-
@subject.public_send(@attribute_name)
|
175
|
-
end
|
176
|
-
|
177
|
-
def dummy_file
|
178
|
-
{
|
179
|
-
io: Tempfile.new('Hello world!'),
|
180
|
-
filename: 'test.png',
|
181
|
-
content_type: 'image/png'
|
182
|
-
}
|
183
|
-
end
|
184
192
|
end
|
185
193
|
end
|
186
194
|
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
|
@@ -1,140 +1,22 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
# https://github.com/thoughtbot/paperclip/blob/v6.1.0/lib/paperclip/matchers/validate_attachment_size_matcher.rb
|
5
|
-
|
6
|
-
require_relative 'concerns/validatable.rb'
|
3
|
+
require_relative 'base_size_validator_matcher'
|
7
4
|
|
8
5
|
module ActiveStorageValidations
|
9
6
|
module Matchers
|
10
|
-
def validate_size_of(
|
11
|
-
SizeValidatorMatcher.new(
|
7
|
+
def validate_size_of(attribute_name)
|
8
|
+
SizeValidatorMatcher.new(attribute_name)
|
12
9
|
end
|
13
10
|
|
14
|
-
class SizeValidatorMatcher
|
15
|
-
include Validatable
|
16
|
-
|
17
|
-
def initialize(attribute_name)
|
18
|
-
@attribute_name = attribute_name
|
19
|
-
@min = @max = nil
|
20
|
-
@custom_message = nil
|
21
|
-
end
|
22
|
-
|
11
|
+
class SizeValidatorMatcher < BaseSizeValidatorMatcher
|
23
12
|
def description
|
24
|
-
"validate file size of
|
25
|
-
end
|
26
|
-
|
27
|
-
def less_than(size)
|
28
|
-
@max = size - 1.byte
|
29
|
-
self
|
30
|
-
end
|
31
|
-
|
32
|
-
def less_than_or_equal_to(size)
|
33
|
-
@max = size
|
34
|
-
self
|
35
|
-
end
|
36
|
-
|
37
|
-
def greater_than(size)
|
38
|
-
@min = size + 1.byte
|
39
|
-
self
|
40
|
-
end
|
41
|
-
|
42
|
-
def greater_than_or_equal_to(size)
|
43
|
-
@min = size
|
44
|
-
self
|
45
|
-
end
|
46
|
-
|
47
|
-
def between(range)
|
48
|
-
@min, @max = range.first, range.last
|
49
|
-
self
|
50
|
-
end
|
51
|
-
|
52
|
-
def with_message(message)
|
53
|
-
@custom_message = message
|
54
|
-
self
|
55
|
-
end
|
56
|
-
|
57
|
-
def matches?(subject)
|
58
|
-
@subject = subject.is_a?(Class) ? subject.new : subject
|
59
|
-
|
60
|
-
responds_to_methods &&
|
61
|
-
not_lower_than_min? &&
|
62
|
-
higher_than_min? &&
|
63
|
-
lower_than_max? &&
|
64
|
-
not_higher_than_max? &&
|
65
|
-
validate_custom_message?
|
13
|
+
"validate file size of :#{@attribute_name}"
|
66
14
|
end
|
67
15
|
|
68
16
|
def failure_message
|
69
|
-
"is expected to validate file size of
|
70
|
-
|
71
|
-
|
72
|
-
def failure_message_when_negated
|
73
|
-
"is expected to not validate file size of #{@attribute_name} to be between #{@min} and #{@max} bytes"
|
74
|
-
end
|
75
|
-
|
76
|
-
protected
|
77
|
-
|
78
|
-
def responds_to_methods
|
79
|
-
@subject.respond_to?(@attribute_name) &&
|
80
|
-
@subject.public_send(@attribute_name).respond_to?(:attach) &&
|
81
|
-
@subject.public_send(@attribute_name).respond_to?(:detach)
|
82
|
-
end
|
83
|
-
|
84
|
-
def not_lower_than_min?
|
85
|
-
@min.nil? || !passes_validation_with_size(@min - 1)
|
86
|
-
end
|
87
|
-
|
88
|
-
def higher_than_min?
|
89
|
-
@min.nil? || passes_validation_with_size(@min + 1)
|
90
|
-
end
|
91
|
-
|
92
|
-
def lower_than_max?
|
93
|
-
@max.nil? || @max == Float::INFINITY || passes_validation_with_size(@max - 1)
|
94
|
-
end
|
95
|
-
|
96
|
-
def not_higher_than_max?
|
97
|
-
@max.nil? || @max == Float::INFINITY || !passes_validation_with_size(@max + 1)
|
98
|
-
end
|
99
|
-
|
100
|
-
def passes_validation_with_size(size)
|
101
|
-
mock_size_for(io, size) do
|
102
|
-
attach_file
|
103
|
-
validate
|
104
|
-
is_valid?
|
105
|
-
end
|
106
|
-
end
|
107
|
-
|
108
|
-
def validate_custom_message?
|
109
|
-
return true unless @custom_message
|
110
|
-
|
111
|
-
mock_size_for(io, -1.kilobytes) do
|
112
|
-
attach_file
|
113
|
-
validate
|
114
|
-
has_an_error_message_which_is_custom_message?
|
115
|
-
end
|
116
|
-
end
|
117
|
-
|
118
|
-
def mock_size_for(io, size)
|
119
|
-
Matchers.stub_method(io, :size, size) do
|
120
|
-
yield
|
121
|
-
end
|
122
|
-
end
|
123
|
-
|
124
|
-
def attach_file
|
125
|
-
@subject.public_send(@attribute_name).attach(dummy_file)
|
126
|
-
end
|
127
|
-
|
128
|
-
def dummy_file
|
129
|
-
{
|
130
|
-
io: io,
|
131
|
-
filename: 'test.png',
|
132
|
-
content_type: 'image/png'
|
133
|
-
}
|
134
|
-
end
|
135
|
-
|
136
|
-
def io
|
137
|
-
@io ||= Tempfile.new('Hello world!')
|
17
|
+
message = ["is expected to validate file size of :#{@attribute_name}"]
|
18
|
+
build_failure_message(message)
|
19
|
+
message.join("\n")
|
138
20
|
end
|
139
21
|
end
|
140
22
|
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'base_size_validator_matcher'
|
4
|
+
|
5
|
+
module ActiveStorageValidations
|
6
|
+
module Matchers
|
7
|
+
def validate_total_size_of(attribute_name)
|
8
|
+
TotalSizeValidatorMatcher.new(attribute_name)
|
9
|
+
end
|
10
|
+
|
11
|
+
class TotalSizeValidatorMatcher < BaseSizeValidatorMatcher
|
12
|
+
def description
|
13
|
+
"validate total file size of :#{@attribute_name}"
|
14
|
+
end
|
15
|
+
|
16
|
+
def failure_message
|
17
|
+
message = ["is expected to validate total file size of :#{@attribute_name}"]
|
18
|
+
build_failure_message(message)
|
19
|
+
message.join("\n")
|
20
|
+
end
|
21
|
+
|
22
|
+
protected
|
23
|
+
|
24
|
+
def attach_file
|
25
|
+
# We attach blobs instead of io for has_many_attached relation
|
26
|
+
@subject.public_send(@attribute_name).attach([dummy_blob])
|
27
|
+
@subject.public_send(@attribute_name)
|
28
|
+
end
|
29
|
+
|
30
|
+
def dummy_blob
|
31
|
+
ActiveStorage::Blob.create_and_upload!(
|
32
|
+
io: io,
|
33
|
+
filename: 'test.png',
|
34
|
+
content_type: 'image/png',
|
35
|
+
service_name: 'test'
|
36
|
+
)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -1,9 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'active_storage_validations/matchers/aspect_ratio_validator_matcher'
|
3
4
|
require 'active_storage_validations/matchers/attached_validator_matcher'
|
5
|
+
require 'active_storage_validations/matchers/processable_image_validator_matcher'
|
4
6
|
require 'active_storage_validations/matchers/content_type_validator_matcher'
|
5
7
|
require 'active_storage_validations/matchers/dimension_validator_matcher'
|
6
8
|
require 'active_storage_validations/matchers/size_validator_matcher'
|
9
|
+
require 'active_storage_validations/matchers/total_size_validator_matcher'
|
7
10
|
|
8
11
|
module ActiveStorageValidations
|
9
12
|
module Matchers
|
@@ -4,29 +4,18 @@ module ActiveStorageValidations
|
|
4
4
|
|
5
5
|
attr_reader :file
|
6
6
|
|
7
|
+
DEFAULT_IMAGE_PROCESSOR = :mini_magick.freeze
|
8
|
+
|
7
9
|
def initialize(file)
|
8
10
|
require_image_processor
|
9
11
|
@file = file
|
10
12
|
end
|
11
13
|
|
12
|
-
def
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
if image_processor == :vips && defined?(Vips)
|
18
|
-
Vips::Error
|
19
|
-
elsif defined?(MiniMagick)
|
20
|
-
MiniMagick::Error
|
21
|
-
end
|
22
|
-
end
|
23
|
-
|
24
|
-
def require_image_processor
|
25
|
-
if image_processor == :vips
|
26
|
-
require 'vips' unless defined?(Vips)
|
27
|
-
else
|
28
|
-
require 'mini_magick' unless defined?(MiniMagick)
|
29
|
-
end
|
14
|
+
def valid?
|
15
|
+
read_image
|
16
|
+
true
|
17
|
+
rescue InvalidImageError
|
18
|
+
false
|
30
19
|
end
|
31
20
|
|
32
21
|
def metadata
|
@@ -42,14 +31,31 @@ module ActiveStorageValidations
|
|
42
31
|
{}
|
43
32
|
end
|
44
33
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
34
|
+
private
|
35
|
+
|
36
|
+
def image_processor
|
37
|
+
# Rails returns nil for default image processor, because it is set in an after initialize callback
|
38
|
+
# https://github.com/rails/rails/blob/89d8569abe2564c8187debf32dd3b4e33d6ad983/activestorage/lib/active_storage/engine.rb
|
39
|
+
Rails.application.config.active_storage.variant_processor || DEFAULT_IMAGE_PROCESSOR
|
50
40
|
end
|
51
41
|
|
52
|
-
|
42
|
+
def require_image_processor
|
43
|
+
case image_processor
|
44
|
+
when :vips then require 'vips' unless defined?(Vips)
|
45
|
+
when :mini_magick then require 'mini_magick' unless defined?(MiniMagick)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def exception_class
|
50
|
+
case image_processor
|
51
|
+
when :vips then Vips::Error
|
52
|
+
when :mini_magick then MiniMagick::Error
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def vips_image_processor?
|
57
|
+
image_processor == :vips
|
58
|
+
end
|
53
59
|
|
54
60
|
def read_image
|
55
61
|
is_string = file.is_a?(String)
|
@@ -95,23 +101,49 @@ module ActiveStorageValidations
|
|
95
101
|
end
|
96
102
|
|
97
103
|
def new_image_from_path(path)
|
98
|
-
if
|
99
|
-
|
104
|
+
if vips_image_processor? && (supported_vips_suffix?(path) || vips_version_below_8_8? || open_uri_tempfile?(path))
|
105
|
+
begin
|
106
|
+
Vips::Image.new_from_file(path)
|
107
|
+
rescue exception_class
|
108
|
+
# We handle cases where an error is raised when reading the file
|
109
|
+
# because Vips can throw errors rather than returning false
|
110
|
+
# We stumble upon this issue while reading 0 byte size file
|
111
|
+
# https://github.com/janko/image_processing/issues/97
|
112
|
+
false
|
113
|
+
end
|
100
114
|
elsif defined?(MiniMagick)
|
101
115
|
MiniMagick::Image.new(path)
|
102
116
|
end
|
103
117
|
end
|
104
118
|
|
119
|
+
def supported_vips_suffix?(path)
|
120
|
+
Vips::get_suffixes.include?(File.extname(path).downcase)
|
121
|
+
end
|
122
|
+
|
123
|
+
def vips_version_below_8_8?
|
124
|
+
# FYI, Vips 8.8 was released in 2019
|
125
|
+
# https://github.com/libvips/libvips/releases/tag/v8.8.0
|
126
|
+
!Vips::respond_to?(:vips_foreign_get_suffixes)
|
127
|
+
end
|
128
|
+
|
129
|
+
def open_uri_tempfile?(path)
|
130
|
+
# When trying to open urls for 'large' images, OpenURI will return a
|
131
|
+
# tempfile. That tempfile does not have an extension indicating the type
|
132
|
+
# of file. However, Vips will be able to process it anyway.
|
133
|
+
# The 'large' file value is derived from OpenUri::Buffer class (> 10ko)
|
134
|
+
path.split('/').last.starts_with?("open-uri")
|
135
|
+
end
|
136
|
+
|
105
137
|
def valid_image?(image)
|
106
138
|
return false unless image
|
107
139
|
|
108
|
-
|
140
|
+
vips_image_processor? && image.is_a?(Vips::Image) ? image.avg : image.valid?
|
109
141
|
rescue exception_class
|
110
142
|
false
|
111
143
|
end
|
112
144
|
|
113
145
|
def rotated_image?(image)
|
114
|
-
if
|
146
|
+
if vips_image_processor? && image.is_a?(Vips::Image)
|
115
147
|
image.get('exif-ifd0-Orientation').include?('Right-top') ||
|
116
148
|
image.get('exif-ifd0-Orientation').include?('Left-bottom')
|
117
149
|
else
|
@@ -1,27 +1,33 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative 'concerns/errorable.rb'
|
3
4
|
require_relative 'concerns/symbolizable.rb'
|
4
5
|
require_relative 'metadata.rb'
|
5
6
|
|
6
7
|
module ActiveStorageValidations
|
7
8
|
class ProcessableImageValidator < ActiveModel::EachValidator # :nodoc
|
8
9
|
include OptionProcUnfolding
|
9
|
-
include
|
10
|
+
include Errorable
|
10
11
|
include Symbolizable
|
11
12
|
|
13
|
+
ERROR_TYPES = %i[
|
14
|
+
image_not_processable
|
15
|
+
].freeze
|
16
|
+
|
12
17
|
if Rails.gem_version >= Gem::Version.new('6.0.0')
|
13
18
|
def validate_each(record, attribute, _value)
|
14
19
|
return true unless record.send(attribute).attached?
|
15
20
|
|
16
|
-
errors_options = initialize_error_options(options)
|
17
|
-
|
18
21
|
changes = record.attachment_changes[attribute.to_s]
|
19
22
|
return true if changes.blank?
|
20
23
|
|
21
24
|
files = Array.wrap(changes.is_a?(ActiveStorage::Attached::Changes::CreateMany) ? changes.attachables : changes.attachable)
|
22
25
|
|
23
26
|
files.each do |file|
|
24
|
-
|
27
|
+
if !Metadata.new(file).valid?
|
28
|
+
errors_options = initialize_error_options(options, file)
|
29
|
+
add_error(record, attribute, ERROR_TYPES.first , **errors_options) unless Metadata.new(file).valid?
|
30
|
+
end
|
25
31
|
end
|
26
32
|
end
|
27
33
|
else
|
@@ -32,7 +38,10 @@ module ActiveStorageValidations
|
|
32
38
|
files = Array.wrap(record.send(attribute))
|
33
39
|
|
34
40
|
files.each do |file|
|
35
|
-
|
41
|
+
if !Metadata.new(file).valid?
|
42
|
+
errors_options = initialize_error_options(options, file)
|
43
|
+
add_error(record, attribute, ERROR_TYPES.first , **errors_options) unless Metadata.new(file).valid?
|
44
|
+
end
|
36
45
|
end
|
37
46
|
end
|
38
47
|
end
|