active_storage_validations 1.1.3 → 1.2.0
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.
- 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
|