schemacop 3.0.39 → 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/.github/workflows/ruby.yml +1 -1
- data/.rubocop.yml +4 -1
- data/CHANGELOG.md +87 -0
- data/Gemfile +1 -1
- data/README.md +8 -7
- data/README_V3.md +38 -5
- data/VERSION +1 -1
- data/lib/schemacop/schema3.rb +2 -2
- data/lib/schemacop/v2/node.rb +1 -1
- data/lib/schemacop/v2/validator/string_validator.rb +1 -1
- data/lib/schemacop/v3/any_of_node.rb +13 -0
- data/lib/schemacop/v3/array_node.rb +2 -3
- data/lib/schemacop/v3/global_context.rb +1 -1
- data/lib/schemacop/v3/hash_node.rb +23 -21
- data/lib/schemacop/v3/node.rb +5 -0
- data/lib/schemacop/v3/numeric_node.rb +2 -2
- data/lib/schemacop/v3/object_node.rb +1 -1
- data/lib/schemacop/v3/one_of_node.rb +16 -3
- data/lib/schemacop/v3/result.rb +14 -1
- data/lib/schemacop/v3/string_node.rb +32 -4
- data/lib/schemacop/v3.rb +32 -0
- data/lib/schemacop.rb +12 -9
- data/schemacop.gemspec +3 -3
- data/test/lib/test_helper.rb +7 -0
- data/test/unit/schemacop/v2/casting_test.rb +1 -1
- data/test/unit/schemacop/v2/custom_check_test.rb +2 -2
- data/test/unit/schemacop/v2/custom_if_test.rb +2 -2
- data/test/unit/schemacop/v3/any_of_node_test.rb +24 -0
- data/test/unit/schemacop/v3/array_node_test.rb +60 -1
- data/test/unit/schemacop/v3/binary_node_test.rb +8 -0
- data/test/unit/schemacop/v3/hash_node_test.rb +143 -1
- data/test/unit/schemacop/v3/integer_node_test.rb +15 -1
- data/test/unit/schemacop/v3/is_not_node_test.rb +12 -0
- data/test/unit/schemacop/v3/number_node_test.rb +1 -1
- data/test/unit/schemacop/v3/object_node_test.rb +8 -0
- data/test/unit/schemacop/v3/one_of_node_test.rb +61 -0
- data/test/unit/schemacop/v3/string_node_test.rb +250 -2
- metadata +3 -3
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
|
|
|
@@ -77,18 +80,18 @@ module Schemacop
|
|
|
77
80
|
register_string_formatter(
|
|
78
81
|
:symbol,
|
|
79
82
|
pattern: nil,
|
|
80
|
-
handler:
|
|
83
|
+
handler: lambda(&:to_sym)
|
|
81
84
|
)
|
|
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.
|
|
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.
|
|
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
|
|
@@ -75,7 +75,7 @@ module Schemacop
|
|
|
75
75
|
end
|
|
76
76
|
end
|
|
77
77
|
|
|
78
|
-
assert_equal 'Casting is only allowed for single-value datatypes, '\
|
|
78
|
+
assert_equal 'Casting is only allowed for single-value datatypes, ' \
|
|
79
79
|
'but type Schemacop::V2::NumberValidator has classes ["Integer", "Float"].',
|
|
80
80
|
e.message
|
|
81
81
|
end
|
|
@@ -4,7 +4,7 @@ module Schemacop
|
|
|
4
4
|
module V2
|
|
5
5
|
class CustomCheckTest < V2Test
|
|
6
6
|
def test_integer_check_short_form
|
|
7
|
-
s = Schema.new :integer, check: proc
|
|
7
|
+
s = Schema.new :integer, check: proc(&:even?)
|
|
8
8
|
assert_nothing_raised { s.validate!(2) }
|
|
9
9
|
assert_nothing_raised { s.validate!(-8) }
|
|
10
10
|
assert_nothing_raised { s.validate!(0) }
|
|
@@ -22,7 +22,7 @@ module Schemacop
|
|
|
22
22
|
|
|
23
23
|
def test_integer_check_with_lambda
|
|
24
24
|
s = Schema.new do
|
|
25
|
-
type :integer, check:
|
|
25
|
+
type :integer, check: lambda(&:even?)
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
assert_nothing_raised { s.validate!(2) }
|
|
@@ -5,7 +5,7 @@ module Schemacop
|
|
|
5
5
|
class CustomIfTest < V2Test
|
|
6
6
|
def test_allowed_subset_only
|
|
7
7
|
s = Schema.new do
|
|
8
|
-
type :integer, if: proc
|
|
8
|
+
type :integer, if: proc(&:odd?)
|
|
9
9
|
end
|
|
10
10
|
|
|
11
11
|
assert_nothing_raised { s.validate! 5 }
|
|
@@ -15,7 +15,7 @@ module Schemacop
|
|
|
15
15
|
|
|
16
16
|
def test_if_with_multiple_types
|
|
17
17
|
s = Schema.new do
|
|
18
|
-
type :integer, if: proc
|
|
18
|
+
type :integer, if: proc(&:odd?)
|
|
19
19
|
type :string
|
|
20
20
|
end
|
|
21
21
|
|
|
@@ -362,6 +362,30 @@ module Schemacop
|
|
|
362
362
|
schema :any_of
|
|
363
363
|
end
|
|
364
364
|
end
|
|
365
|
+
|
|
366
|
+
# With cast_str as default option, int gets wrapped in a OneOfNode containing
|
|
367
|
+
# [IntegerNode, StringNode(format: :integer)]. In any_of, the first matching
|
|
368
|
+
# schema is used for casting. Since the wrapped int node matches numeric-looking
|
|
369
|
+
# strings (via StringNode(format: :integer)), a string like "1" gets cast to
|
|
370
|
+
# integer 1, even though the plain str branch also matches and the value is
|
|
371
|
+
# already a valid string.
|
|
372
|
+
def test_default_cast_str_with_int_and_str
|
|
373
|
+
Schemacop.v3_default_options = { cast_str: true }.freeze
|
|
374
|
+
|
|
375
|
+
schema :any_of do
|
|
376
|
+
int
|
|
377
|
+
str
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
assert_validation(42)
|
|
381
|
+
assert_validation('hello')
|
|
382
|
+
assert_validation('1')
|
|
383
|
+
assert_cast(42, 42)
|
|
384
|
+
assert_cast('hello', 'hello')
|
|
385
|
+
assert_cast('1', '1')
|
|
386
|
+
ensure
|
|
387
|
+
Schemacop.v3_default_options = {}
|
|
388
|
+
end
|
|
365
389
|
end
|
|
366
390
|
end
|
|
367
391
|
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
|
|
@@ -988,13 +1008,52 @@ module Schemacop
|
|
|
988
1008
|
end
|
|
989
1009
|
|
|
990
1010
|
assert_validation('{42]') do
|
|
991
|
-
error '/', /JSON parse error: "((\d+: )?unexpected token at '{42]'|expected object key, got '42]' at line \d+ column \d+)"\./
|
|
1011
|
+
error '/', /JSON parse error: "((\d+: )?unexpected token at '{42\]'|expected object key, got '42\]' at line \d+ column \d+)"\./
|
|
992
1012
|
end
|
|
993
1013
|
|
|
994
1014
|
assert_validation('"foo"') do
|
|
995
1015
|
error '/', 'Invalid type, got type "String", expected "array".'
|
|
996
1016
|
end
|
|
997
1017
|
end
|
|
1018
|
+
|
|
1019
|
+
def test_tuple_cast_with_default_cast_str
|
|
1020
|
+
Schemacop.v3_default_options = { cast_str: true }.freeze
|
|
1021
|
+
|
|
1022
|
+
schema :array do
|
|
1023
|
+
list :array do
|
|
1024
|
+
int
|
|
1025
|
+
str
|
|
1026
|
+
end
|
|
1027
|
+
end
|
|
1028
|
+
|
|
1029
|
+
# String "1" at position 1 (str) must not be cast to integer
|
|
1030
|
+
assert_validation([[1, '1']])
|
|
1031
|
+
assert_cast([[1, '1']], [[1, '1']])
|
|
1032
|
+
|
|
1033
|
+
# String "1" at position 0 (int with cast_str) should be cast to integer
|
|
1034
|
+
assert_validation([%w[1 foo]])
|
|
1035
|
+
assert_cast([%w[1 foo]], [[1, 'foo']])
|
|
1036
|
+
ensure
|
|
1037
|
+
Schemacop.v3_default_options = {}
|
|
1038
|
+
end
|
|
1039
|
+
|
|
1040
|
+
# With cast_str as default option, cont :integer creates a OneOfNode containing
|
|
1041
|
+
# [IntegerNode, StringNode(format: :integer)]. The cont_item takes priority in
|
|
1042
|
+
# casting over the list_item, so numeric-looking strings that match the cont
|
|
1043
|
+
# schema get cast to integers even though the list schema says they are strings.
|
|
1044
|
+
def test_cont_cast_with_default_cast_str
|
|
1045
|
+
Schemacop.v3_default_options = { cast_str: true }.freeze
|
|
1046
|
+
|
|
1047
|
+
schema :array do
|
|
1048
|
+
list :string
|
|
1049
|
+
cont :integer
|
|
1050
|
+
end
|
|
1051
|
+
|
|
1052
|
+
assert_validation(%w[hello 42 world])
|
|
1053
|
+
assert_cast(%w[hello 42 world], %w[hello 42 world])
|
|
1054
|
+
ensure
|
|
1055
|
+
Schemacop.v3_default_options = {}
|
|
1056
|
+
end
|
|
998
1057
|
end
|
|
999
1058
|
end
|
|
1000
1059
|
end
|
|
@@ -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
|
|
|
@@ -32,7 +32,7 @@ module Schemacop
|
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
assert_validation('{42]') do
|
|
35
|
-
error '/', /JSON parse error: "((\d+: )?unexpected token at '{42]'|expected object key, got '42]' at line \d+ column \d+)"\./
|
|
35
|
+
error '/', /JSON parse error: "((\d+: )?unexpected token at '{42\]'|expected object key, got '42\]' at line \d+ column \d+)"\./
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
assert_validation('"foo"') do
|
|
@@ -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
|
|
@@ -256,7 +256,7 @@ module Schemacop
|
|
|
256
256
|
end
|
|
257
257
|
|
|
258
258
|
assert_raises_with_message Exceptions::InvalidSchemaError,
|
|
259
|
-
'Option "exclusive_minimum" can\'t be '\
|
|
259
|
+
'Option "exclusive_minimum" can\'t be ' \
|
|
260
260
|
'greater than "exclusive_maximum".' do
|
|
261
261
|
schema :integer, exclusive_minimum: 5, exclusive_maximum: 4
|
|
262
262
|
end
|
|
@@ -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
|
|
@@ -298,7 +298,7 @@ module Schemacop
|
|
|
298
298
|
end
|
|
299
299
|
|
|
300
300
|
assert_raises_with_message Exceptions::InvalidSchemaError,
|
|
301
|
-
'Option "exclusive_minimum" can\'t be '\
|
|
301
|
+
'Option "exclusive_minimum" can\'t be ' \
|
|
302
302
|
'greater than "exclusive_maximum".' do
|
|
303
303
|
schema :number, exclusive_minimum: 5, exclusive_maximum: 4
|
|
304
304
|
end
|
|
@@ -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
|
|
|
@@ -259,6 +259,67 @@ module Schemacop
|
|
|
259
259
|
assert_validation('true')
|
|
260
260
|
assert_validation(true)
|
|
261
261
|
end
|
|
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
|
+
|
|
301
|
+
# With cast_str as default option, int gets wrapped in a OneOfNode containing
|
|
302
|
+
# [IntegerNode, StringNode(format: :integer)]. When combined with a sibling str
|
|
303
|
+
# node in one_of, numeric-looking strings like "1" match both the wrapped int
|
|
304
|
+
# (via StringNode(format: :integer)) and the plain str, causing a "matches 2"
|
|
305
|
+
# validation error.
|
|
306
|
+
def test_default_cast_str_with_int_and_str
|
|
307
|
+
Schemacop.v3_default_options = { cast_str: true }.freeze
|
|
308
|
+
|
|
309
|
+
schema :one_of do
|
|
310
|
+
int
|
|
311
|
+
str
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
assert_validation(42)
|
|
315
|
+
assert_validation('hello')
|
|
316
|
+
assert_validation('1')
|
|
317
|
+
assert_cast(42, 42)
|
|
318
|
+
assert_cast('hello', 'hello')
|
|
319
|
+
assert_cast('1', '1')
|
|
320
|
+
ensure
|
|
321
|
+
Schemacop.v3_default_options = {}
|
|
322
|
+
end
|
|
262
323
|
end
|
|
263
324
|
end
|
|
264
325
|
end
|