schemacop 3.0.39 → 3.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/ruby.yml +1 -1
- data/.rubocop.yml +4 -1
- data/CHANGELOG.md +87 -0
- data/Gemfile +1 -1
- data/README.md +8 -7
- data/README_V3.md +38 -5
- data/VERSION +1 -1
- data/lib/schemacop/schema3.rb +2 -2
- data/lib/schemacop/v2/node.rb +1 -1
- data/lib/schemacop/v2/validator/string_validator.rb +1 -1
- data/lib/schemacop/v3/any_of_node.rb +13 -0
- data/lib/schemacop/v3/array_node.rb +2 -3
- data/lib/schemacop/v3/global_context.rb +1 -1
- data/lib/schemacop/v3/hash_node.rb +23 -21
- data/lib/schemacop/v3/node.rb +5 -0
- data/lib/schemacop/v3/numeric_node.rb +2 -2
- data/lib/schemacop/v3/object_node.rb +1 -1
- data/lib/schemacop/v3/one_of_node.rb +16 -3
- data/lib/schemacop/v3/result.rb +14 -1
- data/lib/schemacop/v3/string_node.rb +32 -4
- data/lib/schemacop/v3.rb +32 -0
- data/lib/schemacop.rb +12 -9
- data/schemacop.gemspec +3 -3
- data/test/lib/test_helper.rb +7 -0
- data/test/unit/schemacop/v2/casting_test.rb +1 -1
- data/test/unit/schemacop/v2/custom_check_test.rb +2 -2
- data/test/unit/schemacop/v2/custom_if_test.rb +2 -2
- data/test/unit/schemacop/v3/any_of_node_test.rb +24 -0
- data/test/unit/schemacop/v3/array_node_test.rb +60 -1
- data/test/unit/schemacop/v3/binary_node_test.rb +8 -0
- data/test/unit/schemacop/v3/hash_node_test.rb +143 -1
- data/test/unit/schemacop/v3/integer_node_test.rb +15 -1
- data/test/unit/schemacop/v3/is_not_node_test.rb +12 -0
- data/test/unit/schemacop/v3/number_node_test.rb +1 -1
- data/test/unit/schemacop/v3/object_node_test.rb +8 -0
- data/test/unit/schemacop/v3/one_of_node_test.rb +61 -0
- data/test/unit/schemacop/v3/string_node_test.rb +250 -2
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f790c910d2c32accea93c9f4e67b85c80ababfbaca2b64930d7e380a65125d9e
|
|
4
|
+
data.tar.gz: ae3f22c58697112408efde125190a0cd8518199ceb8b47b65094e289d9e8769d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b3f20de721d5e94f4bbb1504292c8ecd7136ae8468e8fde9d0885e9503aee4c6f950e7bd888ea50067d0dbf40c3dd0e36ab12d2ee674143a28405bd3c70bfe2f
|
|
7
|
+
data.tar.gz: efd82ce7677ee0c3d5031af98997840cd6cf38a3aa2337e68ead04e383add88e32d29f15d08ad0739e47691c48299f329c547677abd6aca1118a29fe6308a4e4
|
data/.github/workflows/ruby.yml
CHANGED
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,92 @@
|
|
|
1
1
|
# Change log
|
|
2
2
|
|
|
3
|
+
## 3.1.1 (2026-09-08)
|
|
4
|
+
|
|
5
|
+
* Fix crashes when validating strings whose encoding does not fit their
|
|
6
|
+
contents or the schema:
|
|
7
|
+
|
|
8
|
+
* A string with an invalid byte sequence is reported as such and no longer
|
|
9
|
+
handed to `allow_blank`, `pattern` and `format`, which decode it and
|
|
10
|
+
therefore raised an `ArgumentError`. The checks that do not decode the
|
|
11
|
+
string (`min_length`, `max_length`, `encoding` and `enum`) are still
|
|
12
|
+
reported.
|
|
13
|
+
|
|
14
|
+
* A string that cannot be compared with the `pattern` or with the pattern of
|
|
15
|
+
the given `format` because their encodings are incompatible (e.g. a UTF-16
|
|
16
|
+
string) is reported as not matching it, rather than raising an
|
|
17
|
+
`Encoding::CompatibilityError` or a `RegexpError`. The same holds for
|
|
18
|
+
`allow_blank`, `dep` dependencies and `treat_blank_as_nil`, which treat
|
|
19
|
+
such a string as blank only if it is empty.
|
|
20
|
+
|
|
21
|
+
* Hash keys and values with an invalid byte sequence no longer raise an
|
|
22
|
+
`ArgumentError` or an `EncodingError` when used together with
|
|
23
|
+
`property_names`, pattern properties, `ignore_obsolete_properties` or `dep`
|
|
24
|
+
dependencies. Such a key matches none of the patterns.
|
|
25
|
+
|
|
26
|
+
* A key with an invalid byte sequence in the path of a validation error no
|
|
27
|
+
longer makes `Result#messages`, `Result#exception_message` and `validate!`
|
|
28
|
+
raise an `ArgumentError`. The invalid bytes are replaced in the path of the
|
|
29
|
+
message.
|
|
30
|
+
|
|
31
|
+
* The `encoding` error of a string is now reported before its `pattern` and
|
|
32
|
+
`format` errors, rather than after them.
|
|
33
|
+
|
|
34
|
+
Internal reference: `#153682`.
|
|
35
|
+
|
|
36
|
+
* Anchor the patterns of the built-in string formats with `\A` and `\z` instead
|
|
37
|
+
of `^` and `$`, which match at the start and the end of every line in Ruby. A
|
|
38
|
+
multi-line string passed the validation as soon as one of its lines matched
|
|
39
|
+
and was then handed to the cast handler: `"32\ntest"` passed the `integer`
|
|
40
|
+
format and raised an `ArgumentError`, `"true\n"` passed the `boolean` format
|
|
41
|
+
and was cast to `false`. A value with a trailing newline is rejected as well
|
|
42
|
+
now, where it used to pass and be cast. Affects the formats `date`,
|
|
43
|
+
`date-time`, `time`, `mailbox`, `boolean`, `integer`, `number` and
|
|
44
|
+
`integer-list`.
|
|
45
|
+
|
|
46
|
+
Internal reference: `#153681`.
|
|
47
|
+
|
|
48
|
+
* Require `tempfile`, which the `bin` node needs. Requiring the gem outside of
|
|
49
|
+
Rails failed with a `NameError` for `Tempfile`.
|
|
50
|
+
|
|
51
|
+
* Fix `ignore_obsolete_properties` ignoring an allow list given as strings
|
|
52
|
+
(e.g. `ignore_obsolete_properties: %w[foo]`), which reported the listed
|
|
53
|
+
properties as obsolete. Strings and symbols are now treated alike.
|
|
54
|
+
|
|
55
|
+
* Fix a `NoMethodError` when casting a hash that declares pattern properties
|
|
56
|
+
and contains a key matching none of them.
|
|
57
|
+
|
|
58
|
+
* Fix casting of hash properties that are specified in the schema: in a hash
|
|
59
|
+
that also declares pattern properties or additional properties, they were
|
|
60
|
+
casted a second time as such, which could cast them with the wrong node and
|
|
61
|
+
added the original key to the result next to the one given via `as`.
|
|
62
|
+
|
|
63
|
+
## 3.1.0 (2026-05-09)
|
|
64
|
+
|
|
65
|
+
* Add option `encoding` to `str` node for Schemacop V3 schemas to validate that
|
|
66
|
+
a string has one of the specified encodings. Accepts a single encoding name or
|
|
67
|
+
an array of encoding names (e.g. `encoding: 'UTF-8'` or
|
|
68
|
+
`encoding: %w[UTF-8 US-ASCII]`).
|
|
69
|
+
|
|
70
|
+
Internal reference: `#143271`.
|
|
71
|
+
|
|
72
|
+
* All strings are now validated for valid encoding (via `valid_encoding?`),
|
|
73
|
+
regardless of whether the `encoding` option is set. Strings with invalid byte
|
|
74
|
+
sequences for their declared encoding will now produce a validation error.
|
|
75
|
+
|
|
76
|
+
Internal reference: `#143271`.
|
|
77
|
+
|
|
78
|
+
* Update RuboCop from 1.24.1 to 1.69.2.
|
|
79
|
+
|
|
80
|
+
* Drop support for all EOL Rubies (2.6.2, 2.7.1, 3.0.1, 3.1.0)
|
|
81
|
+
|
|
82
|
+
* Fix `cast_str` default option causing incorrect casting and validation errors
|
|
83
|
+
when used with combination nodes and array tuples. Numeric-looking strings
|
|
84
|
+
(e.g. `"1"`) were incorrectly cast to integers in schemas where a string type
|
|
85
|
+
was also valid. With this fix, `cast_str` only activates when the value cannot
|
|
86
|
+
be matched natively by another sibling schema.
|
|
87
|
+
|
|
88
|
+
Internal reference: `#149255`.
|
|
89
|
+
|
|
3
90
|
## 3.0.39 (2026-03-26)
|
|
4
91
|
|
|
5
92
|
* Add `BinaryNode` for validating binary data fields such as file uploads.
|
data/Gemfile
CHANGED
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
@@ -213,8 +213,17 @@ transformed into various types.
|
|
|
213
213
|
* `pattern`
|
|
214
214
|
Defines a (ruby) regex pattern the value will be matched against. Must be either
|
|
215
215
|
a string which should not be enclosed in `/` characters, or a Ruby Regexp.
|
|
216
|
-
The pattern should generally
|
|
217
|
-
the
|
|
216
|
+
The pattern should generally be anchored so as to evaluate the entire string.
|
|
217
|
+
In Ruby, `^` and `$` match at the start and the end of every *line*, so
|
|
218
|
+
`/^[0-9]+$/` accepts `"32\nfoo"`; `\A` and `\z` match only at the start and
|
|
219
|
+
the end of the string.
|
|
220
|
+
|
|
221
|
+
Patterns are exported to the JSON schema verbatim, and JSON Schema uses the
|
|
222
|
+
ECMA-262 dialect, which knows no `\A` and `\z` (it reads them as the literal
|
|
223
|
+
characters `A` and `z`) and where `^` and `$` already match the whole string
|
|
224
|
+
only. A pattern can therefore be correct on one side or the other, but not on
|
|
225
|
+
both: use `\A` and `\z` for schemas that are validated in Ruby, and `^` and
|
|
226
|
+
`$` for schemas that are exported and validated elsewhere.
|
|
218
227
|
* `format`
|
|
219
228
|
The `format` option allows for basic semantic validation on certain kinds of
|
|
220
229
|
string values that are commonly used. See section *formats* for more
|
|
@@ -224,6 +233,23 @@ transformed into various types.
|
|
|
224
233
|
By default, blank strings are allowed and left as they are when casted (e.g.
|
|
225
234
|
the string `''` is valid). If you want to disallow blank strings, set this
|
|
226
235
|
option to `false`.
|
|
236
|
+
* `encoding`
|
|
237
|
+
Validates the encoding of the string. Accepts a single encoding name or an
|
|
238
|
+
array of encoding names (e.g. `encoding: 'UTF-8'` or
|
|
239
|
+
`encoding: %w[UTF-8 US-ASCII]`). See `Encoding.name_list` for all available
|
|
240
|
+
encoding names.
|
|
241
|
+
|
|
242
|
+
Note that regardless of the `encoding` option, all strings are validated for
|
|
243
|
+
valid encoding using `valid_encoding?`. Strings with invalid byte sequences for
|
|
244
|
+
their declared encoding will always produce a validation error. The `pattern`
|
|
245
|
+
and `format` of such a string are not checked, as matching a pattern requires
|
|
246
|
+
decoding it; the checks that do not decode the string (`min_length`,
|
|
247
|
+
`max_length`, `encoding` and `enum`) are still reported.
|
|
248
|
+
|
|
249
|
+
A string whose encoding is valid but cannot be compared with the `pattern` or
|
|
250
|
+
with the pattern of the given `format` (e.g. a UTF-16 string) is reported as not
|
|
251
|
+
matching it, rather than raising an error. Note that `min_length` and
|
|
252
|
+
`max_length` count bytes for such an encoding, as it has no characters.
|
|
227
253
|
|
|
228
254
|
#### Formats
|
|
229
255
|
|
|
@@ -286,12 +312,16 @@ You can also implement your custom formats or override the behavior of the
|
|
|
286
312
|
standard formats. For Rails applications, this can be done in
|
|
287
313
|
`config/schemacop.rb`:
|
|
288
314
|
|
|
315
|
+
Anchor the pattern with `\A` and `\z`. In Ruby, `^` and `$` match at the start
|
|
316
|
+
and the end of every line, so a multi-line value would pass the validation and
|
|
317
|
+
reach your handler.
|
|
318
|
+
|
|
289
319
|
```ruby
|
|
290
320
|
# config/schemacop.rb
|
|
291
321
|
Schemacop.register_string_formatter(
|
|
292
|
-
:character_array,
|
|
293
|
-
pattern:
|
|
294
|
-
handler: ->(value) { value.split(',') }
|
|
322
|
+
:character_array, # Formatter name
|
|
323
|
+
pattern: /\A[a-zA-Z](,[a-zA-Z])*\z/, # Regex pattern for validation
|
|
324
|
+
handler: ->(value) { value.split(',') } # Casting callback
|
|
295
325
|
)
|
|
296
326
|
|
|
297
327
|
# In your schema
|
|
@@ -927,6 +957,9 @@ It consists of key-value-pairs that can be validated using arbitrary nodes.
|
|
|
927
957
|
This option allows to specify a regexp pattern (as string) which validates the
|
|
928
958
|
keys of any properties that are not specified in the hash. This option only
|
|
929
959
|
makes sense if `additional_properties` is enabled. See below for more information.
|
|
960
|
+
The pattern is anchored the same way as the `pattern` option of the string
|
|
961
|
+
node, i.e. a pattern using `^` and `$` accepts a key that matches on any of
|
|
962
|
+
its lines.
|
|
930
963
|
|
|
931
964
|
* `min_properties`
|
|
932
965
|
Specifies the (inclusive) minimum number of properties a hash must contain.
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.
|
|
1
|
+
3.1.1
|
data/lib/schemacop/schema3.rb
CHANGED
data/lib/schemacop/v2/node.rb
CHANGED
|
@@ -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
|
|
@@ -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
|
-
|
|
172
|
-
result << item.cast(value_item)
|
|
171
|
+
result << items[index].cast(value_item)
|
|
173
172
|
end
|
|
174
173
|
else
|
|
175
174
|
result << value_item
|
|
@@ -97,7 +97,7 @@ module Schemacop
|
|
|
97
97
|
fail "Schema #{path.inspect} does not define any schema."
|
|
98
98
|
when 1
|
|
99
99
|
if @schemas.include?(virtual_path)
|
|
100
|
-
fail "Schema #{virtual_path.to_s.inspect} is defined in both load paths "\
|
|
100
|
+
fail "Schema #{virtual_path.to_s.inspect} is defined in both load paths " \
|
|
101
101
|
"#{@load_paths_by_schemas[virtual_path].inspect} and #{load_path.inspect}."
|
|
102
102
|
end
|
|
103
103
|
|
|
@@ -65,8 +65,8 @@ module Schemacop
|
|
|
65
65
|
end
|
|
66
66
|
|
|
67
67
|
json = {}
|
|
68
|
-
json[:properties] = properties.values.
|
|
69
|
-
json[:patternProperties] = pattern_properties.values.
|
|
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
|
|
@@ -168,21 +168,26 @@ module Schemacop
|
|
|
168
168
|
property_names = options[:property_names]
|
|
169
169
|
property_names = Regexp.compile(property_names) if property_names
|
|
170
170
|
|
|
171
|
+
# `name.to_sym` raises an EncodingError on a key with an invalid byte
|
|
172
|
+
# sequence, so the allow list is compared as strings.
|
|
173
|
+
if options[:ignore_obsolete_properties].is_a?(Enumerable)
|
|
174
|
+
obsolete_allow_list = options[:ignore_obsolete_properties].to_set(&:to_s)
|
|
175
|
+
end
|
|
176
|
+
|
|
171
177
|
additional_properties.each do |name, additional_property|
|
|
172
|
-
if property_names && !
|
|
178
|
+
if property_names && !V3.match?(property_names, name.to_s)
|
|
173
179
|
result.error "Property name #{name.inspect} does not match #{options[:property_names].inspect}."
|
|
174
180
|
end
|
|
175
181
|
|
|
176
182
|
if options[:additional_properties].is_a?(TrueClass)
|
|
177
183
|
next
|
|
178
184
|
elsif options[:additional_properties].is_a?(FalseClass) || options[:additional_properties].blank?
|
|
179
|
-
match = property_patterns.keys.find { |p|
|
|
185
|
+
match = property_patterns.keys.find { |p| V3.match?(p, name.to_s) }
|
|
180
186
|
if match
|
|
181
187
|
result.in_path(name) do
|
|
182
188
|
property_patterns[match]._validate(additional_property, result: result)
|
|
183
189
|
end
|
|
184
|
-
elsif (
|
|
185
|
-
options[:ignore_obsolete_properties].exclude?(name.to_sym)) ||
|
|
190
|
+
elsif obsolete_allow_list&.exclude?(name.to_s) ||
|
|
186
191
|
!options[:ignore_obsolete_properties]
|
|
187
192
|
result.error "Obsolete property #{name.to_s.inspect}."
|
|
188
193
|
end
|
|
@@ -196,7 +201,7 @@ module Schemacop
|
|
|
196
201
|
# Validate dependencies #
|
|
197
202
|
options[:dependencies]&.each do |source, targets|
|
|
198
203
|
targets.each do |target|
|
|
199
|
-
if data_hash[source]
|
|
204
|
+
if !V3.blank?(data_hash[source]) && V3.blank?(data_hash[target])
|
|
200
205
|
result.error "Missing property #{target.to_s.inspect} because #{source.to_s.inspect} is given."
|
|
201
206
|
end
|
|
202
207
|
end
|
|
@@ -268,23 +273,24 @@ module Schemacop
|
|
|
268
273
|
end
|
|
269
274
|
|
|
270
275
|
# Handle regex properties
|
|
276
|
+
# Property names are strings, see Node#init.
|
|
271
277
|
specified_properties = @properties.keys.to_set + inline_ref_property_names
|
|
272
|
-
additional_properties = data_hash.reject { |k, _v| specified_properties.include?(k.to_s
|
|
278
|
+
additional_properties = data_hash.reject { |k, _v| specified_properties.include?(k.to_s) }
|
|
273
279
|
|
|
274
280
|
if additional_properties.any? && property_patterns.any?
|
|
275
281
|
additional_properties.each do |name, additional_property|
|
|
276
|
-
match_key = property_patterns.keys.find { |p|
|
|
277
|
-
|
|
278
|
-
|
|
282
|
+
match_key = property_patterns.keys.find { |p| V3.match?(p, name.to_s) }
|
|
283
|
+
next unless match_key
|
|
284
|
+
|
|
285
|
+
result[name] = property_patterns[match_key].cast(additional_property)
|
|
279
286
|
end
|
|
280
287
|
end
|
|
281
288
|
|
|
282
289
|
# Handle additional properties
|
|
283
290
|
if options[:additional_properties].is_a?(TrueClass)
|
|
284
|
-
result = data_hash.merge(result)
|
|
291
|
+
result = data_hash.reject { |k, _v| specified_properties.include?(k.to_s) }.merge(result)
|
|
285
292
|
elsif options[:additional_properties].is_a?(Node)
|
|
286
|
-
|
|
287
|
-
additional_properties = data_hash.reject { |k, _v| add_prop_specified.include?(k.to_s.to_sym) }
|
|
293
|
+
additional_properties = data_hash.reject { |k, _v| specified_properties.include?(k.to_s) }
|
|
288
294
|
if additional_properties.any?
|
|
289
295
|
additional_properties_result = {}
|
|
290
296
|
additional_properties.each do |key, value|
|
|
@@ -300,19 +306,15 @@ module Schemacop
|
|
|
300
306
|
protected
|
|
301
307
|
|
|
302
308
|
def as_json_with_inline_refs(properties, pattern_properties)
|
|
303
|
-
all_of = []
|
|
304
|
-
|
|
305
309
|
# Add each inline ref
|
|
306
|
-
@inline_refs.
|
|
307
|
-
all_of << inline_ref.as_json
|
|
308
|
-
end
|
|
310
|
+
all_of = @inline_refs.map(&:as_json)
|
|
309
311
|
|
|
310
312
|
# Add own properties schema if any direct properties exist
|
|
311
313
|
if properties.any? || pattern_properties.any?
|
|
312
314
|
own_schema = {}
|
|
313
315
|
own_schema[:type] = :object
|
|
314
|
-
own_schema[:properties] = properties.values.
|
|
315
|
-
own_schema[:patternProperties] = pattern_properties.values.
|
|
316
|
+
own_schema[:properties] = properties.values.to_h { |p| [p.name, p.as_json] } if properties.any?
|
|
317
|
+
own_schema[:patternProperties] = pattern_properties.values.to_h { |p| [V3.sanitize_exp(p.name), p.as_json] } if pattern_properties.any?
|
|
316
318
|
|
|
317
319
|
if options[:additional_properties].is_a?(TrueClass)
|
|
318
320
|
own_schema[:additionalProperties] = true
|
data/lib/schemacop/v3/node.rb
CHANGED
|
@@ -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
|
|
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
|
|
@@ -13,7 +13,7 @@ module Schemacop
|
|
|
13
13
|
item = match(value)
|
|
14
14
|
|
|
15
15
|
unless item
|
|
16
|
-
if options[:treat_blank_as_nil] &&
|
|
16
|
+
if options[:treat_blank_as_nil] && V3.blank?(value) && !value.is_a?(FalseClass)
|
|
17
17
|
return nil
|
|
18
18
|
else
|
|
19
19
|
return value
|
|
@@ -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
|
-
if options[:treat_blank_as_nil] &&
|
|
40
|
+
if options[:treat_blank_as_nil] && V3.blank?(data) && !data.is_a?(FalseClass)
|
|
28
41
|
data = nil
|
|
29
42
|
end
|
|
30
43
|
|
|
31
|
-
super_data = super
|
|
44
|
+
super_data = super
|
|
32
45
|
return if super_data.nil?
|
|
33
46
|
|
|
34
47
|
matches = matches(super_data)
|
data/lib/schemacop/v3/result.rb
CHANGED
|
@@ -55,10 +55,23 @@ module Schemacop
|
|
|
55
55
|
|
|
56
56
|
def in_path(segment)
|
|
57
57
|
prev_path = @current_path
|
|
58
|
-
@current_path += [segment]
|
|
58
|
+
@current_path += [sanitize_segment(segment)]
|
|
59
59
|
yield
|
|
60
60
|
ensure
|
|
61
61
|
@current_path = prev_path
|
|
62
62
|
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
# A path segment can be a key of the validated data and therefore carry any
|
|
67
|
+
# encoding. Joining or splitting a message built from a string with an
|
|
68
|
+
# invalid byte sequence, or from one that is incompatible with the other
|
|
69
|
+
# messages, raises.
|
|
70
|
+
def sanitize_segment(segment)
|
|
71
|
+
return segment unless segment.is_a?(String)
|
|
72
|
+
return segment if segment.valid_encoding? && Encoding.compatible?(Encoding::UTF_8, segment)
|
|
73
|
+
|
|
74
|
+
return segment.scrub.encode(Encoding::UTF_8, invalid: :replace, undef: :replace)
|
|
75
|
+
end
|
|
63
76
|
end
|
|
64
77
|
end
|
|
@@ -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
|
|
@@ -27,7 +27,7 @@ module Schemacop
|
|
|
27
27
|
super_data = super
|
|
28
28
|
|
|
29
29
|
# Validate blank #
|
|
30
|
-
if options[:allow_blank].is_a?(FalseClass) &&
|
|
30
|
+
if options[:allow_blank].is_a?(FalseClass) && V3.blank?(super_data)
|
|
31
31
|
result.error 'String is blank but must not be blank!'
|
|
32
32
|
end
|
|
33
33
|
|
|
@@ -44,13 +44,29 @@ module Schemacop
|
|
|
44
44
|
result.error "String is #{length} characters long but must be at most #{options[:max_length]}."
|
|
45
45
|
end
|
|
46
46
|
|
|
47
|
+
# Validate encoding matches #
|
|
48
|
+
if options[:encoding]
|
|
49
|
+
allowed_encodings = Array(options[:encoding])
|
|
50
|
+
unless allowed_encodings.include?(super_data.encoding.name)
|
|
51
|
+
result.error "String has encoding #{super_data.encoding.name.inspect} but must be #{allowed_encodings.map(&:inspect).join(' or ')}."
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Validate byte sequence #
|
|
56
|
+
# `match?` raises an ArgumentError on an invalid byte sequence, so no
|
|
57
|
+
# check below may see such a string.
|
|
58
|
+
unless super_data.valid_encoding?
|
|
59
|
+
result.error "String has invalid #{super_data.encoding.name.inspect} encoding."
|
|
60
|
+
return
|
|
61
|
+
end
|
|
62
|
+
|
|
47
63
|
# Validate pattern #
|
|
48
64
|
if (pattern = options[:pattern])
|
|
49
65
|
unless options[:pattern].is_a?(Regexp)
|
|
50
66
|
pattern = Regexp.compile(pattern)
|
|
51
67
|
end
|
|
52
68
|
|
|
53
|
-
unless
|
|
69
|
+
unless V3.match?(pattern, super_data)
|
|
54
70
|
result.error "String does not match pattern #{V3.sanitize_exp(pattern).inspect}."
|
|
55
71
|
end
|
|
56
72
|
end
|
|
@@ -59,7 +75,7 @@ module Schemacop
|
|
|
59
75
|
if options[:format] && Schemacop.string_formatters.include?(options[:format])
|
|
60
76
|
pattern = Schemacop.string_formatters[options[:format]][:pattern]
|
|
61
77
|
|
|
62
|
-
if pattern && !
|
|
78
|
+
if pattern && !V3.match?(pattern, super_data)
|
|
63
79
|
result.error "String does not match format #{options[:format].to_s.inspect}."
|
|
64
80
|
elsif options[:format_options] && Node.resolve_class(options[:format])
|
|
65
81
|
node = create(options[:format], **options[:format_options])
|
|
@@ -109,6 +125,18 @@ module Schemacop
|
|
|
109
125
|
fail 'Option "min_length" can\'t be greater than "max_length".'
|
|
110
126
|
end
|
|
111
127
|
|
|
128
|
+
if options[:encoding]
|
|
129
|
+
unless options[:encoding].is_a?(String) || (options[:encoding].is_a?(Array) && options[:encoding].all? { |e| e.is_a?(String) })
|
|
130
|
+
fail 'Option "encoding" must be a string or an array of strings.'
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
Array(options[:encoding]).each do |encoding|
|
|
134
|
+
Encoding.find(encoding)
|
|
135
|
+
rescue ArgumentError
|
|
136
|
+
fail "Option \"encoding\" contains unknown encoding #{encoding.inspect}."
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
112
140
|
if options[:pattern]
|
|
113
141
|
unless options[:pattern].is_a?(String) || options[:pattern].is_a?(Regexp)
|
|
114
142
|
fail 'Option "pattern" must be a string or Regexp.'
|
data/lib/schemacop/v3.rb
CHANGED
|
@@ -4,6 +4,38 @@ module Schemacop
|
|
|
4
4
|
NodeRegistry.register(*args)
|
|
5
5
|
end
|
|
6
6
|
|
|
7
|
+
# `String#blank?` matches a regular expression and therefore raises on a
|
|
8
|
+
# string it cannot compare: an `ArgumentError` on an invalid byte sequence,
|
|
9
|
+
# a `RegexpError` on a dummy encoding such as UTF-16. Neither kind of
|
|
10
|
+
# string is blank unless it is empty.
|
|
11
|
+
# @private
|
|
12
|
+
def self.blank?(value)
|
|
13
|
+
return false if invalid_encoding?(value)
|
|
14
|
+
|
|
15
|
+
return value.blank?
|
|
16
|
+
rescue Encoding::CompatibilityError, RegexpError
|
|
17
|
+
return value.empty?
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# `Regexp#match?` raises on a string it cannot compare: an `ArgumentError`
|
|
21
|
+
# on an invalid byte sequence, an `Encoding::CompatibilityError` on an
|
|
22
|
+
# encoding incompatible with the pattern's. Neither kind of string matches.
|
|
23
|
+
# @private
|
|
24
|
+
def self.match?(pattern, value)
|
|
25
|
+
return false if invalid_encoding?(value)
|
|
26
|
+
|
|
27
|
+
begin
|
|
28
|
+
return pattern.match?(value)
|
|
29
|
+
rescue Encoding::CompatibilityError
|
|
30
|
+
return false
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @private
|
|
35
|
+
def self.invalid_encoding?(value)
|
|
36
|
+
return value.is_a?(String) && !value.valid_encoding?
|
|
37
|
+
end
|
|
38
|
+
|
|
7
39
|
# @private
|
|
8
40
|
def self.sanitize_exp(exp)
|
|
9
41
|
return exp if exp.is_a?(String)
|