active_storage_validations 1.3.5 → 2.0.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.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +620 -279
  3. data/config/locales/da.yml +50 -29
  4. data/config/locales/de.yml +50 -29
  5. data/config/locales/en.yml +50 -29
  6. data/config/locales/es.yml +50 -29
  7. data/config/locales/fr.yml +50 -29
  8. data/config/locales/it.yml +50 -29
  9. data/config/locales/ja.yml +50 -29
  10. data/config/locales/nl.yml +50 -29
  11. data/config/locales/pl.yml +50 -29
  12. data/config/locales/pt-BR.yml +50 -29
  13. data/config/locales/ru.yml +50 -29
  14. data/config/locales/sv.yml +50 -29
  15. data/config/locales/tr.yml +50 -29
  16. data/config/locales/uk.yml +50 -29
  17. data/config/locales/vi.yml +50 -29
  18. data/config/locales/zh-CN.yml +50 -29
  19. data/lib/active_storage_validations/analyzer/audio_analyzer.rb +58 -0
  20. data/lib/active_storage_validations/analyzer/content_type_analyzer.rb +60 -0
  21. data/lib/active_storage_validations/analyzer/image_analyzer/image_magick.rb +12 -11
  22. data/lib/active_storage_validations/analyzer/image_analyzer/vips.rb +12 -12
  23. data/lib/active_storage_validations/analyzer/image_analyzer.rb +18 -46
  24. data/lib/active_storage_validations/analyzer/null_analyzer.rb +2 -2
  25. data/lib/active_storage_validations/analyzer/shared/asv_ff_probable.rb +61 -0
  26. data/lib/active_storage_validations/analyzer/video_analyzer.rb +130 -0
  27. data/lib/active_storage_validations/analyzer.rb +54 -1
  28. data/lib/active_storage_validations/aspect_ratio_validator.rb +154 -120
  29. data/lib/active_storage_validations/{base_size_validator.rb → base_comparison_validator.rb} +18 -16
  30. data/lib/active_storage_validations/content_type_validator.rb +51 -17
  31. data/lib/active_storage_validations/dimension_validator.rb +20 -19
  32. data/lib/active_storage_validations/duration_validator.rb +55 -0
  33. data/lib/active_storage_validations/extensors/asv_blob_metadatable.rb +24 -0
  34. data/lib/active_storage_validations/{marcel_extensor.rb → extensors/asv_marcelable.rb} +5 -0
  35. data/lib/active_storage_validations/limit_validator.rb +14 -2
  36. data/lib/active_storage_validations/matchers/aspect_ratio_validator_matcher.rb +1 -1
  37. data/lib/active_storage_validations/matchers/{base_size_validator_matcher.rb → base_comparison_validator_matcher.rb} +31 -25
  38. data/lib/active_storage_validations/matchers/content_type_validator_matcher.rb +7 -3
  39. data/lib/active_storage_validations/matchers/dimension_validator_matcher.rb +1 -1
  40. data/lib/active_storage_validations/matchers/duration_validator_matcher.rb +39 -0
  41. data/lib/active_storage_validations/matchers/{processable_image_validator_matcher.rb → processable_file_validator_matcher.rb} +5 -5
  42. data/lib/active_storage_validations/matchers/size_validator_matcher.rb +18 -2
  43. data/lib/active_storage_validations/matchers/total_size_validator_matcher.rb +18 -2
  44. data/lib/active_storage_validations/matchers.rb +4 -3
  45. data/lib/active_storage_validations/{processable_image_validator.rb → processable_file_validator.rb} +4 -3
  46. data/lib/active_storage_validations/railtie.rb +5 -0
  47. data/lib/active_storage_validations/shared/asv_active_storageable.rb +2 -2
  48. data/lib/active_storage_validations/shared/asv_analyzable.rb +38 -3
  49. data/lib/active_storage_validations/shared/asv_attachable.rb +36 -15
  50. data/lib/active_storage_validations/size_validator.rb +11 -3
  51. data/lib/active_storage_validations/total_size_validator.rb +9 -3
  52. data/lib/active_storage_validations/version.rb +1 -1
  53. data/lib/active_storage_validations.rb +7 -3
  54. metadata +14 -8
  55. data/lib/active_storage_validations/content_type_spoof_detector.rb +0 -96
@@ -6,14 +6,12 @@ require_relative 'shared/asv_optionable'
6
6
  require_relative 'shared/asv_symbolizable'
7
7
 
8
8
  module ActiveStorageValidations
9
- class BaseSizeValidator < ActiveModel::EachValidator # :nodoc:
9
+ class BaseComparisonValidator < ActiveModel::EachValidator # :nodoc:
10
10
  include ASVActiveStorageable
11
11
  include ASVErrorable
12
12
  include ASVOptionable
13
13
  include ASVSymbolizable
14
14
 
15
- delegate :number_to_human_size, to: ActiveSupport::NumberHelper
16
-
17
15
  AVAILABLE_CHECKS = %i[
18
16
  less_than
19
17
  less_than_or_equal_to
@@ -23,8 +21,8 @@ module ActiveStorageValidations
23
21
  ].freeze
24
22
 
25
23
  def initialize(*args)
26
- if self.class == BaseSizeValidator
27
- raise NotImplementedError, 'BaseSizeValidator is an abstract class and cannot be instantiated directly.'
24
+ if self.class == BaseComparisonValidator
25
+ raise NotImplementedError, 'BaseComparisonValidator is an abstract class and cannot be instantiated directly.'
28
26
  end
29
27
  super
30
28
  end
@@ -37,32 +35,36 @@ module ActiveStorageValidations
37
35
 
38
36
  private
39
37
 
40
- def is_valid?(size, flat_options)
41
- return false if size < 0
38
+ def is_valid?(value, flat_options)
39
+ return false if value < 0
42
40
 
43
41
  if flat_options[:between].present?
44
- flat_options[:between].include?(size)
42
+ flat_options[:between].include?(value)
45
43
  elsif flat_options[:less_than].present?
46
- size < flat_options[:less_than]
44
+ value < flat_options[:less_than]
47
45
  elsif flat_options[:less_than_or_equal_to].present?
48
- size <= flat_options[:less_than_or_equal_to]
46
+ value <= flat_options[:less_than_or_equal_to]
49
47
  elsif flat_options[:greater_than].present?
50
- size > flat_options[:greater_than]
48
+ value > flat_options[:greater_than]
51
49
  elsif flat_options[:greater_than_or_equal_to].present?
52
- size >= flat_options[:greater_than_or_equal_to]
50
+ value >= flat_options[:greater_than_or_equal_to]
53
51
  end
54
52
  end
55
53
 
56
54
  def populate_error_options(errors_options, flat_options)
57
- errors_options[:min_size] = number_to_human_size(min_size(flat_options))
58
- errors_options[:max_size] = number_to_human_size(max_size(flat_options))
55
+ errors_options[:min] = format_bound_value(min(flat_options))
56
+ errors_options[:max] = format_bound_value(max(flat_options))
57
+ end
58
+
59
+ def format_bound_value
60
+ raise NotImplementedError
59
61
  end
60
62
 
61
- def min_size(flat_options)
63
+ def min(flat_options)
62
64
  flat_options[:between]&.min || flat_options[:greater_than] || flat_options[:greater_than_or_equal_to]
63
65
  end
64
66
 
65
- def max_size(flat_options)
67
+ def max(flat_options)
66
68
  flat_options[:between]&.max || flat_options[:less_than] || flat_options[:less_than_or_equal_to]
67
69
  end
68
70
  end
@@ -6,7 +6,7 @@ require_relative 'shared/asv_attachable'
6
6
  require_relative 'shared/asv_errorable'
7
7
  require_relative 'shared/asv_optionable'
8
8
  require_relative 'shared/asv_symbolizable'
9
- require_relative 'content_type_spoof_detector'
9
+ require_relative 'analyzer/content_type_analyzer'
10
10
 
11
11
  module ActiveStorageValidations
12
12
  class ContentTypeValidator < ActiveModel::EachValidator # :nodoc:
@@ -20,8 +20,9 @@ module ActiveStorageValidations
20
20
  AVAILABLE_CHECKS = %i[with in].freeze
21
21
  ERROR_TYPES = %i[
22
22
  content_type_invalid
23
- spoofed_content_type
23
+ content_type_spoofed
24
24
  ].freeze
25
+ METADATA_KEYS = %i[content_type].freeze
25
26
 
26
27
  def check_validity!
27
28
  ensure_exactly_one_validator_option
@@ -34,9 +35,9 @@ module ActiveStorageValidations
34
35
  @authorized_content_types = authorized_content_types_from_options(record)
35
36
  return if @authorized_content_types.empty?
36
37
 
37
- attachables_from_changes(record, attribute).each do |attachable|
38
- set_attachable_cached_values(attachable)
39
- is_valid?(record, attribute, attachable)
38
+ attachables_and_blobs(record, attribute).each do |attachable, blob|
39
+ set_attachable_cached_values(blob)
40
+ is_valid?(record, attribute, attachable, blob)
40
41
  end
41
42
  end
42
43
 
@@ -53,16 +54,16 @@ module ActiveStorageValidations
53
54
  end
54
55
  end
55
56
 
56
- def set_attachable_cached_values(attachable)
57
- @attachable_content_type = attachable_content_type_rails_like(attachable)
58
- @attachable_filename = attachable_filename(attachable).to_s
57
+ def set_attachable_cached_values(blob)
58
+ @attachable_content_type = blob.content_type
59
+ @attachable_filename = blob.filename.to_s
59
60
  end
60
61
 
61
62
  # Check if the provided content_type is authorized and not spoofed against
62
63
  # the file io.
63
- def is_valid?(record, attribute, attachable)
64
+ def is_valid?(record, attribute, attachable, blob)
64
65
  authorized_content_type?(record, attribute, attachable) &&
65
- not_spoofing_content_type?(record, attribute, attachable)
66
+ not_spoofing_content_type?(record, attribute, attachable, blob)
66
67
  end
67
68
 
68
69
  # Dead code that we keep here for some time, maybe we will find a solution
@@ -100,11 +101,19 @@ module ActiveStorageValidations
100
101
  false
101
102
  end
102
103
 
103
- def not_spoofing_content_type?(record, attribute, attachable)
104
+ def marcel_attachable_content_type(attachable)
105
+ Marcel::MimeType.for(declared_type: @attachable_content_type, name: @attachable_filename)
106
+ end
107
+
108
+ def not_spoofing_content_type?(record, attribute, attachable, blob)
104
109
  return true unless enable_spoofing_protection?
105
110
 
106
- if ContentTypeSpoofDetector.new(record, attribute, attachable).spoofed?
107
- errors_options = initialize_error_options(options, attachable)
111
+ @detected_content_type = metadata_for(blob, attachable, METADATA_KEYS)&.fetch(:content_type, nil)
112
+
113
+ if attachable_content_type_vs_detected_content_type_mismatch?
114
+ errors_options = initialize_and_populate_error_options(options, attachable)
115
+ errors_options[:detected_content_type] = @detected_content_type
116
+ errors_options[:detected_human_content_type] = content_type_to_human_format(@detected_content_type)
108
117
  add_error(record, attribute, ERROR_TYPES.second, **errors_options)
109
118
  false
110
119
  else
@@ -112,19 +121,40 @@ module ActiveStorageValidations
112
121
  end
113
122
  end
114
123
 
115
- def marcel_attachable_content_type(attachable)
116
- Marcel::MimeType.for(declared_type: @attachable_content_type, name: @attachable_filename)
124
+ def disable_spoofing_protection?
125
+ !enable_spoofing_protection?
117
126
  end
118
127
 
119
128
  def enable_spoofing_protection?
120
129
  options[:spoofing_protection] == true
121
130
  end
122
131
 
132
+ def attachable_content_type_vs_detected_content_type_mismatch?
133
+ @attachable_content_type.present? &&
134
+ !attachable_content_type_intersects_detected_content_type?
135
+ end
136
+
137
+ def attachable_content_type_intersects_detected_content_type?
138
+ # Ruby intersects? method is only available from 3.1
139
+ enlarged_content_type(content_type_without_parameters(@attachable_content_type)).any? do |item|
140
+ enlarged_content_type(content_type_without_parameters(@detected_content_type)).include?(item)
141
+ end
142
+ end
143
+
144
+ def enlarged_content_type(content_type)
145
+ [content_type, *parent_content_types(content_type)].compact.uniq
146
+ end
147
+
148
+ def parent_content_types(content_type)
149
+ Marcel::TYPE_PARENTS[content_type] || []
150
+ end
151
+
123
152
  def initialize_and_populate_error_options(options, attachable)
124
153
  errors_options = initialize_error_options(options, attachable)
125
154
  errors_options[:content_type] = @attachable_content_type
126
155
  errors_options[:human_content_type] = content_type_to_human_format(@attachable_content_type)
127
- errors_options[:authorized_types] = content_type_to_human_format(@authorized_content_types)
156
+ errors_options[:authorized_human_content_types] = content_type_to_human_format(@authorized_content_types)
157
+ errors_options[:count] = @authorized_content_types.size
128
158
  errors_options
129
159
  end
130
160
 
@@ -152,7 +182,7 @@ module ActiveStorageValidations
152
182
  def ensure_content_types_validity
153
183
  return true if options[:with]&.is_a?(Proc) || options[:in]&.is_a?(Proc)
154
184
 
155
- ([options[:with]] || options[:in]).each do |content_type|
185
+ (Array(options[:with]) + Array(options[:in])).each do |content_type|
156
186
  raise ArgumentError, invalid_content_type_option_message(content_type) if invalid_option?(content_type)
157
187
  end
158
188
  end
@@ -181,6 +211,10 @@ module ActiveStorageValidations
181
211
  end
182
212
 
183
213
  def invalid_content_type?(content_type)
214
+ if content_type == 'image/jpg'
215
+ raise ArgumentError, "'image/jpg' is not a valid content type, you should use 'image/jpeg' instead"
216
+ end
217
+
184
218
  Marcel::TYPE_EXTS[content_type.to_s] == nil
185
219
  end
186
220
 
@@ -18,18 +18,19 @@ module ActiveStorageValidations
18
18
 
19
19
  AVAILABLE_CHECKS = %i[width height min max].freeze
20
20
  ERROR_TYPES = %i[
21
- image_metadata_missing
22
- dimension_min_inclusion
23
- dimension_max_inclusion
24
- dimension_width_inclusion
25
- dimension_height_inclusion
26
- dimension_width_greater_than_or_equal_to
27
- dimension_height_greater_than_or_equal_to
28
- dimension_width_less_than_or_equal_to
29
- dimension_height_less_than_or_equal_to
30
- dimension_width_equal_to
31
- dimension_height_equal_to
21
+ dimension_min_not_included_in
22
+ dimension_max_not_included_in
23
+ dimension_width_not_included_in
24
+ dimension_height_not_included_in
25
+ dimension_width_not_greater_than_or_equal_to
26
+ dimension_height_not_greater_than_or_equal_to
27
+ dimension_width_not_less_than_or_equal_to
28
+ dimension_height_not_less_than_or_equal_to
29
+ dimension_width_not_equal_to
30
+ dimension_height_not_equal_to
31
+ media_metadata_missing
32
32
  ].freeze
33
+ METADATA_KEYS = %i[width height].freeze
33
34
 
34
35
  def check_validity!
35
36
  unless AVAILABLE_CHECKS.any? { |argument| options.key?(argument) }
@@ -40,7 +41,7 @@ module ActiveStorageValidations
40
41
  def validate_each(record, attribute, _value)
41
42
  return if no_attachments?(record, attribute)
42
43
 
43
- validate_changed_files_from_metadata(record, attribute)
44
+ validate_changed_files_from_metadata(record, attribute, METADATA_KEYS)
44
45
  end
45
46
 
46
47
  private
@@ -51,7 +52,7 @@ module ActiveStorageValidations
51
52
 
52
53
  # Validation fails unless file metadata contains valid width and height.
53
54
  if metadata[:width].to_i <= 0 || metadata[:height].to_i <= 0
54
- add_error(record, attribute, :image_metadata_missing, **errors_options)
55
+ add_error(record, attribute, :media_metadata_missing, **errors_options)
55
56
  return false
56
57
  end
57
58
 
@@ -64,7 +65,7 @@ module ActiveStorageValidations
64
65
  errors_options[:width] = flat_options[:width][:min]
65
66
  errors_options[:height] = flat_options[:height][:min]
66
67
 
67
- add_error(record, attribute, :dimension_min_inclusion, **errors_options)
68
+ add_error(record, attribute, :dimension_min_not_included_in, **errors_options)
68
69
  return false
69
70
  end
70
71
  if flat_options[:max] && (
@@ -74,7 +75,7 @@ module ActiveStorageValidations
74
75
  errors_options[:width] = flat_options[:width][:max]
75
76
  errors_options[:height] = flat_options[:height][:max]
76
77
 
77
- add_error(record, attribute, :dimension_max_inclusion, **errors_options)
78
+ add_error(record, attribute, :dimension_max_not_included_in, **errors_options)
78
79
  return false
79
80
  end
80
81
 
@@ -86,7 +87,7 @@ module ActiveStorageValidations
86
87
  next unless flat_options[length]
87
88
  if flat_options[length].is_a?(Hash)
88
89
  if flat_options[length][:in] && (metadata[length] < flat_options[length][:min] || metadata[length] > flat_options[length][:max])
89
- error_type = :"dimension_#{length}_inclusion"
90
+ error_type = :"dimension_#{length}_not_included_in"
90
91
  errors_options[:min] = flat_options[length][:min]
91
92
  errors_options[:max] = flat_options[length][:max]
92
93
 
@@ -94,13 +95,13 @@ module ActiveStorageValidations
94
95
  width_or_height_invalid = true
95
96
  else
96
97
  if flat_options[length][:min] && metadata[length] < flat_options[length][:min]
97
- error_type = :"dimension_#{length}_greater_than_or_equal_to"
98
+ error_type = :"dimension_#{length}_not_greater_than_or_equal_to"
98
99
  errors_options[:length] = flat_options[length][:min]
99
100
 
100
101
  add_error(record, attribute, error_type, **errors_options)
101
102
  width_or_height_invalid = true
102
103
  elsif flat_options[length][:max] && metadata[length] > flat_options[length][:max]
103
- error_type = :"dimension_#{length}_less_than_or_equal_to"
104
+ error_type = :"dimension_#{length}_not_less_than_or_equal_to"
104
105
  errors_options[:length] = flat_options[length][:max]
105
106
 
106
107
  add_error(record, attribute, error_type, **errors_options)
@@ -109,7 +110,7 @@ module ActiveStorageValidations
109
110
  end
110
111
  else
111
112
  if metadata[length] != flat_options[length]
112
- error_type = :"dimension_#{length}_equal_to"
113
+ error_type = :"dimension_#{length}_not_equal_to"
113
114
  errors_options[:length] = flat_options[length]
114
115
 
115
116
  add_error(record, attribute, error_type, **errors_options)
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_comparison_validator'
4
+
5
+ module ActiveStorageValidations
6
+ class DurationValidator < BaseComparisonValidator
7
+ include ASVAnalyzable
8
+ include ASVAttachable
9
+
10
+ ERROR_TYPES = %i[
11
+ duration_not_less_than
12
+ duration_not_less_than_or_equal_to
13
+ duration_not_greater_than
14
+ duration_not_greater_than_or_equal_to
15
+ duration_not_between
16
+ ].freeze
17
+ METADATA_KEYS = %i[duration].freeze
18
+
19
+ def validate_each(record, attribute, _value)
20
+ return if no_attachments?(record, attribute)
21
+
22
+ flat_options = set_flat_options(record)
23
+
24
+ attachables_and_blobs(record, attribute).each do |attachable, blob|
25
+ duration = metadata_for(blob, attachable, METADATA_KEYS)&.fetch(:duration, nil)
26
+
27
+ if duration.to_i <= 0
28
+ errors_options = initialize_error_options(options, attachable)
29
+ add_error(record, attribute, :media_metadata_missing, **errors_options)
30
+ next
31
+ end
32
+
33
+ next if is_valid?(duration, flat_options)
34
+
35
+ errors_options = initialize_error_options(options, attachable)
36
+ populate_error_options(errors_options, flat_options)
37
+ errors_options[:duration] = format_bound_value(duration)
38
+
39
+ keys = AVAILABLE_CHECKS & flat_options.keys
40
+ error_type = "duration_not_#{keys.first}".to_sym
41
+
42
+ add_error(record, attribute, error_type, **errors_options)
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ def format_bound_value(value)
49
+ return nil unless value
50
+
51
+ custom_value = value == value.to_i ? value.to_i : value
52
+ ActiveSupport::Duration.build(custom_value).inspect
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveStorageValidations
4
+ module ASVBlobMetadatable
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ def active_storage_validations_metadata
9
+ metadata.dig('custom', 'active_storage_validations') || {}
10
+ end
11
+
12
+ def active_storage_validations_metadata=(value)
13
+ metadata['custom'] ||= {}
14
+ metadata['custom']['active_storage_validations'] = value
15
+ end
16
+
17
+ def merge_into_active_storage_validations_metadata(new_data)
18
+ metadata['custom'] ||= {}
19
+ metadata['custom']['active_storage_validations'] ||= {}
20
+ metadata['custom']['active_storage_validations'].merge!(new_data)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -1,7 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "marcel"
4
+
3
5
  Marcel::MimeType.extend "application/x-rar-compressed", parents: %(application/x-rar)
4
6
  Marcel::MimeType.extend "audio/x-hx-aac-adts", parents: %(audio/x-aac)
5
7
  Marcel::MimeType.extend "audio/x-m4a", parents: %(audio/mp4)
6
8
  Marcel::MimeType.extend "text/xml", parents: %(application/xml) # alias
7
9
  Marcel::MimeType.extend "video/theora", parents: %(video/ogg)
10
+
11
+ # Add empty content type
12
+ Marcel::MimeType.extend "inode/x-empty", extensions: %w(empty)
@@ -15,6 +15,8 @@ module ActiveStorageValidations
15
15
  AVAILABLE_CHECKS = %i[max min].freeze
16
16
  ERROR_TYPES = %i[
17
17
  limit_out_of_range
18
+ limit_min_not_reached
19
+ limit_max_exceeded
18
20
  ].freeze
19
21
 
20
22
  def check_validity!
@@ -25,13 +27,23 @@ module ActiveStorageValidations
25
27
  def validate_each(record, attribute, _value)
26
28
  files = attached_files(record, attribute).reject(&:blank?)
27
29
  flat_options = set_flat_options(record)
30
+ count = files.count
28
31
 
29
- return if files_count_valid?(files.count, flat_options)
32
+ return if files_count_valid?(count, flat_options)
30
33
 
31
34
  errors_options = initialize_error_options(options)
32
35
  errors_options[:min] = flat_options[:min]
33
36
  errors_options[:max] = flat_options[:max]
34
- add_error(record, attribute, ERROR_TYPES.first, **errors_options)
37
+ errors_options[:count] = count
38
+ error_type = if flat_options[:min] && flat_options[:max]
39
+ :limit_out_of_range
40
+ elsif flat_options[:min] && count < flat_options[:min]
41
+ :limit_min_not_reached
42
+ else
43
+ :limit_max_exceeded
44
+ end
45
+
46
+ add_error(record, attribute, error_type, **errors_options)
35
47
  end
36
48
 
37
49
  private
@@ -94,7 +94,7 @@ module ActiveStorageValidations
94
94
  end
95
95
 
96
96
  def mock_dimensions_for(attachment, width, height)
97
- Matchers.mock_metadata(attachment, width, height) do
97
+ Matchers.mock_metadata(attachment, { width: width, height: height }) do
98
98
  yield
99
99
  end
100
100
  end
@@ -13,8 +13,8 @@ require_relative 'shared/asv_validatable'
13
13
 
14
14
  module ActiveStorageValidations
15
15
  module Matchers
16
- class BaseSizeValidatorMatcher
17
- # BaseSizeValidatorMatcher is an abstract class and shouldn't be instantiated directly.
16
+ class BaseComparisonValidatorMatcher
17
+ # BaseComparisonValidatorMatcher is an abstract class and shouldn't be instantiated directly.
18
18
 
19
19
  include ASVActiveStorageable
20
20
  include ASVAllowBlankable
@@ -33,23 +33,23 @@ module ActiveStorageValidations
33
33
  @min = @max = nil
34
34
  end
35
35
 
36
- def less_than(size)
37
- @max = size - 1.byte
36
+ def less_than(value)
37
+ @max = value - smallest_measurement
38
38
  self
39
39
  end
40
40
 
41
- def less_than_or_equal_to(size)
42
- @max = size
41
+ def less_than_or_equal_to(value)
42
+ @max = value
43
43
  self
44
44
  end
45
45
 
46
- def greater_than(size)
47
- @min = size + 1.byte
46
+ def greater_than(value)
47
+ @min = value + smallest_measurement
48
48
  self
49
49
  end
50
50
 
51
- def greater_than_or_equal_to(size)
52
- @min = size
51
+ def greater_than_or_equal_to(value)
52
+ @min = value
53
53
  self
54
54
  end
55
55
 
@@ -78,45 +78,53 @@ module ActiveStorageValidations
78
78
 
79
79
  message << " but there seem to have issues with the matcher methods you used, since:"
80
80
  @failure_message_artefacts.each do |error_case|
81
- message << " validation failed when provided with a #{error_case[:size]} bytes test file"
81
+ message << " validation failed when provided with a #{error_case[:value]} #{failure_message_unit} test file"
82
82
  end
83
83
  message << " whereas it should have passed"
84
84
  end
85
85
 
86
+ def failure_message_unit
87
+ raise NotImplementedError
88
+ end
89
+
86
90
  def not_lower_than_min?
87
- @min.nil? || !passes_validation_with_size(@min - 1)
91
+ @min.nil? || !passes_validation_with_value(@min - 1)
88
92
  end
89
93
 
90
94
  def higher_than_min?
91
- @min.nil? || passes_validation_with_size(@min + 1)
95
+ @min.nil? || passes_validation_with_value(@min + 1)
92
96
  end
93
97
 
94
98
  def lower_than_max?
95
- @max.nil? || @max == Float::INFINITY || passes_validation_with_size(@max - 1)
99
+ @max.nil? || @max == Float::INFINITY || passes_validation_with_value(@max - 1)
96
100
  end
97
101
 
98
102
  def not_higher_than_max?
99
- @max.nil? || @max == Float::INFINITY || !passes_validation_with_size(@max + 1)
103
+ @max.nil? || @max == Float::INFINITY || !passes_validation_with_value(@max + 1)
100
104
  end
101
105
 
102
- def passes_validation_with_size(size)
103
- mock_size_for(io, size) do
106
+ def smallest_measurement
107
+ raise NotImplementedError
108
+ end
109
+
110
+ def passes_validation_with_value(value)
111
+ mock_value_for(io, value) do
104
112
  attach_file
105
113
  validate
106
114
  detach_file
107
- is_valid? || add_failure_message_artefact(size)
115
+ is_valid? || add_failure_message_artefact(value)
108
116
  end
109
117
  end
110
118
 
111
- def add_failure_message_artefact(size)
112
- @failure_message_artefacts << { size: size }
119
+ def add_failure_message_artefact(value)
120
+ @failure_message_artefacts << { value: value }
113
121
  false
114
122
  end
115
123
 
116
124
  def is_custom_message_valid?
117
125
  return true unless @custom_message
118
126
 
119
- mock_size_for(io, -1.kilobytes) do
127
+ mock_value_for(io, -smallest_measurement) do
120
128
  attach_file
121
129
  validate
122
130
  detach_file
@@ -124,10 +132,8 @@ module ActiveStorageValidations
124
132
  end
125
133
  end
126
134
 
127
- def mock_size_for(io, size)
128
- Matchers.stub_method(io, :size, size) do
129
- yield
130
- end
135
+ def mock_value_for(io, size)
136
+ raise NotImplementedError
131
137
  end
132
138
  end
133
139
  end
@@ -46,12 +46,12 @@ module ActiveStorageValidations
46
46
  end
47
47
 
48
48
  def allowing(*content_types)
49
- @allowed_content_types = content_types.flatten
49
+ @allowed_content_types = content_types.map { |content_type| normalize_content_type(content_type) }.flatten
50
50
  self
51
51
  end
52
52
 
53
53
  def rejecting(*content_types)
54
- @rejected_content_types = content_types.flatten
54
+ @rejected_content_types = content_types.map { |content_type| normalize_content_type(content_type) }.flatten
55
55
  self
56
56
  end
57
57
 
@@ -88,6 +88,10 @@ module ActiveStorageValidations
88
88
  end
89
89
  end
90
90
 
91
+ def normalize_content_type(content_type)
92
+ Marcel::MimeType.for(declared_type: content_type.to_s, extension: content_type.to_s)
93
+ end
94
+
91
95
  def all_allowed_content_types_allowed?
92
96
  @allowed_content_types_not_allowed ||= @allowed_content_types.reject { |type| type_allowed?(type) }
93
97
  @allowed_content_types_not_allowed.empty?
@@ -135,7 +139,7 @@ module ActiveStorageValidations
135
139
  # (ie spoofed file basically), we need to ignore the error related to
136
140
  # content type spoofing in our matcher to pass the tests
137
141
  def validator_errors_for_attribute
138
- super.reject { |hash| hash[:error] == :spoofed_content_type }
142
+ super.reject { |hash| hash[:error] == :content_type_spoofed }
139
143
  end
140
144
  end
141
145
  end
@@ -185,7 +185,7 @@ module ActiveStorageValidations
185
185
  end
186
186
 
187
187
  def mock_dimensions_for(attachment, width, height)
188
- Matchers.mock_metadata(attachment, width, height) do
188
+ Matchers.mock_metadata(attachment, { width: width, height: height }) do
189
189
  yield
190
190
  end
191
191
  end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_comparison_validator_matcher'
4
+
5
+ module ActiveStorageValidations
6
+ module Matchers
7
+ def validate_duration_of(attribute_name)
8
+ DurationValidatorMatcher.new(attribute_name)
9
+ end
10
+
11
+ class DurationValidatorMatcher < BaseComparisonValidatorMatcher
12
+ def description
13
+ "validate file duration of :#{@attribute_name}"
14
+ end
15
+
16
+ def failure_message
17
+ message = ["is expected to validate file duration of :#{@attribute_name}"]
18
+ build_failure_message(message)
19
+ message.join("\n")
20
+ end
21
+
22
+ private
23
+
24
+ def failure_message_unit
25
+ "seconds"
26
+ end
27
+
28
+ def smallest_measurement
29
+ 1.second
30
+ end
31
+
32
+ def mock_value_for(io, duration)
33
+ Matchers.mock_metadata(io, { duration: duration }) do
34
+ yield
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end