schemacop 3.0.35 → 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 +4 -4
- data/CHANGELOG.md +27 -0
- data/LICENSE +1 -1
- data/README.md +1 -1
- data/README_V2.md +13 -13
- data/README_V3.md +161 -23
- data/VERSION +1 -1
- data/lib/schemacop/railtie.rb +7 -0
- data/lib/schemacop/v3/hash_node.rb +100 -7
- data/schemacop.gemspec +3 -3
- data/test/unit/schemacop/v3/array_node_test.rb +1 -1
- data/test/unit/schemacop/v3/global_context_test.rb +98 -0
- data/test/unit/schemacop/v3/hash_node_test.rb +1 -1
- data/test/unit/schemacop/v3/reference_node_test.rb +398 -0
- metadata +3 -8
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 56a24074bd3792c20ee7e2f0bb1a8c3f4aa6d411a9a4c8010c9a52f0e828e6a3
|
|
4
|
+
data.tar.gz: ca4a752be493fe721122a77c5fb6844098c968dddf860fdcb0e33ef9a98880c3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: da3e3672206befe40e6487677c6c879192a3fdaf450e3621566be86dd1ccc9c487571a70cf7f1933294d5de9cd86094791e22cd659109a97baa432917138f296
|
|
7
|
+
data.tar.gz: 69d79cf5ad0702b80f930a9227f9279c2f758b3cb51965596145d82ec735b0fe8bb6648f219b6c1858d82f8a339ccdc388db71c3767573a074562a063eefcade
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
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
|
+
|
|
12
|
+
## 3.0.36 (2026-01-05)
|
|
13
|
+
|
|
14
|
+
* Fix `v3_default_options` not being applied when schemas are eager loaded in
|
|
15
|
+
production mode. The Railtie now explicitly loads `config/schemacop.rb`
|
|
16
|
+
during initialization, before eager loading schemas. This ensures that
|
|
17
|
+
default options like `cast_str` are applied to all schemas.
|
|
18
|
+
|
|
19
|
+
For Rails applications, place your Schemacop configuration in
|
|
20
|
+
`config/schemacop.rb` (not in the `initializers/` subdirectory) to benefit
|
|
21
|
+
from this fix. Configuration in `config/initializers/schemacop.rb` will
|
|
22
|
+
continue to work in development mode but will not be applied to eager-loaded
|
|
23
|
+
schemas in production mode (same behavior as in 3.0.35 and earlier).
|
|
24
|
+
|
|
25
|
+
Internal reference: `#144209`.
|
|
26
|
+
|
|
27
|
+
* Fix test compatibility with Ruby 3.2+ by updating JSON parse error regex
|
|
28
|
+
patterns to match the new error message format introduced in Ruby 3.2.
|
|
29
|
+
|
|
3
30
|
## 3.0.35 (2025-07-31)
|
|
4
31
|
|
|
5
32
|
* Add option `max_precision` to `number` nodes for Schemacop V3 schemas to validate
|
data/LICENSE
CHANGED
data/README.md
CHANGED
data/README_V2.md
CHANGED
|
@@ -61,7 +61,7 @@ gem 'schemacop'
|
|
|
61
61
|
## Basics
|
|
62
62
|
|
|
63
63
|
Since there is no explicit typing in Ruby, it can be hard to make sure that a
|
|
64
|
-
method is
|
|
64
|
+
method is receiving exactly the right kind of data it needs. The idea of this
|
|
65
65
|
gem is to define a schema at boot time that will validate the data being passed
|
|
66
66
|
around at runtime. Those two steps look as follows:
|
|
67
67
|
|
|
@@ -84,7 +84,7 @@ my_schema.validate!(
|
|
|
84
84
|
`validate!` will fail if the data given to it does not match what was specified
|
|
85
85
|
in the schema.
|
|
86
86
|
|
|
87
|
-
### Type
|
|
87
|
+
### Type Lines vs. Field Lines
|
|
88
88
|
|
|
89
89
|
Schemacop uses a DSL (domain-specific language) to let you describe your
|
|
90
90
|
schemas. We distinguish between two kinds of identifiers:
|
|
@@ -309,7 +309,7 @@ end
|
|
|
309
309
|
You might find the notation cumbersome, and you'd be right to say so. Luckily
|
|
310
310
|
there are plenty of short forms available which we will see below.
|
|
311
311
|
|
|
312
|
-
#### Handling
|
|
312
|
+
#### Handling Hashes with Indifferent Access
|
|
313
313
|
|
|
314
314
|
Schemacop has special handling for objects of the class
|
|
315
315
|
`ActiveSupport::HashWithIndifferentAccess`: You may specify the keys as symbols
|
|
@@ -403,12 +403,12 @@ The following types are supported by Schemacop by default:
|
|
|
403
403
|
All types support the options `if` and `check` (see the section about Type Lines
|
|
404
404
|
above).
|
|
405
405
|
|
|
406
|
-
## Short
|
|
406
|
+
## Short Forms
|
|
407
407
|
|
|
408
408
|
For convenience, the following short forms may be used (and combined if
|
|
409
409
|
possible).
|
|
410
410
|
|
|
411
|
-
### Passing a
|
|
411
|
+
### Passing a Type to a Field Line or Schema
|
|
412
412
|
|
|
413
413
|
Instead of adding a Type Line in the block of a Field Line, you can omit `do
|
|
414
414
|
type ... end` and directly write the type after the key of the field.
|
|
@@ -444,7 +444,7 @@ Schema.new(:string, min: 2, max: 5)
|
|
|
444
444
|
This means that the data given to the schema must be a String that is between 2
|
|
445
445
|
and 5 characters long.
|
|
446
446
|
|
|
447
|
-
### Passing
|
|
447
|
+
### Passing Multiple Types at Once
|
|
448
448
|
|
|
449
449
|
You can specify several types at once by putting them in an array.
|
|
450
450
|
|
|
@@ -522,7 +522,7 @@ Note that this does not allow you to specify any options for the hash itself.
|
|
|
522
522
|
You still need to specify `:hash` as a type if you want to pass any options to
|
|
523
523
|
the hash (i.e. a `default`).
|
|
524
524
|
|
|
525
|
-
### Shortform for
|
|
525
|
+
### Shortform for Subtypes
|
|
526
526
|
|
|
527
527
|
In case of nested arrays, you can group all Type Lines to a single one.
|
|
528
528
|
|
|
@@ -638,14 +638,14 @@ Schema.new do
|
|
|
638
638
|
end
|
|
639
639
|
```
|
|
640
640
|
|
|
641
|
-
### Required
|
|
641
|
+
### Required Data Points
|
|
642
642
|
|
|
643
643
|
Note that any *required* validation is done before applying the defaults. If you
|
|
644
644
|
specify a `req` field, it must always be given, no matter if you have specified
|
|
645
645
|
a default or not. Therefore, specifying `req` fields do not make sense in
|
|
646
646
|
conjunction with defaults, as the default is always ignored.
|
|
647
647
|
|
|
648
|
-
## Type
|
|
648
|
+
## Type Casting
|
|
649
649
|
|
|
650
650
|
Starting from version 2.4.0, Schemacop allows you to specify type castings that
|
|
651
651
|
can alter the validated data. Consider the following:
|
|
@@ -663,7 +663,7 @@ Note that Schemacop never modifies the data you pass to it. If you want to
|
|
|
663
663
|
benefit from Schemacop-applied castings, you need to access the cloned, modified
|
|
664
664
|
data returned by `validate` or `validate!`.
|
|
665
665
|
|
|
666
|
-
### Specifying
|
|
666
|
+
### Specifying Type Castings
|
|
667
667
|
|
|
668
668
|
Type castings can be specified using two forms: Either as a hash or as an array.
|
|
669
669
|
While using an array only allows you to specify the supported source types to be
|
|
@@ -689,7 +689,7 @@ Schema.new do
|
|
|
689
689
|
end
|
|
690
690
|
```
|
|
691
691
|
|
|
692
|
-
### Built-in
|
|
692
|
+
### Built-in Casters
|
|
693
693
|
|
|
694
694
|
Schemacop comes with the following casters:
|
|
695
695
|
|
|
@@ -702,7 +702,7 @@ an error if casted to an Integer. When casting float values and strings
|
|
|
702
702
|
containing float values to integers, the decimal places will be discarded
|
|
703
703
|
however.
|
|
704
704
|
|
|
705
|
-
### Execution
|
|
705
|
+
### Execution Order
|
|
706
706
|
|
|
707
707
|
The casting is done *before* the options `if` and `check` are evaluated.
|
|
708
708
|
Example:
|
|
@@ -748,7 +748,7 @@ Schemacop will throw one of the following checked exceptions:
|
|
|
748
748
|
This exception is thrown when the given data does not comply with the given
|
|
749
749
|
schema definition.
|
|
750
750
|
|
|
751
|
-
## Known
|
|
751
|
+
## Known Limitations
|
|
752
752
|
|
|
753
753
|
* Schemacop does not yet allow cyclic structures with infinite depth.
|
|
754
754
|
|
data/README_V3.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Schemacop
|
|
1
|
+
# Schemacop Schema V3
|
|
2
2
|
|
|
3
3
|
## Table of Contents
|
|
4
4
|
|
|
@@ -282,11 +282,11 @@ transformed into various types.
|
|
|
282
282
|
#### Custom Formats
|
|
283
283
|
|
|
284
284
|
You can also implement your custom formats or override the behavior of the
|
|
285
|
-
standard formats.
|
|
286
|
-
|
|
285
|
+
standard formats. For Rails applications, this can be done in
|
|
286
|
+
`config/schemacop.rb`:
|
|
287
287
|
|
|
288
288
|
```ruby
|
|
289
|
-
# config/
|
|
289
|
+
# config/schemacop.rb
|
|
290
290
|
Schemacop.register_string_formatter(
|
|
291
291
|
:character_array, # Formatter name
|
|
292
292
|
pattern: /^[a-zA-Z](,[a-zA-Z])*/, # Regex pattern for validation
|
|
@@ -424,7 +424,7 @@ and subclasses are valid:
|
|
|
424
424
|
* `Rational`
|
|
425
425
|
* `BigDecimal`
|
|
426
426
|
|
|
427
|
-
As some subclasses of `Numeric`, such as `Complex` don't support all required
|
|
427
|
+
As some subclasses of `Numeric`, such as `Complex` don't support all required operations,
|
|
428
428
|
only the above list is supported. If you need support for additional number classes, please
|
|
429
429
|
contact the Gem maintainers.
|
|
430
430
|
|
|
@@ -507,7 +507,7 @@ and validating a blank string will return `nil`.
|
|
|
507
507
|
If you need a value, use the `required` option:
|
|
508
508
|
|
|
509
509
|
```ruby
|
|
510
|
-
schema = Schemacop::Schema3.new(:number, cast_str: true, minimum: 0.0, maximum: (50r), multiple_of: BigDecimal('0.5'),
|
|
510
|
+
schema = Schemacop::Schema3.new(:number, cast_str: true, minimum: 0.0, maximum: (50r), multiple_of: BigDecimal('0.5'), required: true)
|
|
511
511
|
schema.validate!('42.5') # => 42.5
|
|
512
512
|
schema.validate!(nil) # => Schemacop::Exceptions::ValidationError: /: Value must be given.
|
|
513
513
|
schema.validate!('') # => Schemacop::Exceptions::ValidationError: /: Value must be given.
|
|
@@ -636,7 +636,7 @@ It consists of one or multiple values, which can be validated using arbitrary no
|
|
|
636
636
|
must contain to pass the validation.
|
|
637
637
|
|
|
638
638
|
* `unique_items`
|
|
639
|
-
This option specifies
|
|
639
|
+
This option specifies whether the items in the array must all be distinct from
|
|
640
640
|
each other, or if there may be duplicate values. By default, this is false,
|
|
641
641
|
i.e. duplicate values are allowed
|
|
642
642
|
|
|
@@ -704,13 +704,13 @@ schema.validate!([1, 2, 3]) # => Schemacop::Exceptions::ValidationError: /: At l
|
|
|
704
704
|
schema.validate!([1, 3, 5]) # => [1, 3, 5]
|
|
705
705
|
```
|
|
706
706
|
|
|
707
|
-
#### Specifying
|
|
707
|
+
#### Specifying Properties
|
|
708
708
|
|
|
709
709
|
Array nodes support a block in which you can specify the required array contents.
|
|
710
710
|
The array nodes support either list validation, or tuple validation, depending on
|
|
711
711
|
how you specify your array contents.
|
|
712
712
|
|
|
713
|
-
##### List
|
|
713
|
+
##### List Validation
|
|
714
714
|
|
|
715
715
|
List validation validates a sequence of arbitrary length where each item matches
|
|
716
716
|
the same schema. Unless you specify a `min_items` count on the array node, an
|
|
@@ -759,7 +759,7 @@ end
|
|
|
759
759
|
# => Schemacop::Exceptions::InvalidSchemaError: You can only use "list" once.
|
|
760
760
|
```
|
|
761
761
|
|
|
762
|
-
##### Tuple
|
|
762
|
+
##### Tuple Validation
|
|
763
763
|
|
|
764
764
|
On the other hand, tuple validation validates a sequence of fixed length, where
|
|
765
765
|
each item has its own schema that it has to match. Here, the order of the items
|
|
@@ -850,8 +850,8 @@ Using the options `filter` and `reject`, arrays can be filtered. Filtering
|
|
|
850
850
|
happens before validation. Both options behave in the same way, with the only
|
|
851
851
|
difference being that `filter` uses a inclusive approach and `reject` an
|
|
852
852
|
exclusive (see [filter](https://apidock.com/ruby/Array/filter) and
|
|
853
|
-
[reject](https://apidock.com/ruby/Array/reject
|
|
854
|
-
in a similar
|
|
853
|
+
[reject](https://apidock.com/ruby/Array/reject) in the Ruby API, as they behave
|
|
854
|
+
in a similar manner).
|
|
855
855
|
|
|
856
856
|
You can either pass a Symbol which specifies the name of the method that is
|
|
857
857
|
called on each array item:
|
|
@@ -951,11 +951,11 @@ It consists of key-value-pairs that can be validated using arbitrary nodes.
|
|
|
951
951
|
|
|
952
952
|
Defaults to `false`.
|
|
953
953
|
|
|
954
|
-
#### Specifying
|
|
954
|
+
#### Specifying Properties
|
|
955
955
|
|
|
956
956
|
Hash nodes support a block in which you can specify the required hash contents.
|
|
957
957
|
|
|
958
|
-
##### Standard
|
|
958
|
+
##### Standard Properties
|
|
959
959
|
|
|
960
960
|
It supports all type nodes, but requires the suffix `?` or `!` for each
|
|
961
961
|
property, which specifies whether a property is required (`!`) or optional
|
|
@@ -1078,7 +1078,7 @@ schema.validate!({foo: 1}) # => {"foo"=>1}
|
|
|
1078
1078
|
schema.validate!({foo: 'bar'}) # => {"foo"=>"bar"}
|
|
1079
1079
|
```
|
|
1080
1080
|
|
|
1081
|
-
##### Pattern
|
|
1081
|
+
##### Pattern Properties
|
|
1082
1082
|
|
|
1083
1083
|
In addition to symbols, property keys can also be a regular expression. Here,
|
|
1084
1084
|
you may only use the optional `?` suffix for the property. This allows any
|
|
@@ -1098,7 +1098,7 @@ schema.validate!({id_foo: 1, id_bar: 2}) # => {"id_foo"=>1, "id_bar"=>2}
|
|
|
1098
1098
|
schema.validate!({foo: 3}) # => Schemacop::Exceptions::ValidationError: /: Obsolete property "foo".
|
|
1099
1099
|
```
|
|
1100
1100
|
|
|
1101
|
-
##### Additional
|
|
1101
|
+
##### Additional Properties & Property Names
|
|
1102
1102
|
|
|
1103
1103
|
In addition to standard properties, you can allow the hash to contain
|
|
1104
1104
|
additional, unspecified properties. By default, this is turned off if you have
|
|
@@ -1158,7 +1158,7 @@ schema.validate!({foo: :bar}) # => Schemacop::Exceptions::ValidationError:
|
|
|
1158
1158
|
schema.validate!({Foo: :bar}) # => Schemacop::Exceptions::ValidationError: /: Property name :Foo does not match "^[a-z]+$". /Foo: Invalid type, got type "Symbol", expected "array".
|
|
1159
1159
|
```
|
|
1160
1160
|
|
|
1161
|
-
##### Ignoring
|
|
1161
|
+
##### Ignoring Obsolete Properties
|
|
1162
1162
|
|
|
1163
1163
|
By enabling `ignore_obsolete_properties`, you can filter out any unspecified params,
|
|
1164
1164
|
while still passing validation:
|
|
@@ -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
|
|
@@ -1559,7 +1684,7 @@ to use other data in the second context than in the first.
|
|
|
1559
1684
|
|
|
1560
1685
|
## External schemas
|
|
1561
1686
|
|
|
1562
|
-
Finally, Schemacop features the possibility to specify schemas in
|
|
1687
|
+
Finally, Schemacop features the possibility to specify schemas in separate
|
|
1563
1688
|
files. This is especially useful is you have schemas in your application which
|
|
1564
1689
|
are used multiple times throughout the application.
|
|
1565
1690
|
|
|
@@ -1582,7 +1707,7 @@ Where:
|
|
|
1582
1707
|
* context schemas: Defined in the current context using `context.schema`
|
|
1583
1708
|
* global schemas: Defined in a ruby file in the load path
|
|
1584
1709
|
|
|
1585
|
-
### External
|
|
1710
|
+
### External Schemas in Rails Applications
|
|
1586
1711
|
|
|
1587
1712
|
In Rails applications, your schemas are automatically eager-loaded from the load
|
|
1588
1713
|
path `'app/schemas'` when your application is started, unless your application
|
|
@@ -1633,7 +1758,7 @@ schema.validate!({usr: {first_name: 'Joe', last_name: 'Doe', groups: [{name: 'fo
|
|
|
1633
1758
|
# => {"usr"=>{"first_name"=>"Joe", "last_name"=>"Doe", "groups"=>[{"name"=>"foo"}, {"name"=>"bar"}]}}
|
|
1634
1759
|
```
|
|
1635
1760
|
|
|
1636
|
-
### External
|
|
1761
|
+
### External Schemas in Non-Rails Applications
|
|
1637
1762
|
|
|
1638
1763
|
Usage in non-Rails applications is the same as with usage in Rails applications,
|
|
1639
1764
|
however you might need to eager load the schemas yourself:
|
|
@@ -1651,11 +1776,24 @@ to eager-load them on start of your application / script.
|
|
|
1651
1776
|
Using the setting `Schemacop.v3_default_options`, you can specify a hash
|
|
1652
1777
|
containing default options that will be used for every schemacop node (options
|
|
1653
1778
|
not supported by a particular node are automatically ignored). Options passed
|
|
1654
|
-
directly to a node still take precedence.
|
|
1655
|
-
|
|
1779
|
+
directly to a node still take precedence.
|
|
1780
|
+
|
|
1781
|
+
### Rails applications
|
|
1782
|
+
|
|
1783
|
+
For Rails applications, configure default options in `config/schemacop.rb`
|
|
1784
|
+
(note: not in the `initializers/` subdirectory). This ensures that options are
|
|
1785
|
+
applied before schemas are eager loaded in production mode:
|
|
1786
|
+
|
|
1787
|
+
```ruby
|
|
1788
|
+
# config/schemacop.rb
|
|
1789
|
+
Schemacop.v3_default_options = { cast_str: true }.freeze
|
|
1790
|
+
```
|
|
1791
|
+
|
|
1792
|
+
### Non-Rails applications
|
|
1793
|
+
|
|
1794
|
+
For non-Rails applications, set the options before loading any schemas:
|
|
1656
1795
|
|
|
1657
1796
|
```ruby
|
|
1658
|
-
# config/initializers/schemacop.rb
|
|
1659
1797
|
Schemacop.v3_default_options = { cast_str: true }.freeze
|
|
1660
1798
|
|
|
1661
1799
|
# Example schema: As cast_str is enabled in the default options, strings will
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.0.
|
|
1
|
+
3.0.37
|
data/lib/schemacop/railtie.rb
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
module Schemacop
|
|
2
2
|
class Railtie < Rails::Railtie
|
|
3
3
|
initializer 'schemacop' do
|
|
4
|
+
# Load configuration
|
|
5
|
+
config_path = Rails.root.join('config', 'schemacop.rb')
|
|
6
|
+
|
|
7
|
+
if config_path.exist?
|
|
8
|
+
load config_path
|
|
9
|
+
end
|
|
10
|
+
|
|
4
11
|
# Load global schemas
|
|
5
12
|
unless Rails.env.development?
|
|
6
13
|
V3::GlobalContext.eager_load!
|
|
@@ -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
|
-
|
|
26
|
-
|
|
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
|
-
|
|
228
|
-
additional_properties = data_hash.reject { |k, _v|
|
|
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.
|
|
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.
|
|
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 = "
|
|
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]
|
|
@@ -988,7 +988,7 @@ module Schemacop
|
|
|
988
988
|
end
|
|
989
989
|
|
|
990
990
|
assert_validation('{42]') do
|
|
991
|
-
error '/', /JSON parse error: "(\d+: )?unexpected token at '{42]'"\./
|
|
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
|
|
@@ -161,6 +161,104 @@ module Schemacop
|
|
|
161
161
|
GlobalContext.instance.schema_for('user')
|
|
162
162
|
end
|
|
163
163
|
end
|
|
164
|
+
|
|
165
|
+
def test_v3_default_options_with_lazy_load
|
|
166
|
+
dir = Dir.mktmpdir
|
|
167
|
+
Schemacop.load_paths << dir
|
|
168
|
+
File.write(File.join(dir, 'test_schema.rb'), %(schema :integer))
|
|
169
|
+
|
|
170
|
+
Schemacop.v3_default_options = { cast_str: true }.freeze
|
|
171
|
+
|
|
172
|
+
schema = GlobalContext.instance.schema_for('test_schema')
|
|
173
|
+
result = schema.validate('42')
|
|
174
|
+
|
|
175
|
+
assert result.valid?, "Expected string '42' to be cast to integer 42, but got errors: #{result.errors}"
|
|
176
|
+
assert_equal 42, result.data
|
|
177
|
+
ensure
|
|
178
|
+
Schemacop.v3_default_options = {}
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def test_v3_default_options_with_eager_load
|
|
182
|
+
dir = Dir.mktmpdir
|
|
183
|
+
Schemacop.load_paths << dir
|
|
184
|
+
File.write(File.join(dir, 'test_schema.rb'), %(schema :integer))
|
|
185
|
+
|
|
186
|
+
Schemacop.v3_default_options = { cast_str: true }.freeze
|
|
187
|
+
|
|
188
|
+
GlobalContext.instance.eager_load!
|
|
189
|
+
schema = GlobalContext.instance.schema_for('test_schema')
|
|
190
|
+
result = schema.validate('42')
|
|
191
|
+
|
|
192
|
+
assert result.valid?, "Expected string '42' to be cast to integer 42, but got errors: #{result.errors}"
|
|
193
|
+
assert_equal 42, result.data
|
|
194
|
+
ensure
|
|
195
|
+
Schemacop.v3_default_options = {}
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def test_v3_default_options_with_nested_schema
|
|
199
|
+
dir = Dir.mktmpdir
|
|
200
|
+
Schemacop.load_paths << dir
|
|
201
|
+
File.write(File.join(dir, 'form_schema.rb'), <<~SCHEMA)
|
|
202
|
+
schema do
|
|
203
|
+
int! :age
|
|
204
|
+
str! :name
|
|
205
|
+
end
|
|
206
|
+
SCHEMA
|
|
207
|
+
|
|
208
|
+
Schemacop.v3_default_options = { cast_str: true }.freeze
|
|
209
|
+
|
|
210
|
+
schema = GlobalContext.instance.schema_for('form_schema')
|
|
211
|
+
result = schema.validate({ age: '25', name: 'John' })
|
|
212
|
+
|
|
213
|
+
assert result.valid?, "Expected string '25' to be cast to integer 25, but got errors: #{result.errors}"
|
|
214
|
+
assert_equal 25, result.data[:age]
|
|
215
|
+
assert_equal 'John', result.data[:name]
|
|
216
|
+
ensure
|
|
217
|
+
Schemacop.v3_default_options = {}
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def test_v3_default_options_set_before_eager_load
|
|
221
|
+
dir = Dir.mktmpdir
|
|
222
|
+
Schemacop.load_paths << dir
|
|
223
|
+
File.write(File.join(dir, 'test_schema.rb'), %(schema :integer))
|
|
224
|
+
|
|
225
|
+
# Set default options BEFORE eager loading (simulates production mode)
|
|
226
|
+
Schemacop.v3_default_options = { cast_str: true }.freeze
|
|
227
|
+
|
|
228
|
+
GlobalContext.instance.eager_load!
|
|
229
|
+
schema = GlobalContext.instance.schema_for('test_schema')
|
|
230
|
+
result = schema.validate('42')
|
|
231
|
+
|
|
232
|
+
assert result.valid?, "Expected string '42' to be cast to integer 42, but got errors: #{result.errors}"
|
|
233
|
+
assert_equal 42, result.data
|
|
234
|
+
ensure
|
|
235
|
+
Schemacop.v3_default_options = {}
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def test_v3_default_options_set_after_eager_load
|
|
239
|
+
dir = Dir.mktmpdir
|
|
240
|
+
Schemacop.load_paths << dir
|
|
241
|
+
File.write(File.join(dir, 'test_schema.rb'), %(schema :integer))
|
|
242
|
+
|
|
243
|
+
# Eager load schemas BEFORE setting default options
|
|
244
|
+
GlobalContext.instance.eager_load!
|
|
245
|
+
|
|
246
|
+
Schemacop.v3_default_options = { cast_str: true }.freeze
|
|
247
|
+
|
|
248
|
+
schema = GlobalContext.instance.schema_for('test_schema')
|
|
249
|
+
result = schema.validate('42')
|
|
250
|
+
|
|
251
|
+
# NOTE: Setting v3_default_options AFTER eager loading does not work
|
|
252
|
+
# because schemas are already cached. In Rails applications, the
|
|
253
|
+
# Railtie explicitly loads 'config/schemacop.rb' before eager loading,
|
|
254
|
+
# ensuring options are set before schemas are loaded.
|
|
255
|
+
#
|
|
256
|
+
# This test verifies that the options don't retroactively apply.
|
|
257
|
+
refute result.valid?, 'Options set after eager_load! should not affect already-loaded schemas'
|
|
258
|
+
assert_equal({ [] => ['Invalid type, got type "String", expected "integer".'] }, result.errors)
|
|
259
|
+
ensure
|
|
260
|
+
Schemacop.v3_default_options = {}
|
|
261
|
+
end
|
|
164
262
|
end
|
|
165
263
|
end
|
|
166
264
|
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]'"\./
|
|
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
|
|
@@ -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,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: schemacop
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.0.
|
|
4
|
+
version: 3.0.37
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sitrox
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 2026-02-24 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: activesupport
|
|
@@ -38,8 +37,6 @@ dependencies:
|
|
|
38
37
|
- - ">="
|
|
39
38
|
- !ruby/object:Gem::Version
|
|
40
39
|
version: 0.0.4
|
|
41
|
-
description:
|
|
42
|
-
email:
|
|
43
40
|
executables: []
|
|
44
41
|
extensions: []
|
|
45
42
|
extra_rdoc_files: []
|
|
@@ -151,7 +148,6 @@ homepage: https://github.com/sitrox/schemacop
|
|
|
151
148
|
licenses:
|
|
152
149
|
- MIT
|
|
153
150
|
metadata: {}
|
|
154
|
-
post_install_message:
|
|
155
151
|
rdoc_options: []
|
|
156
152
|
require_paths:
|
|
157
153
|
- lib
|
|
@@ -166,8 +162,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
166
162
|
- !ruby/object:Gem::Version
|
|
167
163
|
version: '0'
|
|
168
164
|
requirements: []
|
|
169
|
-
rubygems_version:
|
|
170
|
-
signing_key:
|
|
165
|
+
rubygems_version: 4.0.2
|
|
171
166
|
specification_version: 4
|
|
172
167
|
summary: Schemacop validates ruby structures consisting of nested hashes and arrays
|
|
173
168
|
against simple schema definitions.
|