schemacop 3.1.0 → 3.1.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9ef422cfe57686302457be2282f2b8e5f45ccdc79d6cd62597e8308bbb94026e
4
- data.tar.gz: d45307834a863e1fdfdf616acbb9727fd78db428ce88436c27a67db88a3e802f
3
+ metadata.gz: f790c910d2c32accea93c9f4e67b85c80ababfbaca2b64930d7e380a65125d9e
4
+ data.tar.gz: ae3f22c58697112408efde125190a0cd8518199ceb8b47b65094e289d9e8769d
5
5
  SHA512:
6
- metadata.gz: 1253508e8f7270b6fa5f8f54563e79fa034a97fa8f89334f416d0e4549a5b374a2cdc0b3b4777e15202a7759a275d389a8390d67efd7acbb9114de38e21aceb1
7
- data.tar.gz: 5aa472bf5a7d1aa37f946cf298b76349ad4faee611491c0ecf1bd377baa1127cfaae668f4a71ad00381a0afa88b5c23d073a3b84018ee5387808ad645766b37a
6
+ metadata.gz: b3f20de721d5e94f4bbb1504292c8ecd7136ae8468e8fde9d0885e9503aee4c6f950e7bd888ea50067d0dbf40c3dd0e36ab12d2ee674143a28405bd3c70bfe2f
7
+ data.tar.gz: efd82ce7677ee0c3d5031af98997840cd6cf38a3aa2337e68ead04e383add88e32d29f15d08ad0739e47691c48299f329c547677abd6aca1118a29fe6308a4e4
data/CHANGELOG.md CHANGED
@@ -1,5 +1,65 @@
1
1
  # Change log
2
2
 
3
+ ## 3.1.1 (2026-09-08)
4
+
5
+ * Fix crashes when validating strings whose encoding does not fit their
6
+ contents or the schema:
7
+
8
+ * A string with an invalid byte sequence is reported as such and no longer
9
+ handed to `allow_blank`, `pattern` and `format`, which decode it and
10
+ therefore raised an `ArgumentError`. The checks that do not decode the
11
+ string (`min_length`, `max_length`, `encoding` and `enum`) are still
12
+ reported.
13
+
14
+ * A string that cannot be compared with the `pattern` or with the pattern of
15
+ the given `format` because their encodings are incompatible (e.g. a UTF-16
16
+ string) is reported as not matching it, rather than raising an
17
+ `Encoding::CompatibilityError` or a `RegexpError`. The same holds for
18
+ `allow_blank`, `dep` dependencies and `treat_blank_as_nil`, which treat
19
+ such a string as blank only if it is empty.
20
+
21
+ * Hash keys and values with an invalid byte sequence no longer raise an
22
+ `ArgumentError` or an `EncodingError` when used together with
23
+ `property_names`, pattern properties, `ignore_obsolete_properties` or `dep`
24
+ dependencies. Such a key matches none of the patterns.
25
+
26
+ * A key with an invalid byte sequence in the path of a validation error no
27
+ longer makes `Result#messages`, `Result#exception_message` and `validate!`
28
+ raise an `ArgumentError`. The invalid bytes are replaced in the path of the
29
+ message.
30
+
31
+ * The `encoding` error of a string is now reported before its `pattern` and
32
+ `format` errors, rather than after them.
33
+
34
+ Internal reference: `#153682`.
35
+
36
+ * Anchor the patterns of the built-in string formats with `\A` and `\z` instead
37
+ of `^` and `$`, which match at the start and the end of every line in Ruby. A
38
+ multi-line string passed the validation as soon as one of its lines matched
39
+ and was then handed to the cast handler: `"32\ntest"` passed the `integer`
40
+ format and raised an `ArgumentError`, `"true\n"` passed the `boolean` format
41
+ and was cast to `false`. A value with a trailing newline is rejected as well
42
+ now, where it used to pass and be cast. Affects the formats `date`,
43
+ `date-time`, `time`, `mailbox`, `boolean`, `integer`, `number` and
44
+ `integer-list`.
45
+
46
+ Internal reference: `#153681`.
47
+
48
+ * Require `tempfile`, which the `bin` node needs. Requiring the gem outside of
49
+ Rails failed with a `NameError` for `Tempfile`.
50
+
51
+ * Fix `ignore_obsolete_properties` ignoring an allow list given as strings
52
+ (e.g. `ignore_obsolete_properties: %w[foo]`), which reported the listed
53
+ properties as obsolete. Strings and symbols are now treated alike.
54
+
55
+ * Fix a `NoMethodError` when casting a hash that declares pattern properties
56
+ and contains a key matching none of them.
57
+
58
+ * Fix casting of hash properties that are specified in the schema: in a hash
59
+ that also declares pattern properties or additional properties, they were
60
+ casted a second time as such, which could cast them with the wrong node and
61
+ added the original key to the result next to the one given via `as`.
62
+
3
63
  ## 3.1.0 (2026-05-09)
4
64
 
5
65
  * Add option `encoding` to `str` node for Schemacop V3 schemas to validate that
data/README_V3.md CHANGED
@@ -213,8 +213,17 @@ transformed into various types.
213
213
  * `pattern`
214
214
  Defines a (ruby) regex pattern the value will be matched against. Must be either
215
215
  a string which should not be enclosed in `/` characters, or a Ruby Regexp.
216
- The pattern should generally start with `^` and end with `$` so as to evaluate
217
- the entire string.
216
+ The pattern should generally be anchored so as to evaluate the entire string.
217
+ In Ruby, `^` and `$` match at the start and the end of every *line*, so
218
+ `/^[0-9]+$/` accepts `"32\nfoo"`; `\A` and `\z` match only at the start and
219
+ the end of the string.
220
+
221
+ Patterns are exported to the JSON schema verbatim, and JSON Schema uses the
222
+ ECMA-262 dialect, which knows no `\A` and `\z` (it reads them as the literal
223
+ characters `A` and `z`) and where `^` and `$` already match the whole string
224
+ only. A pattern can therefore be correct on one side or the other, but not on
225
+ both: use `\A` and `\z` for schemas that are validated in Ruby, and `^` and
226
+ `$` for schemas that are exported and validated elsewhere.
218
227
  * `format`
219
228
  The `format` option allows for basic semantic validation on certain kinds of
220
229
  string values that are commonly used. See section *formats* for more
@@ -232,7 +241,15 @@ transformed into various types.
232
241
 
233
242
  Note that regardless of the `encoding` option, all strings are validated for
234
243
  valid encoding using `valid_encoding?`. Strings with invalid byte sequences for
235
- their declared encoding will always produce a validation error.
244
+ their declared encoding will always produce a validation error. The `pattern`
245
+ and `format` of such a string are not checked, as matching a pattern requires
246
+ decoding it; the checks that do not decode the string (`min_length`,
247
+ `max_length`, `encoding` and `enum`) are still reported.
248
+
249
+ A string whose encoding is valid but cannot be compared with the `pattern` or
250
+ with the pattern of the given `format` (e.g. a UTF-16 string) is reported as not
251
+ matching it, rather than raising an error. Note that `min_length` and
252
+ `max_length` count bytes for such an encoding, as it has no characters.
236
253
 
237
254
  #### Formats
238
255
 
@@ -295,12 +312,16 @@ You can also implement your custom formats or override the behavior of the
295
312
  standard formats. For Rails applications, this can be done in
296
313
  `config/schemacop.rb`:
297
314
 
315
+ Anchor the pattern with `\A` and `\z`. In Ruby, `^` and `$` match at the start
316
+ and the end of every line, so a multi-line value would pass the validation and
317
+ reach your handler.
318
+
298
319
  ```ruby
299
320
  # config/schemacop.rb
300
321
  Schemacop.register_string_formatter(
301
- :character_array, # Formatter name
302
- pattern: /^[a-zA-Z](,[a-zA-Z])*/, # Regex pattern for validation
303
- handler: ->(value) { value.split(',') } # Casting callback
322
+ :character_array, # Formatter name
323
+ pattern: /\A[a-zA-Z](,[a-zA-Z])*\z/, # Regex pattern for validation
324
+ handler: ->(value) { value.split(',') } # Casting callback
304
325
  )
305
326
 
306
327
  # In your schema
@@ -936,6 +957,9 @@ It consists of key-value-pairs that can be validated using arbitrary nodes.
936
957
  This option allows to specify a regexp pattern (as string) which validates the
937
958
  keys of any properties that are not specified in the hash. This option only
938
959
  makes sense if `additional_properties` is enabled. See below for more information.
960
+ The pattern is anchored the same way as the `pattern` option of the string
961
+ node, i.e. a pattern using `^` and `$` accepts a key that matches on any of
962
+ its lines.
939
963
 
940
964
  * `min_properties`
941
965
  Specifies the (inclusive) minimum number of properties a hash must contain.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 3.1.0
1
+ 3.1.1
@@ -168,21 +168,26 @@ module Schemacop
168
168
  property_names = options[:property_names]
169
169
  property_names = Regexp.compile(property_names) if property_names
170
170
 
171
+ # `name.to_sym` raises an EncodingError on a key with an invalid byte
172
+ # sequence, so the allow list is compared as strings.
173
+ if options[:ignore_obsolete_properties].is_a?(Enumerable)
174
+ obsolete_allow_list = options[:ignore_obsolete_properties].to_set(&:to_s)
175
+ end
176
+
171
177
  additional_properties.each do |name, additional_property|
172
- if property_names && !property_names.match?(name)
178
+ if property_names && !V3.match?(property_names, name.to_s)
173
179
  result.error "Property name #{name.inspect} does not match #{options[:property_names].inspect}."
174
180
  end
175
181
 
176
182
  if options[:additional_properties].is_a?(TrueClass)
177
183
  next
178
184
  elsif options[:additional_properties].is_a?(FalseClass) || options[:additional_properties].blank?
179
- match = property_patterns.keys.find { |p| p.match?(name.to_s) }
185
+ match = property_patterns.keys.find { |p| V3.match?(p, name.to_s) }
180
186
  if match
181
187
  result.in_path(name) do
182
188
  property_patterns[match]._validate(additional_property, result: result)
183
189
  end
184
- elsif (options[:ignore_obsolete_properties].is_a?(Enumerable) &&
185
- options[:ignore_obsolete_properties].exclude?(name.to_sym)) ||
190
+ elsif obsolete_allow_list&.exclude?(name.to_s) ||
186
191
  !options[:ignore_obsolete_properties]
187
192
  result.error "Obsolete property #{name.to_s.inspect}."
188
193
  end
@@ -196,7 +201,7 @@ module Schemacop
196
201
  # Validate dependencies #
197
202
  options[:dependencies]&.each do |source, targets|
198
203
  targets.each do |target|
199
- if data_hash[source].present? && data_hash[target].blank?
204
+ if !V3.blank?(data_hash[source]) && V3.blank?(data_hash[target])
200
205
  result.error "Missing property #{target.to_s.inspect} because #{source.to_s.inspect} is given."
201
206
  end
202
207
  end
@@ -268,23 +273,24 @@ module Schemacop
268
273
  end
269
274
 
270
275
  # Handle regex properties
276
+ # Property names are strings, see Node#init.
271
277
  specified_properties = @properties.keys.to_set + inline_ref_property_names
272
- additional_properties = data_hash.reject { |k, _v| specified_properties.include?(k.to_s.to_sym) }
278
+ additional_properties = data_hash.reject { |k, _v| specified_properties.include?(k.to_s) }
273
279
 
274
280
  if additional_properties.any? && property_patterns.any?
275
281
  additional_properties.each do |name, additional_property|
276
- match_key = property_patterns.keys.find { |p| p.match?(name.to_s) }
277
- match = property_patterns[match_key]
278
- result[name] = match.cast(additional_property)
282
+ match_key = property_patterns.keys.find { |p| V3.match?(p, name.to_s) }
283
+ next unless match_key
284
+
285
+ result[name] = property_patterns[match_key].cast(additional_property)
279
286
  end
280
287
  end
281
288
 
282
289
  # Handle additional properties
283
290
  if options[:additional_properties].is_a?(TrueClass)
284
- result = data_hash.merge(result)
291
+ result = data_hash.reject { |k, _v| specified_properties.include?(k.to_s) }.merge(result)
285
292
  elsif options[:additional_properties].is_a?(Node)
286
- add_prop_specified = @properties.keys.to_set + inline_ref_property_names
287
- additional_properties = data_hash.reject { |k, _v| add_prop_specified.include?(k.to_s.to_sym) }
293
+ additional_properties = data_hash.reject { |k, _v| specified_properties.include?(k.to_s) }
288
294
  if additional_properties.any?
289
295
  additional_properties_result = {}
290
296
  additional_properties.each do |key, value|
@@ -13,7 +13,7 @@ module Schemacop
13
13
  item = match(value)
14
14
 
15
15
  unless item
16
- if options[:treat_blank_as_nil] && value.blank? && !value.is_a?(FalseClass)
16
+ if options[:treat_blank_as_nil] && V3.blank?(value) && !value.is_a?(FalseClass)
17
17
  return nil
18
18
  else
19
19
  return value
@@ -37,7 +37,7 @@ module Schemacop
37
37
  public
38
38
 
39
39
  def _validate(data, result:)
40
- if options[:treat_blank_as_nil] && data.blank? && !data.is_a?(FalseClass)
40
+ if options[:treat_blank_as_nil] && V3.blank?(data) && !data.is_a?(FalseClass)
41
41
  data = nil
42
42
  end
43
43
 
@@ -55,10 +55,23 @@ module Schemacop
55
55
 
56
56
  def in_path(segment)
57
57
  prev_path = @current_path
58
- @current_path += [segment]
58
+ @current_path += [sanitize_segment(segment)]
59
59
  yield
60
60
  ensure
61
61
  @current_path = prev_path
62
62
  end
63
+
64
+ private
65
+
66
+ # A path segment can be a key of the validated data and therefore carry any
67
+ # encoding. Joining or splitting a message built from a string with an
68
+ # invalid byte sequence, or from one that is incompatible with the other
69
+ # messages, raises.
70
+ def sanitize_segment(segment)
71
+ return segment unless segment.is_a?(String)
72
+ return segment if segment.valid_encoding? && Encoding.compatible?(Encoding::UTF_8, segment)
73
+
74
+ return segment.scrub.encode(Encoding::UTF_8, invalid: :replace, undef: :replace)
75
+ end
63
76
  end
64
77
  end
@@ -27,7 +27,7 @@ module Schemacop
27
27
  super_data = super
28
28
 
29
29
  # Validate blank #
30
- if options[:allow_blank].is_a?(FalseClass) && super_data.blank?
30
+ if options[:allow_blank].is_a?(FalseClass) && V3.blank?(super_data)
31
31
  result.error 'String is blank but must not be blank!'
32
32
  end
33
33
 
@@ -44,17 +44,6 @@ module Schemacop
44
44
  result.error "String is #{length} characters long but must be at most #{options[:max_length]}."
45
45
  end
46
46
 
47
- # Validate pattern #
48
- if (pattern = options[:pattern])
49
- unless options[:pattern].is_a?(Regexp)
50
- pattern = Regexp.compile(pattern)
51
- end
52
-
53
- unless super_data.match?(pattern)
54
- result.error "String does not match pattern #{V3.sanitize_exp(pattern).inspect}."
55
- end
56
- end
57
-
58
47
  # Validate encoding matches #
59
48
  if options[:encoding]
60
49
  allowed_encodings = Array(options[:encoding])
@@ -63,16 +52,30 @@ module Schemacop
63
52
  end
64
53
  end
65
54
 
66
- # Validate encoding #
55
+ # Validate byte sequence #
56
+ # `match?` raises an ArgumentError on an invalid byte sequence, so no
57
+ # check below may see such a string.
67
58
  unless super_data.valid_encoding?
68
59
  result.error "String has invalid #{super_data.encoding.name.inspect} encoding."
60
+ return
61
+ end
62
+
63
+ # Validate pattern #
64
+ if (pattern = options[:pattern])
65
+ unless options[:pattern].is_a?(Regexp)
66
+ pattern = Regexp.compile(pattern)
67
+ end
68
+
69
+ unless V3.match?(pattern, super_data)
70
+ result.error "String does not match pattern #{V3.sanitize_exp(pattern).inspect}."
71
+ end
69
72
  end
70
73
 
71
74
  # Validate format #
72
75
  if options[:format] && Schemacop.string_formatters.include?(options[:format])
73
76
  pattern = Schemacop.string_formatters[options[:format]][:pattern]
74
77
 
75
- if pattern && !super_data.match?(pattern)
78
+ if pattern && !V3.match?(pattern, super_data)
76
79
  result.error "String does not match format #{options[:format].to_s.inspect}."
77
80
  elsif options[:format_options] && Node.resolve_class(options[:format])
78
81
  node = create(options[:format], **options[:format_options])
data/lib/schemacop/v3.rb CHANGED
@@ -4,6 +4,38 @@ module Schemacop
4
4
  NodeRegistry.register(*args)
5
5
  end
6
6
 
7
+ # `String#blank?` matches a regular expression and therefore raises on a
8
+ # string it cannot compare: an `ArgumentError` on an invalid byte sequence,
9
+ # a `RegexpError` on a dummy encoding such as UTF-16. Neither kind of
10
+ # string is blank unless it is empty.
11
+ # @private
12
+ def self.blank?(value)
13
+ return false if invalid_encoding?(value)
14
+
15
+ return value.blank?
16
+ rescue Encoding::CompatibilityError, RegexpError
17
+ return value.empty?
18
+ end
19
+
20
+ # `Regexp#match?` raises on a string it cannot compare: an `ArgumentError`
21
+ # on an invalid byte sequence, an `Encoding::CompatibilityError` on an
22
+ # encoding incompatible with the pattern's. Neither kind of string matches.
23
+ # @private
24
+ def self.match?(pattern, value)
25
+ return false if invalid_encoding?(value)
26
+
27
+ begin
28
+ return pattern.match?(value)
29
+ rescue Encoding::CompatibilityError
30
+ return false
31
+ end
32
+ end
33
+
34
+ # @private
35
+ def self.invalid_encoding?(value)
36
+ return value.is_a?(String) && !value.valid_encoding?
37
+ end
38
+
7
39
  # @private
8
40
  def self.sanitize_exp(exp)
9
41
  return exp if exp.is_a?(String)
data/lib/schemacop.rb CHANGED
@@ -5,6 +5,7 @@ require 'active_support/all'
5
5
  require 'set'
6
6
  require 'uri'
7
7
  require 'resolv'
8
+ require 'tempfile'
8
9
 
9
10
  # Schemacop module
10
11
  module Schemacop
@@ -22,6 +23,8 @@ module Schemacop
22
23
  mattr_accessor :v3_default_options
23
24
  self.v3_default_options = {}
24
25
 
26
+ # The pattern must be anchored with \A and \z: ^ and $ match at the start and
27
+ # the end of every line, so a multi-line value would reach the handler.
25
28
  def self.register_string_formatter(name, pattern:, handler:)
26
29
  name = name.to_s.dasherize.to_sym
27
30
 
@@ -33,21 +36,21 @@ module Schemacop
33
36
 
34
37
  register_string_formatter(
35
38
  :date,
36
- pattern: /^([0-9]{4})-?(1[0-2]|0[1-9])-?(3[01]|0[1-9]|[12][0-9])$/,
39
+ pattern: /\A([0-9]{4})-?(1[0-2]|0[1-9])-?(3[01]|0[1-9]|[12][0-9])\z/,
37
40
  handler: ->(value) { Date.parse(value) }
38
41
  )
39
42
 
40
43
  # rubocop: disable Layout/LineLength
41
44
  register_string_formatter(
42
45
  :'date-time',
43
- pattern: /^(-?(?:[1-9][0-9]*)?[0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])(\.[0-9]+)?(Z|[+-](?:2[0-3]|[01][0-9]):[0-5][0-9])?$/,
46
+ pattern: /\A(-?(?:[1-9][0-9]*)?[0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])(\.[0-9]+)?(Z|[+-](?:2[0-3]|[01][0-9]):[0-5][0-9])?\z/,
44
47
  handler: ->(value) { DateTime.parse(value) }
45
48
  )
46
49
  # rubocop: enable Layout/LineLength
47
50
 
48
51
  register_string_formatter(
49
52
  :time,
50
- pattern: /^(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])(\.[0-9]+)?(Z|[+-](?:2[0-3]|[01][0-9]):[0-5][0-9])?$/,
53
+ pattern: /\A(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])(\.[0-9]+)?(Z|[+-](?:2[0-3]|[01][0-9]):[0-5][0-9])?\z/,
51
54
  handler: ->(value) { Time.parse(value) }
52
55
  )
53
56
 
@@ -59,12 +62,12 @@ module Schemacop
59
62
 
60
63
  register_string_formatter(
61
64
  :mailbox,
62
- pattern: /^("\p{Print}+"\s)?<#{URI::MailTo::EMAIL_REGEXP.source[2...-2]}>$/,
65
+ pattern: /\A("\p{Print}+"\s)?<#{URI::MailTo::EMAIL_REGEXP.source[2...-2]}>\z/,
63
66
  handler: ->(value) { value }
64
67
  )
65
68
  register_string_formatter(
66
69
  :boolean,
67
- pattern: /^(true|false|0|1)$/i,
70
+ pattern: /\A(true|false|0|1)\z/i,
68
71
  handler: ->(value) { %w[true 1].include?(value&.downcase) }
69
72
  )
70
73
 
@@ -82,13 +85,13 @@ module Schemacop
82
85
 
83
86
  register_string_formatter(
84
87
  :integer,
85
- pattern: /^-?[0-9]+$/,
88
+ pattern: /\A-?[0-9]+\z/,
86
89
  handler: ->(value) { Integer(value, 10) }
87
90
  )
88
91
 
89
92
  register_string_formatter(
90
93
  :number,
91
- pattern: /^-?[0-9]+(\.[0-9]+)?$/,
94
+ pattern: /\A-?[0-9]+(\.[0-9]+)?\z/,
92
95
  handler: proc do |value|
93
96
  if value.include?('.')
94
97
  Float(value)
@@ -100,7 +103,7 @@ module Schemacop
100
103
 
101
104
  register_string_formatter(
102
105
  :'integer-list',
103
- pattern: /^(-?[0-9]+)(,-?[0-9]+)*$/,
106
+ pattern: /\A(-?[0-9]+)(,-?[0-9]+)*\z/,
104
107
  handler: ->(value) { value.split(',').map { |i| Integer(i, 10) } }
105
108
  )
106
109
 
data/schemacop.gemspec CHANGED
@@ -1,14 +1,14 @@
1
1
  # -*- encoding: utf-8 -*-
2
- # stub: schemacop 3.1.0 ruby lib
2
+ # stub: schemacop 3.1.1 ruby lib
3
3
 
4
4
  Gem::Specification.new do |s|
5
5
  s.name = "schemacop".freeze
6
- s.version = "3.1.0".freeze
6
+ s.version = "3.1.1".freeze
7
7
 
8
8
  s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
9
9
  s.require_paths = ["lib".freeze]
10
10
  s.authors = ["Sitrox".freeze]
11
- s.date = "2026-05-12"
11
+ s.date = "2026-09-08"
12
12
  s.files = [".github/workflows/ruby.yml".freeze, ".gitignore".freeze, ".releaser_config".freeze, ".rubocop.yml".freeze, ".yardopts".freeze, "CHANGELOG.md".freeze, "Gemfile".freeze, "LICENSE".freeze, "README.md".freeze, "README_V2.md".freeze, "README_V3.md".freeze, "RUBY_VERSION".freeze, "Rakefile".freeze, "VERSION".freeze, "lib/schemacop.rb".freeze, "lib/schemacop/base_schema.rb".freeze, "lib/schemacop/exceptions.rb".freeze, "lib/schemacop/railtie.rb".freeze, "lib/schemacop/schema.rb".freeze, "lib/schemacop/schema2.rb".freeze, "lib/schemacop/schema3.rb".freeze, "lib/schemacop/scoped_env.rb".freeze, "lib/schemacop/v2.rb".freeze, "lib/schemacop/v2/caster.rb".freeze, "lib/schemacop/v2/collector.rb".freeze, "lib/schemacop/v2/dupper.rb".freeze, "lib/schemacop/v2/field_node.rb".freeze, "lib/schemacop/v2/node.rb".freeze, "lib/schemacop/v2/node_resolver.rb".freeze, "lib/schemacop/v2/node_supporting_field.rb".freeze, "lib/schemacop/v2/node_supporting_type.rb".freeze, "lib/schemacop/v2/node_with_block.rb".freeze, "lib/schemacop/v2/validator/array_validator.rb".freeze, "lib/schemacop/v2/validator/boolean_validator.rb".freeze, "lib/schemacop/v2/validator/float_validator.rb".freeze, "lib/schemacop/v2/validator/hash_validator.rb".freeze, "lib/schemacop/v2/validator/integer_validator.rb".freeze, "lib/schemacop/v2/validator/nil_validator.rb".freeze, "lib/schemacop/v2/validator/number_validator.rb".freeze, "lib/schemacop/v2/validator/object_validator.rb".freeze, "lib/schemacop/v2/validator/string_validator.rb".freeze, "lib/schemacop/v2/validator/symbol_validator.rb".freeze, "lib/schemacop/v3.rb".freeze, "lib/schemacop/v3/all_of_node.rb".freeze, "lib/schemacop/v3/any_of_node.rb".freeze, "lib/schemacop/v3/array_node.rb".freeze, "lib/schemacop/v3/binary_node.rb".freeze, "lib/schemacop/v3/boolean_node.rb".freeze, "lib/schemacop/v3/combination_node.rb".freeze, "lib/schemacop/v3/context.rb".freeze, "lib/schemacop/v3/dsl_scope.rb".freeze, "lib/schemacop/v3/global_context.rb".freeze, "lib/schemacop/v3/hash_node.rb".freeze, "lib/schemacop/v3/integer_node.rb".freeze, "lib/schemacop/v3/is_not_node.rb".freeze, "lib/schemacop/v3/node.rb".freeze, "lib/schemacop/v3/node_registry.rb".freeze, "lib/schemacop/v3/number_node.rb".freeze, "lib/schemacop/v3/numeric_node.rb".freeze, "lib/schemacop/v3/object_node.rb".freeze, "lib/schemacop/v3/one_of_node.rb".freeze, "lib/schemacop/v3/reference_node.rb".freeze, "lib/schemacop/v3/result.rb".freeze, "lib/schemacop/v3/string_node.rb".freeze, "lib/schemacop/v3/symbol_node.rb".freeze, "schemacop.gemspec".freeze, "test/lib/test_helper.rb".freeze, "test/schemas/nested/group.rb".freeze, "test/schemas/user.rb".freeze, "test/unit/schemacop/v2/casting_test.rb".freeze, "test/unit/schemacop/v2/collector_test.rb".freeze, "test/unit/schemacop/v2/custom_check_test.rb".freeze, "test/unit/schemacop/v2/custom_if_test.rb".freeze, "test/unit/schemacop/v2/defaults_test.rb".freeze, "test/unit/schemacop/v2/empty_test.rb".freeze, "test/unit/schemacop/v2/nil_dis_allow_test.rb".freeze, "test/unit/schemacop/v2/node_resolver_test.rb".freeze, "test/unit/schemacop/v2/short_forms_test.rb".freeze, "test/unit/schemacop/v2/types_test.rb".freeze, "test/unit/schemacop/v2/validator_array_test.rb".freeze, "test/unit/schemacop/v2/validator_boolean_test.rb".freeze, "test/unit/schemacop/v2/validator_float_test.rb".freeze, "test/unit/schemacop/v2/validator_hash_test.rb".freeze, "test/unit/schemacop/v2/validator_integer_test.rb".freeze, "test/unit/schemacop/v2/validator_nil_test.rb".freeze, "test/unit/schemacop/v2/validator_number_test.rb".freeze, "test/unit/schemacop/v2/validator_object_test.rb".freeze, "test/unit/schemacop/v2/validator_string_test.rb".freeze, "test/unit/schemacop/v2/validator_symbol_test.rb".freeze, "test/unit/schemacop/v3/all_of_node_test.rb".freeze, "test/unit/schemacop/v3/any_of_node_test.rb".freeze, "test/unit/schemacop/v3/array_node_test.rb".freeze, "test/unit/schemacop/v3/binary_node_test.rb".freeze, "test/unit/schemacop/v3/boolean_node_test.rb".freeze, "test/unit/schemacop/v3/global_context_test.rb".freeze, "test/unit/schemacop/v3/hash_node_test.rb".freeze, "test/unit/schemacop/v3/integer_node_test.rb".freeze, "test/unit/schemacop/v3/is_not_node_test.rb".freeze, "test/unit/schemacop/v3/node_test.rb".freeze, "test/unit/schemacop/v3/number_node_test.rb".freeze, "test/unit/schemacop/v3/object_node_test.rb".freeze, "test/unit/schemacop/v3/one_of_node_test.rb".freeze, "test/unit/schemacop/v3/reference_node_test.rb".freeze, "test/unit/schemacop/v3/string_node_test.rb".freeze, "test/unit/schemacop/v3/symbol_node_test.rb".freeze]
13
13
  s.homepage = "https://github.com/sitrox/schemacop".freeze
14
14
  s.licenses = ["MIT".freeze]
@@ -96,6 +96,13 @@ class V3Test < SchemacopTest
96
96
  @schema = Schemacop::Schema3.new(type, **options, &block)
97
97
  end
98
98
 
99
+ # `\x80` is a UTF-8 continuation byte and invalid on its own, as it is in
100
+ # US-ASCII. Operations that decode the string (`blank?`, `match?`, `to_sym`)
101
+ # raise on it.
102
+ def invalid_string(encoding = 'UTF-8')
103
+ "abc\x80def".dup.force_encoding(encoding)
104
+ end
105
+
99
106
  def with_context(context, &block)
100
107
  Schemacop.with_context(context, &block)
101
108
  end
@@ -172,6 +172,26 @@ module Schemacop
172
172
  end
173
173
  end
174
174
 
175
+ def test_items_with_invalid_encoding
176
+ schema :array do
177
+ list :string
178
+ end
179
+
180
+ assert_validation ['foo', invalid_string, 'bar'] do
181
+ error '/[1]', 'String has invalid "UTF-8" encoding.'
182
+ end
183
+
184
+ schema :array, unique_items: true do
185
+ list :string
186
+ end
187
+
188
+ assert_validation [invalid_string, invalid_string] do
189
+ error '/', 'Array has duplicate items.'
190
+ error '/[0]', 'String has invalid "UTF-8" encoding.'
191
+ error '/[1]', 'String has invalid "UTF-8" encoding.'
192
+ end
193
+ end
194
+
175
195
  def test_single_item_tuple
176
196
  schema :array do
177
197
  str
@@ -13,6 +13,14 @@ module Schemacop
13
13
  assert_json(type: :string, format: :binary)
14
14
  end
15
15
 
16
+ def test_invalid_encoding
17
+ schema :binary
18
+
19
+ # Binary data is not decoded, so any byte sequence is accepted.
20
+ assert_validation invalid_string
21
+ assert_cast invalid_string, invalid_string
22
+ end
23
+
16
24
  def test_required
17
25
  schema :binary, required: true
18
26
 
@@ -49,6 +49,29 @@ module Schemacop
49
49
  end
50
50
  end
51
51
 
52
+ def test_parse_json_with_invalid_encoding
53
+ schema :hash, parse_json: true do
54
+ str! :name
55
+ end
56
+
57
+ assert_validation(%({"name":"foo"}))
58
+
59
+ # The invalid bytes survive parsing and are reported by the string node.
60
+ assert_validation(%({"name":"#{invalid_string}"})) do
61
+ error '/name', 'String has invalid "UTF-8" encoding.'
62
+ end
63
+ end
64
+
65
+ def test_property_value_with_invalid_encoding
66
+ schema do
67
+ str! :foo, format: :integer
68
+ end
69
+
70
+ assert_validation(foo: invalid_string) do
71
+ error '/foo', 'String has invalid "UTF-8" encoding.'
72
+ end
73
+ end
74
+
52
75
  def test_additional_properties_false_by_default
53
76
  schema
54
77
  assert_validation({})
@@ -350,6 +373,98 @@ module Schemacop
350
373
  assert_cast({ id_foo: 1, id_bar: 2, value: 4 }, { id_foo: 1, id_bar: 2, value: 4 }.with_indifferent_access)
351
374
  end
352
375
 
376
+ def test_encoding_invalid_bytes_property_name
377
+ schema :hash, additional_properties: true, property_names: '^[a-z]+$'
378
+
379
+ assert_validation(invalid_string => 'bar') do
380
+ error '/', 'Property name "abc\x80def" does not match "^[a-z]+$".'
381
+ end
382
+ end
383
+
384
+ def test_encoding_invalid_bytes_pattern_property_name
385
+ schema additional_properties: false do
386
+ str?(/^foo_.*$/)
387
+ end
388
+
389
+ assert_validation(invalid_string => 'bar') do
390
+ error '/', 'Obsolete property "abc\x80def".'
391
+ end
392
+ end
393
+
394
+ def test_encoding_invalid_bytes_pattern_property_casting
395
+ schema ignore_obsolete_properties: true do
396
+ str?(/^foo_.*$/)
397
+ end
398
+
399
+ assert_validation(foo_bar: 'baz', invalid_string => 'bar')
400
+ assert_cast({ :foo_bar => 'baz', invalid_string => 'bar' }, { foo_bar: 'baz' }.with_indifferent_access)
401
+ end
402
+
403
+ def test_encoding_invalid_bytes_property_name_with_obsolete_allow_list
404
+ schema :hash, ignore_obsolete_properties: %i[obsolete_key] do
405
+ str? :foo
406
+ end
407
+
408
+ assert_validation(:foo => 'bar', :obsolete_key => 'baz', invalid_string => 'qux') do
409
+ error '/', 'Obsolete property "abc\x80def".'
410
+ end
411
+
412
+ schema :hash, ignore_obsolete_properties: [invalid_string] do
413
+ str? :foo
414
+ end
415
+
416
+ assert_validation(:foo => 'bar', invalid_string => 'qux')
417
+ end
418
+
419
+ def test_encoding_invalid_bytes_in_error_path
420
+ schema :hash do
421
+ add :string
422
+ end
423
+
424
+ assert_validation(invalid_string => invalid_string) do
425
+ error '/abc�def', 'String has invalid "UTF-8" encoding.'
426
+ end
427
+
428
+ # The message is composed of the path and the error, so the path must
429
+ # not carry bytes the composition cannot handle.
430
+ assert_raises_with_message Exceptions::ValidationError,
431
+ '/abc�def: String has invalid "UTF-8" encoding.' do
432
+ @schema.validate!(invalid_string => invalid_string)
433
+ end
434
+ end
435
+
436
+ def test_encoding_invalid_bytes_dependency
437
+ schema :hash do
438
+ str? :credit_card
439
+ str? :billing_address
440
+
441
+ dep :credit_card, :billing_address
442
+ end
443
+
444
+ assert_validation(credit_card: invalid_string) do
445
+ error '/credit_card', 'String has invalid "UTF-8" encoding.'
446
+ error '/', 'Missing property "billing_address" because "credit_card" is given.'
447
+ end
448
+
449
+ # A target with an invalid byte sequence is given, not blank.
450
+ assert_validation(credit_card: 'foo', billing_address: invalid_string) do
451
+ error '/billing_address', 'String has invalid "UTF-8" encoding.'
452
+ end
453
+ end
454
+
455
+ def test_encoding_incompatible_dependency
456
+ schema :hash do
457
+ str? :credit_card
458
+ str? :billing_address
459
+
460
+ dep :credit_card, :billing_address
461
+ end
462
+
463
+ assert_validation(credit_card: 'foo'.encode('UTF-16')) do
464
+ error '/', 'Missing property "billing_address" because "credit_card" is given.'
465
+ end
466
+ end
467
+
353
468
  def test_defaults
354
469
  schema do
355
470
  str? :first_name, default: 'John'
@@ -1005,6 +1120,33 @@ module Schemacop
1005
1120
  assert_cast({ foo: 42 }, { bar: 42 }.with_indifferent_access)
1006
1121
  end
1007
1122
 
1123
+ def test_as_option_with_additional_properties
1124
+ schema do
1125
+ str? :foo, as: :bar
1126
+ add :string
1127
+ end
1128
+
1129
+ assert_cast({ foo: 'baz' }, { bar: 'baz' }.with_indifferent_access)
1130
+ assert_cast({ foo: 'baz', other: 'qux' }, { bar: 'baz', other: 'qux' }.with_indifferent_access)
1131
+
1132
+ schema :hash, additional_properties: true do
1133
+ str? :foo, as: :bar
1134
+ end
1135
+
1136
+ assert_cast({ foo: 'baz' }, { bar: 'baz' }.with_indifferent_access)
1137
+ assert_cast({ foo: 'baz', other: 42 }, { bar: 'baz', other: 42 }.with_indifferent_access)
1138
+ end
1139
+
1140
+ def test_as_option_with_pattern_property
1141
+ schema do
1142
+ str? :foo_bar, as: :bar, format: :integer
1143
+ str?(/^foo_.*$/)
1144
+ end
1145
+
1146
+ assert_cast({ foo_bar: '42' }, { bar: 42 }.with_indifferent_access)
1147
+ assert_cast({ foo_bar: '42', foo_qux: 'x' }, { bar: 42, foo_qux: 'x' }.with_indifferent_access)
1148
+ end
1149
+
1008
1150
  def test_as_option_overriding
1009
1151
  schema :hash do
1010
1152
  int? :foo, as: :bar
@@ -366,6 +366,20 @@ module Schemacop
366
366
  end
367
367
  end
368
368
 
369
+ def test_cast_str_with_invalid_encoding
370
+ schema :integer, cast_str: true
371
+
372
+ assert_validation(invalid_string) do
373
+ error '/', <<~PLAIN.strip
374
+ Matches 0 schemas but should match exactly 1:
375
+ - Schema 1:
376
+ - /: Invalid type, got type "String", expected "integer".
377
+ - Schema 2:
378
+ - /: String has invalid "UTF-8" encoding.
379
+ PLAIN
380
+ end
381
+ end
382
+
369
383
  def test_cast_str_required
370
384
  schema :integer, cast_str: true, required: true
371
385
 
@@ -25,6 +25,18 @@ module Schemacop
25
25
  end
26
26
  end
27
27
 
28
+ def test_invalid_encoding
29
+ schema :is_not do
30
+ str pattern: /\Aabc/
31
+ end
32
+
33
+ assert_validation('def')
34
+
35
+ # A string with an invalid byte sequence matches no pattern, so it does
36
+ # not match the inner schema either.
37
+ assert_validation(invalid_string)
38
+ end
39
+
28
40
  def test_required
29
41
  schema :is_not, required: true do
30
42
  int minimum: 5
@@ -15,6 +15,14 @@ module Schemacop
15
15
  assert_json({})
16
16
  end
17
17
 
18
+ def test_invalid_encoding
19
+ schema :object, classes: [String]
20
+
21
+ # An object is only checked for its class, never decoded.
22
+ assert_validation invalid_string
23
+ assert_cast invalid_string, invalid_string
24
+ end
25
+
18
26
  def test_required_with_no_types
19
27
  schema :object, required: true
20
28
 
@@ -260,6 +260,44 @@ module Schemacop
260
260
  assert_validation(true)
261
261
  end
262
262
 
263
+ def test_treat_blank_as_nil_with_invalid_encoding
264
+ schema :one_of, treat_blank_as_nil: true do
265
+ str
266
+ int
267
+ end
268
+
269
+ # A string with an invalid byte sequence is not blank, so it is matched
270
+ # against the items rather than treated as nil.
271
+ assert_validation invalid_string do
272
+ error '/', <<~PLAIN.strip
273
+ Matches 0 schemas but should match exactly 1:
274
+ - Schema 1:
275
+ - /: String has invalid "UTF-8" encoding.
276
+ - Schema 2:
277
+ - /: Invalid type, got type "String", expected "integer".
278
+ PLAIN
279
+ end
280
+ end
281
+
282
+ def test_treat_blank_as_nil_with_incompatible_encoding
283
+ schema :one_of, treat_blank_as_nil: true do
284
+ boo
285
+ str format: :boolean
286
+ end
287
+
288
+ assert_validation ''.encode('UTF-16')
289
+
290
+ assert_validation 'true'.encode('UTF-16') do
291
+ error '/', <<~PLAIN.strip
292
+ Matches 0 schemas but should match exactly 1:
293
+ - Schema 1:
294
+ - /: Invalid type, got type "String", expected "boolean".
295
+ - Schema 2:
296
+ - /: String does not match format "boolean".
297
+ PLAIN
298
+ end
299
+ end
300
+
263
301
  # With cast_str as default option, int gets wrapped in a OneOfNode containing
264
302
  # [IntegerNode, StringNode(format: :integer)]. When combined with a sibling str
265
303
  # node in one_of, numeric-looking strings like "1" match both the wrapped int
@@ -323,6 +323,10 @@ module Schemacop
323
323
  error '/', 'String does not match format "integer".'
324
324
  end
325
325
 
326
+ assert_validation "32\ntest" do
327
+ error '/', 'String does not match format "integer".'
328
+ end
329
+
326
330
  assert_cast(nil, nil)
327
331
  assert_cast('2234', 2234)
328
332
  assert_cast('-1', -1)
@@ -516,9 +520,11 @@ module Schemacop
516
520
  end
517
521
 
518
522
  def test_format_custom
523
+ formatters_was = Schemacop.string_formatters.dup
524
+
519
525
  Schemacop.register_string_formatter(
520
526
  :integer_tuple_list,
521
- pattern: /^(-?[0-9]+):(-?[0-9]+)(,(-?[0-9]+):(-?[0-9]+))*$/,
527
+ pattern: /\A(-?[0-9]+):(-?[0-9]+)(,(-?[0-9]+):(-?[0-9]+))*\z/,
522
528
  handler: proc do |value|
523
529
  value.split(',').map { |t| t.split(':').map(&:to_i) }
524
530
  end
@@ -539,8 +545,55 @@ module Schemacop
539
545
  error '/', 'String does not match format "integer-tuple-list".'
540
546
  end
541
547
 
548
+ assert_validation "1:5\nsd sfdij soidf" do
549
+ error '/', 'String does not match format "integer-tuple-list".'
550
+ end
551
+
542
552
  assert_cast nil, nil
543
553
  assert_cast '1:2,3:4,5:-6', [[1, 2], [3, 4], [5, -6]]
554
+ ensure
555
+ Schemacop.string_formatters = formatters_was
556
+ end
557
+
558
+ # A format pattern must be anchored with \A and \z: ^ and $ match the
559
+ # start and the end of any line, so a multi-line string would pass the
560
+ # validation and be handed to the cast handler.
561
+ def test_format_multiline
562
+ values = {
563
+ date: '2020-01-13',
564
+ date_time: '2018-11-13T20:20:39Z',
565
+ time: '20:30:39Z',
566
+ email: 'support@example.com',
567
+ mailbox: '<support@example.com>',
568
+ boolean: 'true',
569
+ integer: '23425',
570
+ number: '2.34',
571
+ integer_list: '1,2,3',
572
+ ipv4: '192.168.0.1',
573
+ ipv4_cidr: '192.168.0.1/24',
574
+ ipv6: '2001:db8::1'
575
+ }
576
+
577
+ assert_equal Schemacop.string_formatters.reject { |_, f| f[:pattern].nil? }.keys.sort,
578
+ values.keys.map { |format| format.to_s.dasherize.to_sym }.sort,
579
+ 'Every format with a pattern must be covered here'
580
+
581
+ values.each do |format, value|
582
+ name = format.to_s.dasherize.to_sym
583
+
584
+ schema :string, format: format
585
+
586
+ assert_validation value
587
+
588
+ ["#{value}\nfoo", "foo\n#{value}", "#{value}\n"].each do |multiline|
589
+ refute Schemacop.string_formatters[name][:pattern].match?(multiline),
590
+ "Format #{name} matches the multi-line value #{multiline.inspect}"
591
+
592
+ assert_validation multiline do
593
+ error '/', "String does not match format #{name.to_s.inspect}."
594
+ end
595
+ end
596
+ end
544
597
  end
545
598
 
546
599
  def test_enum
@@ -781,7 +834,6 @@ module Schemacop
781
834
  def test_encoding_invalid_bytes
782
835
  schema :string, encoding: 'UTF-8'
783
836
 
784
- invalid_string = "abc\x80def".force_encoding('UTF-8')
785
837
  assert_validation invalid_string do
786
838
  error '/', 'String has invalid "UTF-8" encoding.'
787
839
  end
@@ -790,12 +842,140 @@ module Schemacop
790
842
  def test_encoding_invalid_bytes_without_specific_encoding
791
843
  schema :string
792
844
 
793
- invalid_string = "abc\x80def".force_encoding('UTF-8')
794
845
  assert_validation invalid_string do
795
846
  error '/', 'String has invalid "UTF-8" encoding.'
796
847
  end
797
848
  end
798
849
 
850
+ def test_encoding_invalid_bytes_us_ascii
851
+ schema :string
852
+
853
+ assert_validation invalid_string('US-ASCII') do
854
+ error '/', 'String has invalid "US-ASCII" encoding.'
855
+ end
856
+ end
857
+
858
+ def test_encoding_invalid_bytes_and_encoding_mismatch
859
+ schema :string, encoding: 'UTF-8'
860
+
861
+ assert_validation invalid_string('US-ASCII') do
862
+ error '/', 'String has encoding "US-ASCII" but must be "UTF-8".'
863
+ error '/', 'String has invalid "US-ASCII" encoding.'
864
+ end
865
+ end
866
+
867
+ def test_encoding_invalid_bytes_with_format
868
+ schema :string, format: :integer
869
+
870
+ assert_validation invalid_string do
871
+ error '/', 'String has invalid "UTF-8" encoding.'
872
+ end
873
+ end
874
+
875
+ def test_encoding_invalid_bytes_with_format_options
876
+ schema :string, format: :integer, format_options: { minimum: 5 }
877
+
878
+ assert_validation invalid_string do
879
+ error '/', 'String has invalid "UTF-8" encoding.'
880
+ end
881
+ end
882
+
883
+ def test_encoding_invalid_bytes_with_format_without_pattern
884
+ schema :string, format: :symbol
885
+
886
+ assert_validation :foo.to_s
887
+
888
+ # The symbol formatter has no pattern to fail on, and its handler calls
889
+ # "to_sym", which raises on an invalid byte sequence.
890
+ assert_validation invalid_string do
891
+ error '/', 'String has invalid "UTF-8" encoding.'
892
+ end
893
+ end
894
+
895
+ def test_encoding_incompatible_with_pattern
896
+ schema :string, pattern: /^ü+$/
897
+
898
+ assert_validation 'üü'
899
+ assert_validation 'üü'.encode('ISO-8859-1') do
900
+ error '/', 'String does not match pattern "^ü+$".'
901
+ end
902
+ end
903
+
904
+ def test_encoding_incompatible_with_format
905
+ schema :string, format: :integer
906
+
907
+ assert_validation '42'
908
+
909
+ # UTF-16 is a dummy encoding and cannot be matched against a pattern in
910
+ # any other encoding, so the string does not match, valid as it looks.
911
+ assert_validation '42'.encode('UTF-16') do
912
+ error '/', 'String does not match format "integer".'
913
+ end
914
+ end
915
+
916
+ def test_encoding_invalid_bytes_with_pattern
917
+ schema :string, pattern: /\Aabc.*\z/
918
+
919
+ assert_validation invalid_string do
920
+ error '/', 'String has invalid "UTF-8" encoding.'
921
+ end
922
+ end
923
+
924
+ def test_encoding_invalid_bytes_with_allow_blank_false
925
+ schema :string, allow_blank: false
926
+
927
+ assert_validation invalid_string do
928
+ error '/', 'String has invalid "UTF-8" encoding.'
929
+ end
930
+ end
931
+
932
+ def test_encoding_incompatible_with_allow_blank_false
933
+ schema :string, allow_blank: false
934
+
935
+ assert_validation '42'.encode('UTF-16')
936
+ assert_validation ''.encode('UTF-16') do
937
+ error '/', 'String is blank but must not be blank!'
938
+ end
939
+ end
940
+
941
+ def test_encoding_binary_has_no_invalid_bytes
942
+ schema :string
943
+
944
+ # Every byte sequence is valid in ASCII-8BIT.
945
+ assert_validation invalid_string('ASCII-8BIT')
946
+ end
947
+
948
+ def test_encoding_invalid_bytes_with_length
949
+ schema :string, min_length: 20, max_length: 30
950
+
951
+ # The length checks do not decode the string, so they are still
952
+ # reported.
953
+ assert_validation invalid_string do
954
+ error '/', 'String is 7 characters long but must be at least 20.'
955
+ error '/', 'String has invalid "UTF-8" encoding.'
956
+ end
957
+ end
958
+
959
+ def test_encoding_incompatible_with_length
960
+ schema :string, max_length: 2
961
+
962
+ assert_validation '42'
963
+
964
+ # A dummy encoding has no characters, so "size" counts bytes.
965
+ assert_validation '42'.encode('UTF-16') do
966
+ error '/', 'String is 6 characters long but must be at most 2.'
967
+ end
968
+ end
969
+
970
+ def test_encoding_invalid_bytes_with_enum
971
+ schema :string, enum: ['foo']
972
+
973
+ assert_validation invalid_string do
974
+ error '/', 'Value not included in enum ["foo"].'
975
+ error '/', 'String has invalid "UTF-8" encoding.'
976
+ end
977
+ end
978
+
799
979
  def test_encoding_validate_self
800
980
  assert_raises_with_message Exceptions::InvalidSchemaError,
801
981
  'Option "encoding" must be a string or an array of strings.' do
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: schemacop
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.1.0
4
+ version: 3.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sitrox
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-05-12 00:00:00.000000000 Z
10
+ date: 2026-09-08 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: activesupport
@@ -164,7 +164,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
164
164
  - !ruby/object:Gem::Version
165
165
  version: '0'
166
166
  requirements: []
167
- rubygems_version: 4.0.2
167
+ rubygems_version: 4.0.8
168
168
  specification_version: 4
169
169
  summary: Schemacop validates ruby structures consisting of nested hashes and arrays
170
170
  against simple schema definitions.