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 +4 -4
- data/CHANGELOG.md +60 -0
- data/README_V3.md +30 -6
- data/VERSION +1 -1
- data/lib/schemacop/v3/hash_node.rb +18 -12
- data/lib/schemacop/v3/one_of_node.rb +2 -2
- data/lib/schemacop/v3/result.rb +14 -1
- data/lib/schemacop/v3/string_node.rb +17 -14
- data/lib/schemacop/v3.rb +32 -0
- data/lib/schemacop.rb +11 -8
- data/schemacop.gemspec +3 -3
- data/test/lib/test_helper.rb +7 -0
- data/test/unit/schemacop/v3/array_node_test.rb +20 -0
- data/test/unit/schemacop/v3/binary_node_test.rb +8 -0
- data/test/unit/schemacop/v3/hash_node_test.rb +142 -0
- data/test/unit/schemacop/v3/integer_node_test.rb +14 -0
- data/test/unit/schemacop/v3/is_not_node_test.rb +12 -0
- data/test/unit/schemacop/v3/object_node_test.rb +8 -0
- data/test/unit/schemacop/v3/one_of_node_test.rb +38 -0
- data/test/unit/schemacop/v3/string_node_test.rb +183 -3
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f790c910d2c32accea93c9f4e67b85c80ababfbaca2b64930d7e380a65125d9e
|
|
4
|
+
data.tar.gz: ae3f22c58697112408efde125190a0cd8518199ceb8b47b65094e289d9e8769d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
217
|
-
the
|
|
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,
|
|
302
|
-
pattern:
|
|
303
|
-
handler: ->(value) { value.split(',') }
|
|
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.
|
|
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 && !
|
|
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|
|
|
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 (
|
|
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]
|
|
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
|
|
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|
|
|
277
|
-
|
|
278
|
-
|
|
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
|
-
|
|
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] &&
|
|
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] &&
|
|
40
|
+
if options[:treat_blank_as_nil] && V3.blank?(data) && !data.is_a?(FalseClass)
|
|
41
41
|
data = nil
|
|
42
42
|
end
|
|
43
43
|
|
data/lib/schemacop/v3/result.rb
CHANGED
|
@@ -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) &&
|
|
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
|
|
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 && !
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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.
|
|
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.
|
|
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-
|
|
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]
|
data/test/lib/test_helper.rb
CHANGED
|
@@ -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:
|
|
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.
|
|
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-
|
|
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.
|
|
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.
|