schemacop 3.0.36 → 3.0.37

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: 9494f9a5a921664fae74b99ecba0830775d160ae91250465b4b88ba1d1fdf970
4
- data.tar.gz: 2263f93913342b872ac56c52ddd65dd6c34f34711d3624c52c6ffc2d5a9a516a
3
+ metadata.gz: 56a24074bd3792c20ee7e2f0bb1a8c3f4aa6d411a9a4c8010c9a52f0e828e6a3
4
+ data.tar.gz: ca4a752be493fe721122a77c5fb6844098c968dddf860fdcb0e33ef9a98880c3
5
5
  SHA512:
6
- metadata.gz: 3712232ba9d867f723ee462d80db875d6e0adbd6cd0579d798908df58f9fd9b5448389e2cc9829fe979bdcd972efa209a4b2ae6a27766d8d7884fa3a7189c4ab
7
- data.tar.gz: fd5c87120fb0c2d1a1e13122013b08d902d37a32154d1196385ad6f2fb5fec28b54c0170f129533c7ce116ccadd13c7c653832f2102cfc4c63da62257170ee3a
6
+ metadata.gz: da3e3672206befe40e6487677c6c879192a3fdaf450e3621566be86dd1ccc9c487571a70cf7f1933294d5de9cd86094791e22cd659109a97baa432917138f296
7
+ data.tar.gz: 69d79cf5ad0702b80f930a9227f9279c2f758b3cb51965596145d82ec735b0fe8bb6648f219b6c1858d82f8a339ccdc388db71c3767573a074562a063eefcade
data/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Change log
2
2
 
3
+ ## 3.0.37 (2026-02-24)
4
+
5
+ * Add inline ref support for hash nodes via `ref! nil, :SchemaName`. This
6
+ unpacks the referenced schema's properties directly into the parent hash
7
+ instead of nesting them under a key. Produces `allOf` with `$ref` in the
8
+ JSON/Swagger output.
9
+
10
+ Internal reference: `#146962`.
11
+
3
12
  ## 3.0.36 (2026-01-05)
4
13
 
5
14
  * Fix `v3_default_options` not being applied when schemas are eager loaded in
data/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright © 2016 - 2024 Sitrox
3
+ Copyright © 2016 - 2026 Sitrox
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -122,4 +122,4 @@ To run tests:
122
122
 
123
123
  ## Copyright
124
124
 
125
- Copyright © 2016 - 2025 Sitrox. See `LICENSE` for further details.
125
+ Copyright © 2016 - 2026 Sitrox. See `LICENSE` for further details.
data/README_V3.md CHANGED
@@ -1494,6 +1494,131 @@ schema.validate!([{first_name: 'Joe', last_name: 'Doe'}]) # => [{"first_name"=>"
1494
1494
  schema.validate!([id: 42, first_name: 'Joe']) # => Schemacop::Exceptions::ValidationError: /[0]/last_name: Value must be given. /[0]: Obsolete property "id".
1495
1495
  ```
1496
1496
 
1497
+ #### Inline References
1498
+
1499
+ By passing `nil` as the name, you can "inline" a referenced schema into the
1500
+ parent hash. Instead of nesting the referenced properties under a key, they are
1501
+ unpacked directly into the parent:
1502
+
1503
+ ```ruby
1504
+ schema = Schemacop::Schema3.new :hash do
1505
+ scm :BasicInfo do
1506
+ int! :id
1507
+ str! :name
1508
+ end
1509
+
1510
+ ref! nil, :BasicInfo
1511
+ str! :extra
1512
+ end
1513
+
1514
+ # Properties from the referenced schema are validated at the top level
1515
+ schema.validate!({id: 1, name: 'John', extra: 'info'})
1516
+ # => {"id"=>1, "name"=>"John", "extra"=>"info"}
1517
+
1518
+ # Required properties from the ref are enforced
1519
+ schema.validate!({extra: 'info'})
1520
+ # => Schemacop::Exceptions::ValidationError: /id: Value must be given. /name: Value must be given.
1521
+
1522
+ # Unknown properties are still rejected
1523
+ schema.validate!({id: 1, name: 'John', extra: 'info', unknown: 'value'})
1524
+ # => Schemacop::Exceptions::ValidationError: /: Obsolete property "unknown".
1525
+ ```
1526
+
1527
+ Casting works as expected — values from the inline ref are cast according to the
1528
+ referenced schema's types:
1529
+
1530
+ ```ruby
1531
+ schema = Schemacop::Schema3.new :hash do
1532
+ scm :BasicInfo do
1533
+ str! :born_at, format: :date
1534
+ str! :name
1535
+ end
1536
+
1537
+ ref! nil, :BasicInfo
1538
+ str! :extra
1539
+ end
1540
+
1541
+ result = schema.validate!({born_at: '1990-01-13', name: 'John', extra: 'info'})
1542
+ result[:born_at] # => Date<"Sat, 13 Jan 1990">
1543
+ ```
1544
+
1545
+ You can also use multiple inline refs in the same hash:
1546
+
1547
+ ```ruby
1548
+ schema = Schemacop::Schema3.new :hash do
1549
+ scm :BasicInfo do
1550
+ int! :id
1551
+ str! :name
1552
+ end
1553
+
1554
+ scm :Timestamps do
1555
+ str! :created_at, format: :date
1556
+ end
1557
+
1558
+ ref! nil, :BasicInfo
1559
+ ref! nil, :Timestamps
1560
+ str! :extra
1561
+ end
1562
+
1563
+ schema.validate!({id: 1, name: 'John', created_at: '2024-01-01', extra: 'info'})
1564
+ # => {"id"=>1, "name"=>"John", "created_at"=>Mon, 01 Jan 2024, "extra"=>"info"}
1565
+ ```
1566
+
1567
+ If a direct property has the same name as one from the inline ref, the direct
1568
+ property takes precedence:
1569
+
1570
+ ```ruby
1571
+ schema = Schemacop::Schema3.new :hash do
1572
+ scm :BasicInfo do
1573
+ str! :name
1574
+ end
1575
+
1576
+ ref! nil, :BasicInfo
1577
+ int! :name # Direct property takes precedence
1578
+ end
1579
+
1580
+ schema.validate!({name: 42}) # => {"name"=>42}
1581
+ schema.validate!({name: 'John'})
1582
+ # => Schemacop::Exceptions::ValidationError: /name: Invalid type, got type "String", expected "integer".
1583
+ ```
1584
+
1585
+ In the JSON / Swagger output, inline refs produce an `allOf` array containing
1586
+ the `$ref` entries alongside the hash's own properties:
1587
+
1588
+ ```ruby
1589
+ schema = Schemacop::Schema3.new :hash do
1590
+ scm :BasicInfo do
1591
+ int! :id
1592
+ str! :name
1593
+ end
1594
+
1595
+ ref! nil, :BasicInfo
1596
+ str! :extra
1597
+ end
1598
+
1599
+ schema.as_json
1600
+ # => {
1601
+ # "allOf" => [
1602
+ # { "$ref" => "#/definitions/BasicInfo" },
1603
+ # {
1604
+ # "type" => "object",
1605
+ # "properties" => { "extra" => { "type" => "string" } },
1606
+ # "additionalProperties" => false,
1607
+ # "required" => ["extra"]
1608
+ # }
1609
+ # ],
1610
+ # "definitions" => {
1611
+ # "BasicInfo" => {
1612
+ # "properties" => { "id" => { "type" => "integer" }, "name" => { "type" => "string" } },
1613
+ # "additionalProperties" => false,
1614
+ # "required" => ["id", "name"],
1615
+ # "type" => "object"
1616
+ # }
1617
+ # },
1618
+ # "type" => "object"
1619
+ # }
1620
+ ```
1621
+
1497
1622
  ## Context
1498
1623
 
1499
1624
  Schemacop also features the concept of a `Context`. You can define schemas in a
data/VERSION CHANGED
@@ -1 +1 @@
1
- 3.0.36
1
+ 3.0.37
@@ -12,6 +12,7 @@ module Schemacop
12
12
  supports_children(name: true)
13
13
 
14
14
  attr_reader :properties
15
+ attr_reader :inline_refs
15
16
 
16
17
  def self.allowed_options
17
18
  super + ATTRIBUTES - %i[dependencies] + %i[additional_properties ignore_obsolete_properties parse_json]
@@ -22,8 +23,13 @@ module Schemacop
22
23
  end
23
24
 
24
25
  def add_child(node)
25
- unless node.name
26
- fail Exceptions::InvalidSchemaError, 'Child nodes must have a name.'
26
+ if node.name.nil?
27
+ if node.is_a?(ReferenceNode)
28
+ @inline_refs << node
29
+ return
30
+ else
31
+ fail Exceptions::InvalidSchemaError, 'Child nodes must have a name.'
32
+ end
27
33
  end
28
34
 
29
35
  @properties[node.name] = node
@@ -54,6 +60,10 @@ module Schemacop
54
60
  end
55
61
  end
56
62
 
63
+ if @inline_refs.any?
64
+ return as_json_with_inline_refs(properties, pattern_properties)
65
+ end
66
+
57
67
  json = {}
58
68
  json[:properties] = properties.values.map { |p| [p.name, p.as_json] }.to_h if properties.any?
59
69
  json[:patternProperties] = pattern_properties.values.map { |p| [V3.sanitize_exp(p.name), p.as_json] }.to_h if pattern_properties.any?
@@ -122,8 +132,29 @@ module Schemacop
122
132
  end
123
133
  end
124
134
 
135
+ # Validate inline ref properties #
136
+ inline_ref_property_names = Set.new
137
+
138
+ @inline_refs.each do |inline_ref|
139
+ target = inline_ref.target
140
+ next unless target
141
+
142
+ target.properties.each_value do |prop|
143
+ next if prop.name.is_a?(Regexp)
144
+ next if @properties.key?(prop.name)
145
+ next if inline_ref_property_names.include?(prop.name)
146
+
147
+ inline_ref_property_names << prop.name
148
+
149
+ result.in_path(prop.name) do
150
+ result.error "Key #{prop.name} must be given." if prop.require_key? && !data_hash.include?(prop.name)
151
+ prop._validate(data_hash[prop.name], result: result)
152
+ end
153
+ end
154
+ end
155
+
125
156
  # Validate additional properties #
126
- specified_properties = @properties.keys.to_set
157
+ specified_properties = @properties.keys.to_set + inline_ref_property_names
127
158
  additional_properties = data_hash.reject { |k, _v| specified_properties.include?(k.to_s) }
128
159
 
129
160
  property_patterns = {}
@@ -173,7 +204,7 @@ module Schemacop
173
204
  end
174
205
 
175
206
  def children
176
- @properties.values
207
+ @properties.values + @inline_refs
177
208
  end
178
209
 
179
210
  def cast(data)
@@ -208,8 +239,36 @@ module Schemacop
208
239
  end
209
240
  end
210
241
 
242
+ # Cast inline ref properties
243
+ inline_ref_property_names = Set.new
244
+
245
+ @inline_refs.each do |inline_ref|
246
+ target = inline_ref.target
247
+ next unless target
248
+
249
+ target.properties.each_value do |prop|
250
+ next if prop.name.is_a?(Regexp)
251
+ next if @properties.key?(prop.name)
252
+ next if inline_ref_property_names.include?(prop.name)
253
+
254
+ inline_ref_property_names << prop.name
255
+
256
+ prop_name = prop.as&.to_s || prop.name
257
+
258
+ casted_data = prop.cast(data_hash[prop.name])
259
+
260
+ if !casted_data.nil? || data_hash.include?(prop.name)
261
+ result[prop_name] = casted_data
262
+ end
263
+
264
+ if result[prop_name].nil? && !data_hash.include?(prop.name)
265
+ result.delete(prop_name)
266
+ end
267
+ end
268
+ end
269
+
211
270
  # Handle regex properties
212
- specified_properties = @properties.keys.to_set
271
+ specified_properties = @properties.keys.to_set + inline_ref_property_names
213
272
  additional_properties = data_hash.reject { |k, _v| specified_properties.include?(k.to_s.to_sym) }
214
273
 
215
274
  if additional_properties.any? && property_patterns.any?
@@ -224,8 +283,8 @@ module Schemacop
224
283
  if options[:additional_properties].is_a?(TrueClass)
225
284
  result = data_hash.merge(result)
226
285
  elsif options[:additional_properties].is_a?(Node)
227
- specified_properties = @properties.keys.to_set
228
- additional_properties = data_hash.reject { |k, _v| specified_properties.include?(k.to_s.to_sym) }
286
+ add_prop_specified = @properties.keys.to_set + inline_ref_property_names
287
+ additional_properties = data_hash.reject { |k, _v| add_prop_specified.include?(k.to_s.to_sym) }
229
288
  if additional_properties.any?
230
289
  additional_properties_result = {}
231
290
  additional_properties.each do |key, value|
@@ -240,8 +299,42 @@ module Schemacop
240
299
 
241
300
  protected
242
301
 
302
+ def as_json_with_inline_refs(properties, pattern_properties)
303
+ all_of = []
304
+
305
+ # Add each inline ref
306
+ @inline_refs.each do |inline_ref|
307
+ all_of << inline_ref.as_json
308
+ end
309
+
310
+ # Add own properties schema if any direct properties exist
311
+ if properties.any? || pattern_properties.any?
312
+ own_schema = {}
313
+ 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?
316
+
317
+ if options[:additional_properties].is_a?(TrueClass)
318
+ own_schema[:additionalProperties] = true
319
+ elsif options[:additional_properties].is_a?(Node)
320
+ own_schema[:additionalProperties] = options[:additional_properties].as_json
321
+ else
322
+ own_schema[:additionalProperties] = false
323
+ end
324
+
325
+ required_properties = @properties.values.select(&:required?).map(&:name)
326
+ own_schema[:required] = required_properties if required_properties.any?
327
+
328
+ all_of << own_schema
329
+ end
330
+
331
+ json = { allOf: all_of }
332
+ return process_json(ATTRIBUTES, json)
333
+ end
334
+
243
335
  def init
244
336
  @properties = {}
337
+ @inline_refs = []
245
338
  @options[:type] = :object
246
339
  unless @options[:additional_properties].nil? || @options[:additional_properties].is_a?(TrueClass) || @options[:additional_properties].is_a?(FalseClass)
247
340
  fail Schemacop::Exceptions::InvalidSchemaError, 'Option "additional_properties" must be a boolean value'
data/schemacop.gemspec CHANGED
@@ -1,14 +1,14 @@
1
1
  # -*- encoding: utf-8 -*-
2
- # stub: schemacop 3.0.36 ruby lib
2
+ # stub: schemacop 3.0.37 ruby lib
3
3
 
4
4
  Gem::Specification.new do |s|
5
5
  s.name = "schemacop".freeze
6
- s.version = "3.0.36".freeze
6
+ s.version = "3.0.37".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-01-05"
11
+ s.date = "2026-02-24"
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/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]
13
13
  s.homepage = "https://github.com/sitrox/schemacop".freeze
14
14
  s.licenses = ["MIT".freeze]
@@ -362,6 +362,404 @@ module Schemacop
362
362
 
363
363
  assert_cast({ person: { born_at: '1990-01-13' } }, { person: { born_at: Date.new(1990, 1, 13) } }.with_indifferent_access)
364
364
  end
365
+
366
+ # --- Inline ref tests ---
367
+
368
+ def test_inline_ref_schema_builds_without_error
369
+ schema do
370
+ scm :BasicInfo do
371
+ int! :id
372
+ str! :name
373
+ end
374
+
375
+ ref! nil, :BasicInfo
376
+ end
377
+
378
+ assert_equal [], @schema.root.properties.keys
379
+ assert_equal 1, @schema.root.inline_refs.size
380
+ end
381
+
382
+ def test_inline_ref_validation
383
+ schema do
384
+ scm :BasicInfo do
385
+ int! :id
386
+ str! :name
387
+ end
388
+
389
+ ref! nil, :BasicInfo
390
+ str! :extra
391
+ end
392
+
393
+ assert_validation(id: 1, name: 'John', extra: 'info')
394
+ end
395
+
396
+ def test_inline_ref_validation_errors
397
+ schema do
398
+ scm :BasicInfo do
399
+ int! :id
400
+ str! :name
401
+ end
402
+
403
+ ref! nil, :BasicInfo
404
+ str! :extra
405
+ end
406
+
407
+ assert_validation(extra: 'info') do
408
+ error '/id', 'Value must be given.'
409
+ error '/name', 'Value must be given.'
410
+ end
411
+ end
412
+
413
+ def test_inline_ref_validation_type_errors
414
+ schema do
415
+ scm :BasicInfo do
416
+ int! :id
417
+ str! :name
418
+ end
419
+
420
+ ref! nil, :BasicInfo
421
+ end
422
+
423
+ assert_validation(id: 'not_an_int', name: 42) do
424
+ error '/id', 'Invalid type, got type "String", expected "integer".'
425
+ error '/name', 'Invalid type, got type "Integer", expected "string".'
426
+ end
427
+ end
428
+
429
+ def test_inline_ref_validation_obsolete_properties
430
+ schema do
431
+ scm :BasicInfo do
432
+ int! :id
433
+ str! :name
434
+ end
435
+
436
+ ref! nil, :BasicInfo
437
+ end
438
+
439
+ # Properties from inline ref are accepted
440
+ assert_validation(id: 1, name: 'John')
441
+
442
+ # Unknown properties are still rejected
443
+ assert_validation(id: 1, name: 'John', unknown: 'value') do
444
+ error '/', 'Obsolete property "unknown".'
445
+ end
446
+ end
447
+
448
+ def test_inline_ref_cast
449
+ schema do
450
+ scm :BasicInfo do
451
+ str! :born_at, format: :date
452
+ str! :name
453
+ end
454
+
455
+ ref! nil, :BasicInfo
456
+ str! :extra
457
+ end
458
+
459
+ assert_cast(
460
+ { born_at: '1990-01-13', name: 'John', extra: 'info' },
461
+ { born_at: Date.new(1990, 1, 13), name: 'John', extra: 'info' }.with_indifferent_access
462
+ )
463
+ end
464
+
465
+ def test_inline_ref_with_defaults
466
+ schema do
467
+ scm :BasicInfo do
468
+ str? :name, default: 'Anonymous'
469
+ end
470
+
471
+ ref! nil, :BasicInfo
472
+ end
473
+
474
+ assert_cast({}, { name: 'Anonymous' }.with_indifferent_access)
475
+ end
476
+
477
+ def test_inline_ref_with_optional_properties
478
+ schema do
479
+ scm :BasicInfo do
480
+ int! :id
481
+ str? :nickname
482
+ end
483
+
484
+ ref! nil, :BasicInfo
485
+ end
486
+
487
+ assert_validation(id: 1)
488
+ assert_validation(id: 1, nickname: 'Johnny')
489
+ end
490
+
491
+ def test_inline_ref_as_json
492
+ schema do
493
+ scm :BasicInfo do
494
+ int! :id
495
+ str! :name
496
+ end
497
+
498
+ ref! nil, :BasicInfo
499
+ str! :extra
500
+ end
501
+
502
+ assert_json({
503
+ definitions: {
504
+ BasicInfo: {
505
+ properties: {
506
+ id: { type: :integer },
507
+ name: { type: :string }
508
+ },
509
+ additionalProperties: false,
510
+ required: %w[id name],
511
+ type: :object
512
+ }
513
+ },
514
+ type: :object,
515
+ allOf: [
516
+ { '$ref' => '#/definitions/BasicInfo' },
517
+ {
518
+ type: :object,
519
+ properties: {
520
+ extra: { type: :string }
521
+ },
522
+ additionalProperties: false,
523
+ required: %w[extra]
524
+ }
525
+ ]
526
+ })
527
+ end
528
+
529
+ def test_inline_ref_as_json_no_own_properties
530
+ schema do
531
+ scm :BasicInfo do
532
+ int! :id
533
+ str! :name
534
+ end
535
+
536
+ ref! nil, :BasicInfo
537
+ end
538
+
539
+ assert_json({
540
+ definitions: {
541
+ BasicInfo: {
542
+ properties: {
543
+ id: { type: :integer },
544
+ name: { type: :string }
545
+ },
546
+ additionalProperties: false,
547
+ required: %w[id name],
548
+ type: :object
549
+ }
550
+ },
551
+ type: :object,
552
+ allOf: [
553
+ { '$ref' => '#/definitions/BasicInfo' }
554
+ ]
555
+ })
556
+ end
557
+
558
+ def test_inline_ref_swagger_json
559
+ schema do
560
+ scm :BasicInfo do
561
+ int! :id
562
+ str! :name
563
+ end
564
+
565
+ ref! nil, :BasicInfo
566
+ str! :extra
567
+ end
568
+
569
+ assert_swagger_json({
570
+ type: :object,
571
+ allOf: [
572
+ { '$ref' => '#/components/schemas/BasicInfo' },
573
+ {
574
+ type: :object,
575
+ properties: {
576
+ extra: { type: :string }
577
+ },
578
+ additionalProperties: false,
579
+ required: %w[extra]
580
+ }
581
+ ]
582
+ })
583
+ end
584
+
585
+ def test_inline_ref_multiple
586
+ schema do
587
+ scm :BasicInfo do
588
+ int! :id
589
+ str! :name
590
+ end
591
+
592
+ scm :Timestamps do
593
+ str! :created_at, format: :date
594
+ end
595
+
596
+ ref! nil, :BasicInfo
597
+ ref! nil, :Timestamps
598
+ str! :extra
599
+ end
600
+
601
+ # Validation
602
+ assert_validation(id: 1, name: 'John', created_at: '2024-01-01', extra: 'info')
603
+ assert_validation(extra: 'info') do
604
+ error '/id', 'Value must be given.'
605
+ error '/name', 'Value must be given.'
606
+ error '/created_at', 'Value must be given.'
607
+ end
608
+
609
+ # Casting
610
+ assert_cast(
611
+ { id: 1, name: 'John', created_at: '2024-01-01', extra: 'info' },
612
+ { id: 1, name: 'John', created_at: Date.new(2024, 1, 1), extra: 'info' }.with_indifferent_access
613
+ )
614
+
615
+ # JSON
616
+ assert_json({
617
+ definitions: {
618
+ BasicInfo: {
619
+ properties: {
620
+ id: { type: :integer },
621
+ name: { type: :string }
622
+ },
623
+ additionalProperties: false,
624
+ required: %w[id name],
625
+ type: :object
626
+ },
627
+ Timestamps: {
628
+ properties: {
629
+ created_at: { type: :string, format: :date }
630
+ },
631
+ additionalProperties: false,
632
+ required: %w[created_at],
633
+ type: :object
634
+ }
635
+ },
636
+ type: :object,
637
+ allOf: [
638
+ { '$ref' => '#/definitions/BasicInfo' },
639
+ { '$ref' => '#/definitions/Timestamps' },
640
+ {
641
+ type: :object,
642
+ properties: {
643
+ extra: { type: :string }
644
+ },
645
+ additionalProperties: false,
646
+ required: %w[extra]
647
+ }
648
+ ]
649
+ })
650
+ end
651
+
652
+ def test_inline_ref_with_external_schema
653
+ context = Context.new
654
+
655
+ context.schema :BasicInfo do
656
+ int! :id
657
+ str! :name
658
+ end
659
+
660
+ with_context context do
661
+ schema do
662
+ ref! nil, :BasicInfo
663
+ str! :extra
664
+ end
665
+
666
+ assert_validation(id: 1, name: 'John', extra: 'info')
667
+ assert_validation(extra: 'info') do
668
+ error '/id', 'Value must be given.'
669
+ error '/name', 'Value must be given.'
670
+ end
671
+ end
672
+ end
673
+
674
+ def test_inline_ref_used_external_schemas
675
+ context = Context.new
676
+
677
+ context.schema :BasicInfo do
678
+ int! :id
679
+ str! :name
680
+ end
681
+
682
+ with_context context do
683
+ schema do
684
+ ref! nil, :BasicInfo
685
+ str! :extra
686
+ end
687
+
688
+ assert_equal %i[BasicInfo], @schema.root.used_external_schemas
689
+ end
690
+ end
691
+
692
+ def test_inline_ref_with_additional_properties_true
693
+ schema do
694
+ scm :BasicInfo do
695
+ int! :id
696
+ str! :name
697
+ end
698
+
699
+ ref! nil, :BasicInfo
700
+ str! :extra
701
+ end
702
+
703
+ # Without additional_properties: true, unknown props are rejected
704
+ assert_validation(id: 1, name: 'John', extra: 'info', unknown: 'value') do
705
+ error '/', 'Obsolete property "unknown".'
706
+ end
707
+ end
708
+
709
+ def test_inline_ref_property_name_collision
710
+ schema do
711
+ scm :BasicInfo do
712
+ str! :name
713
+ str? :description
714
+ end
715
+
716
+ ref! nil, :BasicInfo
717
+ int! :name # Direct property takes precedence
718
+ end
719
+
720
+ # Direct property (int!) takes precedence — string should fail
721
+ assert_validation(name: 42)
722
+ assert_validation(name: 'John') do
723
+ error '/name', 'Invalid type, got type "String", expected "integer".'
724
+ end
725
+
726
+ # Optional description from inline ref still works
727
+ assert_validation(name: 42, description: 'A description')
728
+ end
729
+
730
+ def test_inline_ref_collision_between_inline_refs
731
+ schema do
732
+ scm :BasicInfo do
733
+ str! :name
734
+ str? :description
735
+ end
736
+
737
+ scm :ExtraInfo do
738
+ int! :name # Clashes with BasicInfo's :name
739
+ str! :extra
740
+ end
741
+
742
+ ref! nil, :BasicInfo # First inline ref wins for :name
743
+ ref! nil, :ExtraInfo
744
+ end
745
+
746
+ # First inline ref wins: :name is validated as str! (from BasicInfo)
747
+ assert_validation(name: 'John', extra: 'info')
748
+ assert_validation(name: 42, extra: 'info') do
749
+ error '/name', 'Invalid type, got type "Integer", expected "string".'
750
+ end
751
+
752
+ # Properties unique to second inline ref still work
753
+ assert_validation(name: 'John') do
754
+ error '/extra', 'Value must be given.'
755
+ end
756
+
757
+ # Casting uses first inline ref's definition for :name
758
+ assert_cast(
759
+ { name: 'John', description: 'A desc', extra: 'info' },
760
+ { name: 'John', description: 'A desc', extra: 'info' }.with_indifferent_access
761
+ )
762
+ end
365
763
  end
366
764
  end
367
765
  end
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.36
4
+ version: 3.0.37
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sitrox
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-01-05 00:00:00.000000000 Z
10
+ date: 2026-02-24 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: activesupport