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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +1 -1
  3. data/.rubocop.yml +4 -1
  4. data/CHANGELOG.md +87 -0
  5. data/Gemfile +1 -1
  6. data/README.md +8 -7
  7. data/README_V3.md +38 -5
  8. data/VERSION +1 -1
  9. data/lib/schemacop/schema3.rb +2 -2
  10. data/lib/schemacop/v2/node.rb +1 -1
  11. data/lib/schemacop/v2/validator/string_validator.rb +1 -1
  12. data/lib/schemacop/v3/any_of_node.rb +13 -0
  13. data/lib/schemacop/v3/array_node.rb +2 -3
  14. data/lib/schemacop/v3/global_context.rb +1 -1
  15. data/lib/schemacop/v3/hash_node.rb +23 -21
  16. data/lib/schemacop/v3/node.rb +5 -0
  17. data/lib/schemacop/v3/numeric_node.rb +2 -2
  18. data/lib/schemacop/v3/object_node.rb +1 -1
  19. data/lib/schemacop/v3/one_of_node.rb +16 -3
  20. data/lib/schemacop/v3/result.rb +14 -1
  21. data/lib/schemacop/v3/string_node.rb +32 -4
  22. data/lib/schemacop/v3.rb +32 -0
  23. data/lib/schemacop.rb +12 -9
  24. data/schemacop.gemspec +3 -3
  25. data/test/lib/test_helper.rb +7 -0
  26. data/test/unit/schemacop/v2/casting_test.rb +1 -1
  27. data/test/unit/schemacop/v2/custom_check_test.rb +2 -2
  28. data/test/unit/schemacop/v2/custom_if_test.rb +2 -2
  29. data/test/unit/schemacop/v3/any_of_node_test.rb +24 -0
  30. data/test/unit/schemacop/v3/array_node_test.rb +60 -1
  31. data/test/unit/schemacop/v3/binary_node_test.rb +8 -0
  32. data/test/unit/schemacop/v3/hash_node_test.rb +143 -1
  33. data/test/unit/schemacop/v3/integer_node_test.rb +15 -1
  34. data/test/unit/schemacop/v3/is_not_node_test.rb +12 -0
  35. data/test/unit/schemacop/v3/number_node_test.rb +1 -1
  36. data/test/unit/schemacop/v3/object_node_test.rb +8 -0
  37. data/test/unit/schemacop/v3/one_of_node_test.rb +61 -0
  38. data/test/unit/schemacop/v3/string_node_test.rb +250 -2
  39. 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: /^([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
 
@@ -77,18 +80,18 @@ module Schemacop
77
80
  register_string_formatter(
78
81
  :symbol,
79
82
  pattern: nil,
80
- handler: ->(value) { value.to_sym }
83
+ handler: lambda(&:to_sym)
81
84
  )
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.0.39 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.0.39".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-03-26"
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
@@ -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 { |i| i.even? }
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: ->(i) { i.even? }
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 { |data| data.odd? }
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 { |data| data.odd? }
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