schemacop 3.0.38 → 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: 4a3a54c489ac60b4a01424d7a4949724c15fdec793de16a4375e3097f691128b
4
- data.tar.gz: 48390b41d456f5362ecabf0e97ed37fdc035247ec3d8335d105e97ab0d858378
3
+ metadata.gz: 9ef422cfe57686302457be2282f2b8e5f45ccdc79d6cd62597e8308bbb94026e
4
+ data.tar.gz: d45307834a863e1fdfdf616acbb9727fd78db428ce88436c27a67db88a3e802f
5
5
  SHA512:
6
- metadata.gz: 911a618277694321833a85cc051ff139b14e9a1117dab7380bcc4c9ebc08e87a258f131224e1f3cef40d5b6495de6142f1b6f23009ce62917c8a07bf75960678
7
- data.tar.gz: 1add721c851198435cf89b0b9d95d94c607bc443df180072fda1b35d4b956a4a2bdab429252f630fca8e4ddf09baf13153278d60f401318e0ad591bbc7c481b8
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,40 @@
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
+
30
+ ## 3.0.39 (2026-03-26)
31
+
32
+ * Add `BinaryNode` for validating binary data fields such as file uploads.
33
+ Represented as `{ type: 'string', format: 'binary' }` in JSON Schema / OpenAPI
34
+ output. By default accepts `ActionDispatch::Http::UploadedFile`,
35
+ `Rack::Multipart::UploadedFile`, `Tempfile`, and `String`. Custom classes can
36
+ be specified via the `classes` option. DSL: `bin` / `bin!` / `bin?`.
37
+
3
38
  ## 3.0.38 (2026-02-25)
4
39
 
5
40
  * Fix namespaced schema `$ref` paths in generated JSON. For OpenAPI (swagger)
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
@@ -14,11 +14,12 @@
14
14
  6. [Array](#array)
15
15
  7. [Hash](#hash)
16
16
  8. [Object](#object)
17
- 9. [AllOf](#allOf)
18
- 10. [AnyOf](#anyOf)
19
- 11. [OneOf](#oneOf)
20
- 12. [IsNot](#isNot)
21
- 13. [Reference](#reference)
17
+ 9. [Binary](#binary)
18
+ 10. [AllOf](#allOf)
19
+ 11. [AnyOf](#anyOf)
20
+ 12. [OneOf](#oneOf)
21
+ 13. [IsNot](#isNot)
22
+ 14. [Reference](#reference)
22
23
  5. [Context](#context)
23
24
  6. [External schemas](#external-schemas)
24
25
  7. [Default options](#default-options)
@@ -223,6 +224,15 @@ transformed into various types.
223
224
  By default, blank strings are allowed and left as they are when casted (e.g.
224
225
  the string `''` is valid). If you want to disallow blank strings, set this
225
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.
226
236
 
227
237
  #### Formats
228
238
 
@@ -1290,6 +1300,45 @@ schema.validate!('foo'.html_safe) # => "foo"
1290
1300
  If you set the `strict` option to `false`, the check is done using `is_a?` instead of
1291
1301
  `instance_of?`, which also allows subclasses
1292
1302
 
1303
+ ### Binary
1304
+
1305
+ Type: `:binary`\
1306
+ DSL: `bin`
1307
+
1308
+ The binary type represents binary data fields such as file uploads. It is
1309
+ represented as `{ type: 'string', format: 'binary' }` in JSON Schema / OpenAPI
1310
+ output. At runtime, it validates that the value is an instance of one of the
1311
+ configured classes (using `is_a?`, so subclasses are accepted).
1312
+
1313
+ By default, the node accepts instances of `ActionDispatch::Http::UploadedFile`,
1314
+ `Rack::Multipart::UploadedFile`, `Tempfile`, and `String`. Missing classes (e.g.
1315
+ when not running within Rails) are silently skipped.
1316
+
1317
+ ```ruby
1318
+ schema = Schemacop::Schema3.new :binary, classes: [Tempfile, String]
1319
+
1320
+ schema.validate!(nil) # => nil
1321
+ schema.validate!(Tempfile.new('f')) # => #<Tempfile:...>
1322
+ schema.validate!('binary data') # => "binary data"
1323
+ schema.validate!(42) # => Schemacop::Exceptions::ValidationError: /: Invalid type, got type "Integer", expected "String" or "Tempfile".
1324
+ ```
1325
+
1326
+ If you want to limit the accepted classes, use the `classes` option:
1327
+
1328
+ ```ruby
1329
+ schema = Schemacop::Schema3.new :binary, classes: [Tempfile]
1330
+
1331
+ schema.validate!(nil) # => nil
1332
+ schema.validate!(Tempfile.new('f')) # => #<Tempfile:...>
1333
+ schema.validate!('foo') # => Schemacop::Exceptions::ValidationError: /: Invalid type, got type "String", expected "Tempfile".
1334
+ ```
1335
+
1336
+ #### Options
1337
+
1338
+ * `classes`
1339
+ An array of `Class` objects that should be accepted. Must not be empty. If not
1340
+ given, the default classes listed above are used.
1341
+
1293
1342
  ### AllOf
1294
1343
 
1295
1344
  Type: `:all_of`\
data/VERSION CHANGED
@@ -1 +1 @@
1
- 3.0.38
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
@@ -0,0 +1,75 @@
1
+ module Schemacop
2
+ module V3
3
+ # Node type for binary data fields such as file uploads. Represented as
4
+ # `{ type: 'string', format: 'binary' }` in JSON Schema / OpenAPI output.
5
+ #
6
+ # At runtime, validates that the value is an instance of one of the
7
+ # configured classes (using `is_a?`).
8
+ #
9
+ # Default accepted classes (resolved lazily, missing classes are silently
10
+ # skipped):
11
+ # - `ActionDispatch::Http::UploadedFile`
12
+ # - `Rack::Multipart::UploadedFile`
13
+ # - `Tempfile`
14
+ # - `String`
15
+ #
16
+ # @example Default usage (accepts common upload and binary types)
17
+ # bin! :file
18
+ #
19
+ # @example Restrict to specific classes
20
+ # bin! :file, classes: [ActionDispatch::Http::UploadedFile]
21
+ class BinaryNode < Node
22
+ # Classes that are always available (stdlib).
23
+ DEFAULT_CLASSES = [Tempfile, String].freeze
24
+
25
+ # Optional classes resolved lazily via `safe_constantize` (may not be
26
+ # available outside of Rails/Rack).
27
+ OPTIONAL_CLASS_NAMES = %w[
28
+ ActionDispatch::Http::UploadedFile
29
+ Rack::Multipart::UploadedFile
30
+ ].freeze
31
+
32
+ def self.allowed_options
33
+ super + %i[classes]
34
+ end
35
+
36
+ def as_json
37
+ process_json([], type: :string, format: :binary)
38
+ end
39
+
40
+ protected
41
+
42
+ def init
43
+ @classes = options.delete(:classes)
44
+ end
45
+
46
+ def allowed_types
47
+ resolved_classes.to_h { |c| [c, c.name] }
48
+ end
49
+
50
+ def validate_self
51
+ return unless @classes
52
+
53
+ unless @classes.is_a?(Array)
54
+ fail 'Option "classes" must be an array of classes.'
55
+ end
56
+
57
+ if @classes.empty?
58
+ fail 'Option "classes" must not be empty.'
59
+ end
60
+
61
+ @classes.each do |c|
62
+ unless c.is_a?(Class)
63
+ fail "Option \"classes\" must contain classes, got #{c.inspect}."
64
+ end
65
+ end
66
+ end
67
+
68
+ private
69
+
70
+ def resolved_classes
71
+ @resolved_classes ||= @classes || (OPTIONAL_CLASS_NAMES.map(&:safe_constantize).compact + DEFAULT_CLASSES)
72
+ end
73
+ end
74
+ end
75
+ end
@@ -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/v3.rb CHANGED
@@ -39,6 +39,7 @@ require 'schemacop/v3/numeric_node'
39
39
  require 'schemacop/v3/all_of_node'
40
40
  require 'schemacop/v3/any_of_node'
41
41
  require 'schemacop/v3/array_node'
42
+ require 'schemacop/v3/binary_node'
42
43
  require 'schemacop/v3/boolean_node'
43
44
  require 'schemacop/v3/hash_node'
44
45
  require 'schemacop/v3/integer_node'
@@ -54,6 +55,7 @@ require 'schemacop/v3/symbol_node'
54
55
  Schemacop::V3.register :all_of, :all_of, Schemacop::V3::AllOfNode
55
56
  Schemacop::V3.register :any_of, :any_of, Schemacop::V3::AnyOfNode
56
57
  Schemacop::V3.register :array, :ary, Schemacop::V3::ArrayNode
58
+ Schemacop::V3.register :binary, :bin, Schemacop::V3::BinaryNode
57
59
  Schemacop::V3.register :boolean, :boo, Schemacop::V3::BooleanNode
58
60
  Schemacop::V3.register :integer, :int, Schemacop::V3::IntegerNode
59
61
  Schemacop::V3.register :is_not, :is_not, Schemacop::V3::IsNotNode
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,20 +1,20 @@
1
1
  # -*- encoding: utf-8 -*-
2
- # stub: schemacop 3.0.38 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.38".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-02-25"
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/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/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]
11
+ s.date = "2026-05-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]
15
15
  s.rubygems_version = "3.5.18".freeze
16
16
  s.summary = "Schemacop validates ruby structures consisting of nested hashes and arrays against simple schema definitions.".freeze
17
- s.test_files = ["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/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]
17
+ s.test_files = ["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]
18
18
 
19
19
  s.specification_version = 4
20
20
 
@@ -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
@@ -0,0 +1,266 @@
1
+ require 'test_helper'
2
+
3
+ module Schemacop
4
+ module V3
5
+ class BinaryNodeTest < V3Test
6
+ def test_basic
7
+ schema :binary
8
+
9
+ assert_validation nil
10
+ assert_validation Tempfile.new('test')
11
+ assert_validation 'binary string data'
12
+
13
+ assert_json(type: :string, format: :binary)
14
+ end
15
+
16
+ def test_required
17
+ schema :binary, required: true
18
+
19
+ assert_validation Tempfile.new('test')
20
+
21
+ assert_validation nil do
22
+ error '/', 'Value must be given.'
23
+ end
24
+
25
+ assert_json(type: :string, format: :binary)
26
+ end
27
+
28
+ def test_type
29
+ schema :binary
30
+
31
+ assert_validation 'foo'
32
+ assert_validation Tempfile.new('test')
33
+
34
+ assert_validation 42 do
35
+ error '/', 'Invalid type, got type "Integer", expected "String" or "Tempfile".'
36
+ end
37
+
38
+ assert_validation true do
39
+ error '/', 'Invalid type, got type "TrueClass", expected "String" or "Tempfile".'
40
+ end
41
+
42
+ assert_validation :foo do
43
+ error '/', 'Invalid type, got type "Symbol", expected "String" or "Tempfile".'
44
+ end
45
+ end
46
+
47
+ def test_hash
48
+ schema do
49
+ bin! :attachment
50
+ end
51
+
52
+ assert_json(
53
+ type: :object,
54
+ properties: {
55
+ attachment: { type: :string, format: :binary }
56
+ },
57
+ required: %i[attachment],
58
+ additionalProperties: false
59
+ )
60
+
61
+ assert_validation attachment: Tempfile.new('test')
62
+ assert_validation attachment: 'binary data'
63
+
64
+ assert_validation({}) do
65
+ error '/attachment', 'Value must be given.'
66
+ end
67
+
68
+ assert_validation(attachment: 42) do
69
+ error '/attachment', 'Invalid type, got type "Integer", expected "String" or "Tempfile".'
70
+ end
71
+ end
72
+
73
+ def test_hash_optional
74
+ schema do
75
+ bin? :attachment
76
+ end
77
+
78
+ assert_json(
79
+ type: :object,
80
+ properties: {
81
+ attachment: { type: :string, format: :binary }
82
+ },
83
+ additionalProperties: false
84
+ )
85
+
86
+ assert_validation attachment: Tempfile.new('test')
87
+ assert_validation({})
88
+ assert_validation attachment: nil
89
+ end
90
+
91
+ def test_array
92
+ schema(:array) do
93
+ list :binary
94
+ end
95
+
96
+ assert_validation [Tempfile.new('a'), Tempfile.new('b')]
97
+ assert_json(type: :array, items: { type: :string, format: :binary })
98
+ end
99
+
100
+ def test_with_custom_classes
101
+ schema :binary, classes: [String, Integer]
102
+
103
+ assert_validation 'hello'
104
+ assert_validation 42
105
+ assert_validation nil
106
+
107
+ assert_validation :foo do
108
+ error '/', 'Invalid type, got type "Symbol", expected "Integer" or "String".'
109
+ end
110
+ end
111
+
112
+ def test_with_single_custom_class
113
+ schema :binary, classes: [Tempfile]
114
+
115
+ assert_validation Tempfile.new('test')
116
+
117
+ assert_validation 'hello' do
118
+ error '/', 'Invalid type, got type "String", expected "Tempfile".'
119
+ end
120
+ end
121
+
122
+ def test_custom_classes_uses_is_a
123
+ schema :binary, classes: [Numeric]
124
+
125
+ assert_validation 42
126
+ assert_validation 3.14
127
+ assert_validation BigDecimal('1.5')
128
+
129
+ assert_validation 'hello' do
130
+ error '/', 'Invalid type, got type "String", expected "Numeric".'
131
+ end
132
+ end
133
+
134
+ def test_default
135
+ schema :binary, default: Tempfile.new('default')
136
+
137
+ assert_validation nil
138
+ assert_validation Tempfile.new('other')
139
+ end
140
+
141
+ def test_enum
142
+ tempfile_a = Tempfile.new('a')
143
+ tempfile_b = Tempfile.new('b')
144
+ tempfile_c = Tempfile.new('c')
145
+
146
+ schema :binary, enum: [tempfile_a, tempfile_b]
147
+
148
+ assert_validation nil
149
+ assert_validation tempfile_a
150
+ assert_validation tempfile_b
151
+
152
+ assert_validation tempfile_c do
153
+ error '/', /Value not included in enum/
154
+ end
155
+ end
156
+
157
+ def test_with_generic_keywords
158
+ schema :binary, title: 'Binary schema',
159
+ description: 'Binary schema holding generic keywords',
160
+ examples: [
161
+ 'binary data'
162
+ ]
163
+
164
+ assert_json(
165
+ type: :string,
166
+ format: :binary,
167
+ title: 'Binary schema',
168
+ description: 'Binary schema holding generic keywords',
169
+ examples: [
170
+ 'binary data'
171
+ ]
172
+ )
173
+ end
174
+
175
+ def test_validate_self_classes_not_array
176
+ assert_raises_with_message Exceptions::InvalidSchemaError,
177
+ 'Option "classes" must be an array of classes.' do
178
+ schema :binary, classes: 'String'
179
+ end
180
+ end
181
+
182
+ def test_validate_self_classes_empty
183
+ assert_raises_with_message Exceptions::InvalidSchemaError,
184
+ 'Option "classes" must not be empty.' do
185
+ schema :binary, classes: []
186
+ end
187
+ end
188
+
189
+ def test_validate_self_classes_not_classes
190
+ assert_raises_with_message Exceptions::InvalidSchemaError,
191
+ 'Option "classes" must contain classes, got "String".' do
192
+ schema :binary, classes: ['String']
193
+ end
194
+ end
195
+
196
+ def test_validate_self_classes_mixed
197
+ assert_raises_with_message Exceptions::InvalidSchemaError,
198
+ 'Option "classes" must contain classes, got :symbol.' do
199
+ schema :binary, classes: [String, :symbol]
200
+ end
201
+ end
202
+
203
+ def test_validate_self_classes_with_module
204
+ assert_raises_with_message Exceptions::InvalidSchemaError,
205
+ 'Option "classes" must contain classes, got Comparable.' do
206
+ schema :binary, classes: [Comparable]
207
+ end
208
+ end
209
+
210
+ def test_cast
211
+ tempfile = Tempfile.new('test')
212
+ schema :binary
213
+
214
+ result = @schema.validate(tempfile)
215
+ assert_empty result.errors
216
+ assert_equal tempfile, result.data
217
+
218
+ result = @schema.validate(nil)
219
+ assert_empty result.errors
220
+ assert_nil result.data
221
+ end
222
+
223
+ def test_cast_default
224
+ default_file = Tempfile.new('default')
225
+ schema :binary, default: default_file
226
+
227
+ result = @schema.validate(nil)
228
+ assert_empty result.errors
229
+ assert_equal default_file, result.data
230
+ end
231
+
232
+ def test_cast_in_hash
233
+ schema do
234
+ bin? :attachment
235
+ end
236
+
237
+ tempfile = Tempfile.new('test')
238
+ result = @schema.validate(attachment: tempfile)
239
+ assert_empty result.errors
240
+ assert_equal({ 'attachment' => tempfile }, result.data)
241
+
242
+ result = @schema.validate({})
243
+ assert_empty result.errors
244
+ assert_equal({}, result.data)
245
+ end
246
+
247
+ def test_default_classes_include_string
248
+ schema :binary
249
+
250
+ assert_validation 'a plain string'
251
+ end
252
+
253
+ def test_as_json
254
+ schema :binary
255
+
256
+ assert_json(type: :string, format: :binary)
257
+ end
258
+
259
+ def test_swagger_json
260
+ schema :binary
261
+
262
+ assert_swagger_json(type: :string, format: :binary)
263
+ end
264
+ end
265
+ end
266
+ 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.38
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-02-25 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
@@ -87,6 +87,7 @@ files:
87
87
  - lib/schemacop/v3/all_of_node.rb
88
88
  - lib/schemacop/v3/any_of_node.rb
89
89
  - lib/schemacop/v3/array_node.rb
90
+ - lib/schemacop/v3/binary_node.rb
90
91
  - lib/schemacop/v3/boolean_node.rb
91
92
  - lib/schemacop/v3/combination_node.rb
92
93
  - lib/schemacop/v3/context.rb
@@ -132,6 +133,7 @@ files:
132
133
  - test/unit/schemacop/v3/all_of_node_test.rb
133
134
  - test/unit/schemacop/v3/any_of_node_test.rb
134
135
  - test/unit/schemacop/v3/array_node_test.rb
136
+ - test/unit/schemacop/v3/binary_node_test.rb
135
137
  - test/unit/schemacop/v3/boolean_node_test.rb
136
138
  - test/unit/schemacop/v3/global_context_test.rb
137
139
  - test/unit/schemacop/v3/hash_node_test.rb
@@ -193,6 +195,7 @@ test_files:
193
195
  - test/unit/schemacop/v3/all_of_node_test.rb
194
196
  - test/unit/schemacop/v3/any_of_node_test.rb
195
197
  - test/unit/schemacop/v3/array_node_test.rb
198
+ - test/unit/schemacop/v3/binary_node_test.rb
196
199
  - test/unit/schemacop/v3/boolean_node_test.rb
197
200
  - test/unit/schemacop/v3/global_context_test.rb
198
201
  - test/unit/schemacop/v3/hash_node_test.rb