schemacop 3.0.39 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5265f8c948ca9f0d23aa7e13ff78a5a980ce12ccd16c6fd1dae4e8ec6a30b833
4
- data.tar.gz: 77177403eccb133730a9359172c6d37362d2d76184a2758131bcd35fe5869f0a
3
+ metadata.gz: 9ef422cfe57686302457be2282f2b8e5f45ccdc79d6cd62597e8308bbb94026e
4
+ data.tar.gz: d45307834a863e1fdfdf616acbb9727fd78db428ce88436c27a67db88a3e802f
5
5
  SHA512:
6
- metadata.gz: 306a17bfebac65db3f0c7fbd37f2b5d0fa3c44b26c6217732241fa8cd43455a552a07f21327c805d421c0a2649f8b1b936b8063d9cf5b97673e416aea3dff8b6
7
- data.tar.gz: 64f2e2a148374b811fb4ac251606b25c48be802a1255e3dd72389aa2f4250f59695dba9b20b8cc62f48a9e646ea45d6cc9c9aeacc216b4704300b3864fa1701c
6
+ metadata.gz: 1253508e8f7270b6fa5f8f54563e79fa034a97fa8f89334f416d0e4549a5b374a2cdc0b3b4777e15202a7759a275d389a8390d67efd7acbb9114de38e21aceb1
7
+ data.tar.gz: 5aa472bf5a7d1aa37f946cf298b76349ad4faee611491c0ecf1bd377baa1127cfaae668f4a71ad00381a0afa88b5c23d073a3b84018ee5387808ad645766b37a
@@ -12,7 +12,7 @@ jobs:
12
12
  strategy:
13
13
  fail-fast: false
14
14
  matrix:
15
- ruby-version: ['2.6.2', '2.7.1', '3.0.1', '3.1.0', '3.2.0', '3.3.0', '3.4.0']
15
+ ruby-version: ['3.2.0', '3.3.0', '3.4.0', '4.0.0']
16
16
 
17
17
  steps:
18
18
  - uses: actions/checkout@v2
data/.rubocop.yml CHANGED
@@ -10,7 +10,7 @@ AllCops:
10
10
  - 'locale/translations.rb'
11
11
  - 'lib/scratch.rb'
12
12
  - 'schemacop.gemspec'
13
-
13
+ SuggestExtensions: false
14
14
  DisplayCopNames: true
15
15
 
16
16
  Style/SignalException:
@@ -108,4 +108,7 @@ Style/CaseLikeIf:
108
108
  Enabled: false
109
109
 
110
110
  Lint/EmptyClass:
111
+ Enabled: false
112
+
113
+ Lint/RedundantRequireStatement:
111
114
  Enabled: false
data/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # Change log
2
2
 
3
+ ## 3.1.0 (2026-05-09)
4
+
5
+ * Add option `encoding` to `str` node for Schemacop V3 schemas to validate that
6
+ a string has one of the specified encodings. Accepts a single encoding name or
7
+ an array of encoding names (e.g. `encoding: 'UTF-8'` or
8
+ `encoding: %w[UTF-8 US-ASCII]`).
9
+
10
+ Internal reference: `#143271`.
11
+
12
+ * All strings are now validated for valid encoding (via `valid_encoding?`),
13
+ regardless of whether the `encoding` option is set. Strings with invalid byte
14
+ sequences for their declared encoding will now produce a validation error.
15
+
16
+ Internal reference: `#143271`.
17
+
18
+ * Update RuboCop from 1.24.1 to 1.69.2.
19
+
20
+ * Drop support for all EOL Rubies (2.6.2, 2.7.1, 3.0.1, 3.1.0)
21
+
22
+ * Fix `cast_str` default option causing incorrect casting and validation errors
23
+ when used with combination nodes and array tuples. Numeric-looking strings
24
+ (e.g. `"1"`) were incorrectly cast to integers in schemas where a string type
25
+ was also valid. With this fix, `cast_str` only activates when the value cannot
26
+ be matched natively by another sibling schema.
27
+
28
+ Internal reference: `#149255`.
29
+
3
30
  ## 3.0.39 (2026-03-26)
4
31
 
5
32
  * Add `BinaryNode` for validating binary data fields such as file uploads.
data/Gemfile CHANGED
@@ -9,5 +9,5 @@ gem 'minitest'
9
9
  gem 'minitest-reporters'
10
10
  gem 'pry'
11
11
  gem 'rake'
12
- gem 'rubocop', '1.24.1'
12
+ gem 'rubocop', '1.69.2'
13
13
  gem 'simplecov', '0.21.2'
data/README.md CHANGED
@@ -12,13 +12,14 @@ use in conjunction with [OpenAPI](https://swagger.io/specification/).
12
12
 
13
13
  Schemacop is tested with the following ruby versions:
14
14
 
15
- * 2.6.2
16
- * 2.7.1
17
- * 3.0.1
18
- * 3.1.0
19
- * 3.2.0
20
- * 3.3.0
21
- * 3.4.0
15
+ | Schemacop | Ruby |
16
+ |:----------|:------------------------------------------------|
17
+ | >= 3.1.0 | 3.2.0, 3.3.0, 3.4.0, 4.0.0 |
18
+ | >= 3.0.31 | 2.6.2, 2.7.1, 3.0.1, 3.1.0, 3.2.0, 3.3.0, 3.4.0 |
19
+ | >= 3.0.29 | 2.6.2, 2.7.1, 3.0.1, 3.1.0, 3.2.0, 3.3.0 |
20
+ | >= 3.0.23 | 2.6.2, 2.7.1, 3.0.1, 3.1.0, 3.2.0 |
21
+ | >= 3.0.17 | 2.6.2, 2.7.1, 3.0.1, 3.1.0 |
22
+ | <= 3.0.16 | 2.6.2, 2.7.1, 3.0.1 |
22
23
 
23
24
  Other ruby versions might work but are not covered by our automated tests.
24
25
 
data/README_V3.md CHANGED
@@ -224,6 +224,15 @@ transformed into various types.
224
224
  By default, blank strings are allowed and left as they are when casted (e.g.
225
225
  the string `''` is valid). If you want to disallow blank strings, set this
226
226
  option to `false`.
227
+ * `encoding`
228
+ Validates the encoding of the string. Accepts a single encoding name or an
229
+ array of encoding names (e.g. `encoding: 'UTF-8'` or
230
+ `encoding: %w[UTF-8 US-ASCII]`). See `Encoding.name_list` for all available
231
+ encoding names.
232
+
233
+ Note that regardless of the `encoding` option, all strings are validated for
234
+ valid encoding using `valid_encoding?`. Strings with invalid byte sequences for
235
+ their declared encoding will always produce a validation error.
227
236
 
228
237
  #### Formats
229
238
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 3.0.39
1
+ 3.1.0
@@ -1,8 +1,8 @@
1
1
  module Schemacop
2
2
  class Schema3 < BaseSchema
3
- def initialize(*args, **kwargs, &block)
3
+ def initialize(...)
4
4
  super()
5
- @root = V3::Node.create(*args, **kwargs, &block)
5
+ @root = V3::Node.create(...)
6
6
  end
7
7
 
8
8
  # Validate data for the defined Schema
@@ -88,7 +88,7 @@ module Schemacop
88
88
 
89
89
  if option?(:cast) && self.class.klasses.size > 1
90
90
  fail Exceptions::InvalidSchemaError,
91
- "Casting is only allowed for single-value datatypes, but type #{self.class.inspect} has classes "\
91
+ "Casting is only allowed for single-value datatypes, but type #{self.class.inspect} has classes " \
92
92
  "#{self.class.klasses.map(&:inspect)}."
93
93
  end
94
94
  end
@@ -7,7 +7,7 @@ module Schemacop
7
7
  option :max
8
8
 
9
9
  def initialize(options = {})
10
- super(options)
10
+ super
11
11
 
12
12
  validate_options!
13
13
  end
@@ -5,6 +5,19 @@ module Schemacop
5
5
  :anyOf
6
6
  end
7
7
 
8
+ protected
9
+
10
+ def matches(data)
11
+ all_matches = super
12
+ if all_matches.size > 1
13
+ non_wrappers = all_matches.reject(&:cast_str_wrapper?)
14
+ return non_wrappers if non_wrappers.any?
15
+ end
16
+ all_matches
17
+ end
18
+
19
+ public
20
+
8
21
  def _validate(data, result:)
9
22
  super_data = super
10
23
  return if super_data.nil?
@@ -156,7 +156,7 @@ module Schemacop
156
156
  result = []
157
157
 
158
158
  value.each_with_index do |value_item, index|
159
- if cont_item.present? && item_matches?(cont_item, value_item)
159
+ if cont_item.present? && !cont_item.cast_str_wrapper? && item_matches?(cont_item, value_item)
160
160
  result << cont_item.cast(value_item)
161
161
  elsif list?
162
162
  result << list_item.cast(value_item)
@@ -168,8 +168,7 @@ module Schemacop
168
168
  result << value_item
169
169
  end
170
170
  else
171
- item = item_for_data(value_item)
172
- result << item.cast(value_item)
171
+ result << items[index].cast(value_item)
173
172
  end
174
173
  else
175
174
  result << value_item
@@ -97,7 +97,7 @@ module Schemacop
97
97
  fail "Schema #{path.inspect} does not define any schema."
98
98
  when 1
99
99
  if @schemas.include?(virtual_path)
100
- fail "Schema #{virtual_path.to_s.inspect} is defined in both load paths "\
100
+ fail "Schema #{virtual_path.to_s.inspect} is defined in both load paths " \
101
101
  "#{@load_paths_by_schemas[virtual_path].inspect} and #{load_path.inspect}."
102
102
  end
103
103
 
@@ -65,8 +65,8 @@ module Schemacop
65
65
  end
66
66
 
67
67
  json = {}
68
- json[:properties] = properties.values.map { |p| [p.name, p.as_json] }.to_h if properties.any?
69
- json[:patternProperties] = pattern_properties.values.map { |p| [V3.sanitize_exp(p.name), p.as_json] }.to_h if pattern_properties.any?
68
+ json[:properties] = properties.values.to_h { |p| [p.name, p.as_json] } if properties.any?
69
+ json[:patternProperties] = pattern_properties.values.to_h { |p| [V3.sanitize_exp(p.name), p.as_json] } if pattern_properties.any?
70
70
 
71
71
  # In schemacop, by default, additional properties are not allowed,
72
72
  # the users explicitly need to enable additional properties
@@ -300,19 +300,15 @@ module Schemacop
300
300
  protected
301
301
 
302
302
  def as_json_with_inline_refs(properties, pattern_properties)
303
- all_of = []
304
-
305
303
  # Add each inline ref
306
- @inline_refs.each do |inline_ref|
307
- all_of << inline_ref.as_json
308
- end
304
+ all_of = @inline_refs.map(&:as_json)
309
305
 
310
306
  # Add own properties schema if any direct properties exist
311
307
  if properties.any? || pattern_properties.any?
312
308
  own_schema = {}
313
309
  own_schema[:type] = :object
314
- own_schema[:properties] = properties.values.map { |p| [p.name, p.as_json] }.to_h if properties.any?
315
- own_schema[:patternProperties] = pattern_properties.values.map { |p| [V3.sanitize_exp(p.name), p.as_json] }.to_h if pattern_properties.any?
310
+ own_schema[:properties] = properties.values.to_h { |p| [p.name, p.as_json] } if properties.any?
311
+ own_schema[:patternProperties] = pattern_properties.values.to_h { |p| [V3.sanitize_exp(p.name), p.as_json] } if pattern_properties.any?
316
312
 
317
313
  if options[:additional_properties].is_a?(TrueClass)
318
314
  own_schema[:additionalProperties] = true
@@ -45,6 +45,7 @@ module Schemacop
45
45
  self.node node
46
46
  str format: format, format_options: options
47
47
  end
48
+ node.instance_variable_set(:@cast_str_wrapper, true)
48
49
  end
49
50
 
50
51
  return node
@@ -136,6 +137,10 @@ module Schemacop
136
137
  (parent&.schemas || {}).merge(@schemas)
137
138
  end
138
139
 
140
+ def cast_str_wrapper?
141
+ !!@cast_str_wrapper
142
+ end
143
+
139
144
  def required?
140
145
  @required
141
146
  end
@@ -30,7 +30,7 @@ module Schemacop
30
30
  attrs -= %i[exclusive_minimum exclusive_maximum]
31
31
  end
32
32
 
33
- super attrs, json
33
+ super
34
34
  end
35
35
 
36
36
  def _validate(data, result:)
@@ -102,7 +102,7 @@ module Schemacop
102
102
  fail 'Option "minimum" can\'t be greater than "maximum".'
103
103
  end
104
104
 
105
- if options[:exclusive_minimum] && options[:exclusive_maximum]\
105
+ if options[:exclusive_minimum] && options[:exclusive_maximum] \
106
106
  && options[:exclusive_minimum] > options[:exclusive_maximum]
107
107
  fail 'Option "exclusive_minimum" can\'t be greater than "exclusive_maximum".'
108
108
  end
@@ -12,7 +12,7 @@ module Schemacop
12
12
  protected
13
13
 
14
14
  def allowed_types
15
- @classes.map { |c| [c, c.name] }.to_h
15
+ @classes.to_h { |c| [c, c.name] }
16
16
  end
17
17
 
18
18
  def init
@@ -23,12 +23,25 @@ module Schemacop
23
23
  return item.cast(value)
24
24
  end
25
25
 
26
+ protected
27
+
28
+ def matches(data)
29
+ all_matches = super
30
+ if all_matches.size > 1
31
+ non_wrappers = all_matches.reject(&:cast_str_wrapper?)
32
+ return non_wrappers if non_wrappers.any?
33
+ end
34
+ all_matches
35
+ end
36
+
37
+ public
38
+
26
39
  def _validate(data, result:)
27
40
  if options[:treat_blank_as_nil] && data.blank? && !data.is_a?(FalseClass)
28
41
  data = nil
29
42
  end
30
43
 
31
- super_data = super(data, result: result)
44
+ super_data = super
32
45
  return if super_data.nil?
33
46
 
34
47
  matches = matches(super_data)
@@ -8,7 +8,7 @@ module Schemacop
8
8
  ].freeze
9
9
 
10
10
  def self.allowed_options
11
- super + ATTRIBUTES + %i[format_options pattern allow_blank]
11
+ super + ATTRIBUTES + %i[format_options pattern allow_blank encoding]
12
12
  end
13
13
 
14
14
  def allowed_types
@@ -55,6 +55,19 @@ module Schemacop
55
55
  end
56
56
  end
57
57
 
58
+ # Validate encoding matches #
59
+ if options[:encoding]
60
+ allowed_encodings = Array(options[:encoding])
61
+ unless allowed_encodings.include?(super_data.encoding.name)
62
+ result.error "String has encoding #{super_data.encoding.name.inspect} but must be #{allowed_encodings.map(&:inspect).join(' or ')}."
63
+ end
64
+ end
65
+
66
+ # Validate encoding #
67
+ unless super_data.valid_encoding?
68
+ result.error "String has invalid #{super_data.encoding.name.inspect} encoding."
69
+ end
70
+
58
71
  # Validate format #
59
72
  if options[:format] && Schemacop.string_formatters.include?(options[:format])
60
73
  pattern = Schemacop.string_formatters[options[:format]][:pattern]
@@ -109,6 +122,18 @@ module Schemacop
109
122
  fail 'Option "min_length" can\'t be greater than "max_length".'
110
123
  end
111
124
 
125
+ if options[:encoding]
126
+ unless options[:encoding].is_a?(String) || (options[:encoding].is_a?(Array) && options[:encoding].all? { |e| e.is_a?(String) })
127
+ fail 'Option "encoding" must be a string or an array of strings.'
128
+ end
129
+
130
+ Array(options[:encoding]).each do |encoding|
131
+ Encoding.find(encoding)
132
+ rescue ArgumentError
133
+ fail "Option \"encoding\" contains unknown encoding #{encoding.inspect}."
134
+ end
135
+ end
136
+
112
137
  if options[:pattern]
113
138
  unless options[:pattern].is_a?(String) || options[:pattern].is_a?(Regexp)
114
139
  fail 'Option "pattern" must be a string or Regexp.'
data/lib/schemacop.rb CHANGED
@@ -77,7 +77,7 @@ module Schemacop
77
77
  register_string_formatter(
78
78
  :symbol,
79
79
  pattern: nil,
80
- handler: ->(value) { value.to_sym }
80
+ handler: lambda(&:to_sym)
81
81
  )
82
82
 
83
83
  register_string_formatter(
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.0 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.0".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-05-12"
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]
@@ -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
@@ -988,13 +988,52 @@ module Schemacop
988
988
  end
989
989
 
990
990
  assert_validation('{42]') do
991
- error '/', /JSON parse error: "((\d+: )?unexpected token at '{42]'|expected object key, got '42]' at line \d+ column \d+)"\./
991
+ error '/', /JSON parse error: "((\d+: )?unexpected token at '{42\]'|expected object key, got '42\]' at line \d+ column \d+)"\./
992
992
  end
993
993
 
994
994
  assert_validation('"foo"') do
995
995
  error '/', 'Invalid type, got type "String", expected "array".'
996
996
  end
997
997
  end
998
+
999
+ def test_tuple_cast_with_default_cast_str
1000
+ Schemacop.v3_default_options = { cast_str: true }.freeze
1001
+
1002
+ schema :array do
1003
+ list :array do
1004
+ int
1005
+ str
1006
+ end
1007
+ end
1008
+
1009
+ # String "1" at position 1 (str) must not be cast to integer
1010
+ assert_validation([[1, '1']])
1011
+ assert_cast([[1, '1']], [[1, '1']])
1012
+
1013
+ # String "1" at position 0 (int with cast_str) should be cast to integer
1014
+ assert_validation([%w[1 foo]])
1015
+ assert_cast([%w[1 foo]], [[1, 'foo']])
1016
+ ensure
1017
+ Schemacop.v3_default_options = {}
1018
+ end
1019
+
1020
+ # With cast_str as default option, cont :integer creates a OneOfNode containing
1021
+ # [IntegerNode, StringNode(format: :integer)]. The cont_item takes priority in
1022
+ # casting over the list_item, so numeric-looking strings that match the cont
1023
+ # schema get cast to integers even though the list schema says they are strings.
1024
+ def test_cont_cast_with_default_cast_str
1025
+ Schemacop.v3_default_options = { cast_str: true }.freeze
1026
+
1027
+ schema :array do
1028
+ list :string
1029
+ cont :integer
1030
+ end
1031
+
1032
+ assert_validation(%w[hello 42 world])
1033
+ assert_cast(%w[hello 42 world], %w[hello 42 world])
1034
+ ensure
1035
+ Schemacop.v3_default_options = {}
1036
+ end
998
1037
  end
999
1038
  end
1000
1039
  end
@@ -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
@@ -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
@@ -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
@@ -259,6 +259,29 @@ module Schemacop
259
259
  assert_validation('true')
260
260
  assert_validation(true)
261
261
  end
262
+
263
+ # With cast_str as default option, int gets wrapped in a OneOfNode containing
264
+ # [IntegerNode, StringNode(format: :integer)]. When combined with a sibling str
265
+ # node in one_of, numeric-looking strings like "1" match both the wrapped int
266
+ # (via StringNode(format: :integer)) and the plain str, causing a "matches 2"
267
+ # validation error.
268
+ def test_default_cast_str_with_int_and_str
269
+ Schemacop.v3_default_options = { cast_str: true }.freeze
270
+
271
+ schema :one_of do
272
+ int
273
+ str
274
+ end
275
+
276
+ assert_validation(42)
277
+ assert_validation('hello')
278
+ assert_validation('1')
279
+ assert_cast(42, 42)
280
+ assert_cast('hello', 'hello')
281
+ assert_cast('1', '1')
282
+ ensure
283
+ Schemacop.v3_default_options = {}
284
+ end
262
285
  end
263
286
  end
264
287
  end
@@ -653,7 +653,7 @@ module Schemacop
653
653
  end
654
654
 
655
655
  assert_raises_with_message Exceptions::InvalidSchemaError,
656
- 'Option "pattern" can\'t be parsed: end pattern '\
656
+ 'Option "pattern" can\'t be parsed: end pattern ' \
657
657
  'with unmatched parenthesis: /(abcde/.' do
658
658
  schema :string, pattern: '(abcde'
659
659
  end
@@ -750,6 +750,74 @@ module Schemacop
750
750
  assert_cast("\t", "\t")
751
751
  end
752
752
 
753
+ def test_encoding_single
754
+ schema :string, encoding: 'UTF-8'
755
+
756
+ assert_validation 'Hello World'
757
+ assert_validation ''
758
+
759
+ assert_validation 'Hello World'.encode('ASCII') do
760
+ error '/', 'String has encoding "US-ASCII" but must be "UTF-8".'
761
+ end
762
+ end
763
+
764
+ def test_encoding_multiple
765
+ schema :string, encoding: %w[UTF-8 US-ASCII]
766
+
767
+ assert_validation 'Hello World'
768
+ assert_validation 'Hello World'.encode('ASCII')
769
+
770
+ assert_validation 'Hello World'.encode('ISO-8859-1') do
771
+ error '/', 'String has encoding "ISO-8859-1" but must be "UTF-8" or "US-ASCII".'
772
+ end
773
+ end
774
+
775
+ def test_encoding_with_nil
776
+ schema :string, encoding: 'UTF-8'
777
+
778
+ assert_validation nil
779
+ end
780
+
781
+ def test_encoding_invalid_bytes
782
+ schema :string, encoding: 'UTF-8'
783
+
784
+ invalid_string = "abc\x80def".force_encoding('UTF-8')
785
+ assert_validation invalid_string do
786
+ error '/', 'String has invalid "UTF-8" encoding.'
787
+ end
788
+ end
789
+
790
+ def test_encoding_invalid_bytes_without_specific_encoding
791
+ schema :string
792
+
793
+ invalid_string = "abc\x80def".force_encoding('UTF-8')
794
+ assert_validation invalid_string do
795
+ error '/', 'String has invalid "UTF-8" encoding.'
796
+ end
797
+ end
798
+
799
+ def test_encoding_validate_self
800
+ assert_raises_with_message Exceptions::InvalidSchemaError,
801
+ 'Option "encoding" must be a string or an array of strings.' do
802
+ schema :string, encoding: 123
803
+ end
804
+
805
+ assert_raises_with_message Exceptions::InvalidSchemaError,
806
+ 'Option "encoding" must be a string or an array of strings.' do
807
+ schema :string, encoding: [123]
808
+ end
809
+
810
+ assert_raises_with_message Exceptions::InvalidSchemaError,
811
+ 'Option "encoding" contains unknown encoding "UNKNOWN-FOO".' do
812
+ schema :string, encoding: 'UNKNOWN-FOO'
813
+ end
814
+
815
+ assert_raises_with_message Exceptions::InvalidSchemaError,
816
+ 'Option "encoding" contains unknown encoding "UNKNOWN-FOO".' do
817
+ schema :string, encoding: %w[UTF-8 UNKNOWN-FOO]
818
+ end
819
+ end
820
+
753
821
  def test_empty_or_whitespace_string_blank_not_allowed
754
822
  schema :string, allow_blank: false
755
823
 
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.0.39
4
+ version: 3.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sitrox
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-03-26 00:00:00.000000000 Z
10
+ date: 2026-05-12 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: activesupport