schemurai 3.1.0-aarch64-linux-gnu
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 +7 -0
- data/LICENSE +21 -0
- data/README.md +395 -0
- data/lib/schemurai/backend.rb +34 -0
- data/lib/schemurai/container_checks/array_diagnostics.rb +119 -0
- data/lib/schemurai/container_checks/object_diagnostics.rb +152 -0
- data/lib/schemurai/container_checks.rb +534 -0
- data/lib/schemurai/decimal.rb +11 -0
- data/lib/schemurai/dialect.rb +106 -0
- data/lib/schemurai/dialect_keywords.rb +91 -0
- data/lib/schemurai/dialects/draft2019_09.rb +27 -0
- data/lib/schemurai/dialects/draft2020_12.rb +27 -0
- data/lib/schemurai/dialects/draft7.rb +27 -0
- data/lib/schemurai/ecma_regexp.rb +46 -0
- data/lib/schemurai/error_message.rb +102 -0
- data/lib/schemurai/evaluation.rb +63 -0
- data/lib/schemurai/evaluation_limits.rb +65 -0
- data/lib/schemurai/evaluation_pool.rb +66 -0
- data/lib/schemurai/evaluator.rb +861 -0
- data/lib/schemurai/formats.rb +403 -0
- data/lib/schemurai/json_pointer.rb +11 -0
- data/lib/schemurai/limit_budget.rb +78 -0
- data/lib/schemurai/limit_exceeded.rb +18 -0
- data/lib/schemurai/limited_evaluator.rb +43 -0
- data/lib/schemurai/meta_schemas/draft2019_09.rb +149 -0
- data/lib/schemurai/meta_schemas/draft2020_12.rb +149 -0
- data/lib/schemurai/meta_schemas/draft7.rb +121 -0
- data/lib/schemurai/meta_schemas.rb +28 -0
- data/lib/schemurai/native/4.0/schemurai_native.so +0 -0
- data/lib/schemurai/native/diagnostics.rb +53 -0
- data/lib/schemurai/native/evaluator.rb +87 -0
- data/lib/schemurai/native/null_numeric.rb +23 -0
- data/lib/schemurai/number_checks.rb +128 -0
- data/lib/schemurai/schema_domain.rb +95 -0
- data/lib/schemurai/schema_graph.rb +538 -0
- data/lib/schemurai/schema_node.rb +55 -0
- data/lib/schemurai/string_checks.rb +166 -0
- data/lib/schemurai/type_checks.rb +59 -0
- data/lib/schemurai/unique_items.rb +68 -0
- data/lib/schemurai/value_equality.rb +72 -0
- data/lib/schemurai/version.rb +6 -0
- data/lib/schemurai/vm/compiler.rb +410 -0
- data/lib/schemurai/vm/evaluator.rb +663 -0
- data/lib/schemurai/vm/limited_evaluator.rb +27 -0
- data/lib/schemurai.rb +307 -0
- data/lib/schemurai_native.rb +4 -0
- data/sig/schemurai.rbs +108 -0
- metadata +121 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Schemurai
|
|
4
|
+
module Internal
|
|
5
|
+
class Dialect
|
|
6
|
+
TYPE = 1
|
|
7
|
+
ENUM = 2
|
|
8
|
+
COMBINER = 4
|
|
9
|
+
NUMBER = 8
|
|
10
|
+
STRING = 16
|
|
11
|
+
ARRAY = 32
|
|
12
|
+
OBJECT = 64
|
|
13
|
+
|
|
14
|
+
Keyword = Data.define(:mask, :subschema_shape) do
|
|
15
|
+
def initialize(mask:, subschema_shape: nil)
|
|
16
|
+
super
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
class << self
|
|
21
|
+
def register(dialect, default: false)
|
|
22
|
+
registry[normalize_uri(dialect.uri)] = dialect
|
|
23
|
+
@default = dialect if default
|
|
24
|
+
dialect
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def resolve(uri = nil)
|
|
28
|
+
return @default if uri.nil?
|
|
29
|
+
|
|
30
|
+
registry[normalize_uri(uri)]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private def registry
|
|
34
|
+
@registry ||= {}
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private def normalize_uri(uri)
|
|
38
|
+
uri.to_s.delete_suffix("#")
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
attr_reader :name, :uri, :keywords
|
|
43
|
+
|
|
44
|
+
def initialize(name:, uri:, keywords:, ref_siblings:, format_assertion: false)
|
|
45
|
+
@name = name
|
|
46
|
+
@uri = uri
|
|
47
|
+
@keywords = keywords.freeze
|
|
48
|
+
@ref_siblings = ref_siblings
|
|
49
|
+
@format_assertion = format_assertion
|
|
50
|
+
freeze
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def ref_siblings?
|
|
54
|
+
@ref_siblings
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def format_assertion?
|
|
58
|
+
@format_assertion
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def keyword_mask(schema)
|
|
62
|
+
schema.each_key.reduce(0) do |mask, keyword|
|
|
63
|
+
mask | (keywords[keyword]&.mask || 0)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def each_subschema(schema)
|
|
68
|
+
return enum_for(__method__, schema) unless block_given?
|
|
69
|
+
return unless schema.is_a?(Hash)
|
|
70
|
+
return if schema.key?("$ref") && !ref_siblings?
|
|
71
|
+
|
|
72
|
+
schema.each do |keyword, value|
|
|
73
|
+
specification = keywords[keyword]
|
|
74
|
+
next unless specification&.subschema_shape
|
|
75
|
+
|
|
76
|
+
case specification.subschema_shape
|
|
77
|
+
when :single
|
|
78
|
+
yield value, [keyword] if schema?(value)
|
|
79
|
+
when :single_or_list
|
|
80
|
+
if value.is_a?(Array)
|
|
81
|
+
value.each_with_index { |child, index| yield child, [keyword, index] if schema?(child) }
|
|
82
|
+
elsif schema?(value)
|
|
83
|
+
yield value, [keyword]
|
|
84
|
+
end
|
|
85
|
+
when :list
|
|
86
|
+
Array(value).each_with_index { |child, index| yield child, [keyword, index] if schema?(child) }
|
|
87
|
+
when :map
|
|
88
|
+
value.each { |name, child| yield child, [keyword, name] if schema?(child) } if value.is_a?(Hash)
|
|
89
|
+
when :dependencies
|
|
90
|
+
if value.is_a?(Hash)
|
|
91
|
+
value.each do |name, child|
|
|
92
|
+
yield child, [keyword, name] if schema?(child)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
private def schema?(value)
|
|
100
|
+
value == true || value == false || value.is_a?(Hash)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
private_constant :Internal
|
|
106
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "dialect"
|
|
4
|
+
|
|
5
|
+
module Schemurai
|
|
6
|
+
module Internal
|
|
7
|
+
module DialectKeywords
|
|
8
|
+
COMMON = {
|
|
9
|
+
"type" => Dialect::Keyword.new(mask: Dialect::TYPE),
|
|
10
|
+
"enum" => Dialect::Keyword.new(mask: Dialect::ENUM),
|
|
11
|
+
"const" => Dialect::Keyword.new(mask: Dialect::ENUM),
|
|
12
|
+
"allOf" => Dialect::Keyword.new(mask: Dialect::COMBINER, subschema_shape: :list),
|
|
13
|
+
"anyOf" => Dialect::Keyword.new(mask: Dialect::COMBINER, subschema_shape: :list),
|
|
14
|
+
"oneOf" => Dialect::Keyword.new(mask: Dialect::COMBINER, subschema_shape: :list),
|
|
15
|
+
"not" => Dialect::Keyword.new(mask: Dialect::COMBINER, subschema_shape: :single),
|
|
16
|
+
"if" => Dialect::Keyword.new(mask: Dialect::COMBINER, subschema_shape: :single),
|
|
17
|
+
"then" => Dialect::Keyword.new(mask: 0, subschema_shape: :single),
|
|
18
|
+
"else" => Dialect::Keyword.new(mask: 0, subschema_shape: :single),
|
|
19
|
+
"maximum" => Dialect::Keyword.new(mask: Dialect::NUMBER),
|
|
20
|
+
"minimum" => Dialect::Keyword.new(mask: Dialect::NUMBER),
|
|
21
|
+
"exclusiveMaximum" => Dialect::Keyword.new(mask: Dialect::NUMBER),
|
|
22
|
+
"exclusiveMinimum" => Dialect::Keyword.new(mask: Dialect::NUMBER),
|
|
23
|
+
"multipleOf" => Dialect::Keyword.new(mask: Dialect::NUMBER),
|
|
24
|
+
"maxLength" => Dialect::Keyword.new(mask: Dialect::STRING),
|
|
25
|
+
"minLength" => Dialect::Keyword.new(mask: Dialect::STRING),
|
|
26
|
+
"pattern" => Dialect::Keyword.new(mask: Dialect::STRING),
|
|
27
|
+
"contentEncoding" => Dialect::Keyword.new(mask: Dialect::STRING),
|
|
28
|
+
"contentMediaType" => Dialect::Keyword.new(mask: Dialect::STRING),
|
|
29
|
+
"maxItems" => Dialect::Keyword.new(mask: Dialect::ARRAY),
|
|
30
|
+
"minItems" => Dialect::Keyword.new(mask: Dialect::ARRAY),
|
|
31
|
+
"uniqueItems" => Dialect::Keyword.new(mask: Dialect::ARRAY),
|
|
32
|
+
"contains" => Dialect::Keyword.new(mask: Dialect::ARRAY, subschema_shape: :single),
|
|
33
|
+
"maxProperties" => Dialect::Keyword.new(mask: Dialect::OBJECT),
|
|
34
|
+
"minProperties" => Dialect::Keyword.new(mask: Dialect::OBJECT),
|
|
35
|
+
"required" => Dialect::Keyword.new(mask: Dialect::OBJECT),
|
|
36
|
+
"properties" => Dialect::Keyword.new(mask: Dialect::OBJECT, subschema_shape: :map),
|
|
37
|
+
"patternProperties" => Dialect::Keyword.new(mask: Dialect::OBJECT, subschema_shape: :map),
|
|
38
|
+
"additionalProperties" => Dialect::Keyword.new(mask: Dialect::OBJECT, subschema_shape: :single),
|
|
39
|
+
"propertyNames" => Dialect::Keyword.new(mask: Dialect::OBJECT, subschema_shape: :single)
|
|
40
|
+
}.freeze
|
|
41
|
+
|
|
42
|
+
LEGACY = {
|
|
43
|
+
"dependencies" => Dialect::Keyword.new(mask: Dialect::OBJECT, subschema_shape: :dependencies),
|
|
44
|
+
"definitions" => Dialect::Keyword.new(mask: 0, subschema_shape: :map)
|
|
45
|
+
}.freeze
|
|
46
|
+
|
|
47
|
+
MODERN = {
|
|
48
|
+
"$defs" => Dialect::Keyword.new(mask: 0, subschema_shape: :map),
|
|
49
|
+
"$recursiveRef" => Dialect::Keyword.new(mask: Dialect::COMBINER),
|
|
50
|
+
"dependentSchemas" => Dialect::Keyword.new(mask: Dialect::OBJECT, subschema_shape: :map),
|
|
51
|
+
"dependentRequired" => Dialect::Keyword.new(mask: Dialect::OBJECT),
|
|
52
|
+
"minContains" => Dialect::Keyword.new(mask: Dialect::ARRAY),
|
|
53
|
+
"maxContains" => Dialect::Keyword.new(mask: Dialect::ARRAY),
|
|
54
|
+
"unevaluatedItems" => Dialect::Keyword.new(mask: Dialect::ARRAY, subschema_shape: :single),
|
|
55
|
+
"unevaluatedProperties" => Dialect::Keyword.new(mask: Dialect::OBJECT, subschema_shape: :single),
|
|
56
|
+
"contentSchema" => Dialect::Keyword.new(mask: Dialect::STRING, subschema_shape: :single)
|
|
57
|
+
}.freeze
|
|
58
|
+
|
|
59
|
+
private_constant :COMMON, :LEGACY, :MODERN
|
|
60
|
+
|
|
61
|
+
module_function def draft7
|
|
62
|
+
COMMON.merge(
|
|
63
|
+
LEGACY,
|
|
64
|
+
"items" => Dialect::Keyword.new(mask: Dialect::ARRAY, subschema_shape: :single_or_list),
|
|
65
|
+
"additionalItems" => Dialect::Keyword.new(mask: Dialect::ARRAY, subschema_shape: :single)
|
|
66
|
+
).freeze
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
module_function def draft2019_09
|
|
70
|
+
COMMON.merge(
|
|
71
|
+
LEGACY,
|
|
72
|
+
MODERN,
|
|
73
|
+
"items" => Dialect::Keyword.new(mask: Dialect::ARRAY, subschema_shape: :single_or_list),
|
|
74
|
+
"additionalItems" => Dialect::Keyword.new(mask: Dialect::ARRAY, subschema_shape: :single)
|
|
75
|
+
).freeze
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
module_function def draft2020_12
|
|
79
|
+
COMMON.merge(
|
|
80
|
+
LEGACY,
|
|
81
|
+
MODERN,
|
|
82
|
+
"$dynamicRef" => Dialect::Keyword.new(mask: Dialect::COMBINER),
|
|
83
|
+
"prefixItems" => Dialect::Keyword.new(mask: Dialect::ARRAY, subschema_shape: :list),
|
|
84
|
+
"items" => Dialect::Keyword.new(mask: Dialect::ARRAY, subschema_shape: :single)
|
|
85
|
+
).freeze
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private_constant :Internal
|
|
91
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../dialect_keywords"
|
|
4
|
+
|
|
5
|
+
module Schemurai
|
|
6
|
+
module Internal
|
|
7
|
+
module Dialects
|
|
8
|
+
module Draft201909
|
|
9
|
+
META_SCHEMA_URI = "https://json-schema.org/draft/2019-09/schema"
|
|
10
|
+
KEYWORDS = DialectKeywords.draft2019_09
|
|
11
|
+
|
|
12
|
+
DIALECT = Dialect.new(
|
|
13
|
+
name: :draft2019_09,
|
|
14
|
+
uri: META_SCHEMA_URI,
|
|
15
|
+
keywords: KEYWORDS,
|
|
16
|
+
ref_siblings: true
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
Dialect.register(DIALECT)
|
|
20
|
+
|
|
21
|
+
private_constant :META_SCHEMA_URI, :KEYWORDS
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private_constant :Internal
|
|
27
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../dialect_keywords"
|
|
4
|
+
|
|
5
|
+
module Schemurai
|
|
6
|
+
module Internal
|
|
7
|
+
module Dialects
|
|
8
|
+
module Draft202012
|
|
9
|
+
META_SCHEMA_URI = "https://json-schema.org/draft/2020-12/schema"
|
|
10
|
+
KEYWORDS = DialectKeywords.draft2020_12
|
|
11
|
+
|
|
12
|
+
DIALECT = Dialect.new(
|
|
13
|
+
name: :draft2020_12,
|
|
14
|
+
uri: META_SCHEMA_URI,
|
|
15
|
+
keywords: KEYWORDS,
|
|
16
|
+
ref_siblings: true
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
Dialect.register(DIALECT)
|
|
20
|
+
|
|
21
|
+
private_constant :META_SCHEMA_URI, :KEYWORDS
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private_constant :Internal
|
|
27
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../dialect_keywords"
|
|
4
|
+
|
|
5
|
+
module Schemurai
|
|
6
|
+
module Internal
|
|
7
|
+
module Dialects
|
|
8
|
+
module Draft7
|
|
9
|
+
META_SCHEMA_URI = "http://json-schema.org/draft-07/schema"
|
|
10
|
+
KEYWORDS = DialectKeywords.draft7
|
|
11
|
+
|
|
12
|
+
DIALECT = Dialect.new(
|
|
13
|
+
name: :draft7,
|
|
14
|
+
uri: META_SCHEMA_URI,
|
|
15
|
+
keywords: KEYWORDS,
|
|
16
|
+
ref_siblings: false
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
Dialect.register(DIALECT, default: true)
|
|
20
|
+
|
|
21
|
+
private_constant :META_SCHEMA_URI, :KEYWORDS
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private_constant :Internal
|
|
27
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Schemurai
|
|
4
|
+
module Internal
|
|
5
|
+
# Shared ECMA character classes and absolute anchors for Ruby's Regexp.
|
|
6
|
+
module EcmaRegexp
|
|
7
|
+
module_function def compile(pattern, budget = nil)
|
|
8
|
+
whitespace = "\\u0009-\\u000D\\u0020\\u00A0\\u1680\\u2000-\\u200A\\u2028\\u2029\\u202F\\u205F\\u3000\\uFEFF"
|
|
9
|
+
translated = +""
|
|
10
|
+
escaped = false
|
|
11
|
+
in_class = false
|
|
12
|
+
pattern.each_char do |character|
|
|
13
|
+
budget&.checkpoint
|
|
14
|
+
if escaped
|
|
15
|
+
translated << case character
|
|
16
|
+
when "d" then in_class ? "0-9" : "[0-9]"
|
|
17
|
+
when "D" then in_class ? "^0-9" : "[^0-9]"
|
|
18
|
+
when "w" then in_class ? "A-Za-z0-9_" : "[A-Za-z0-9_]"
|
|
19
|
+
when "W" then in_class ? "^A-Za-z0-9_" : "[^A-Za-z0-9_]"
|
|
20
|
+
when "s" then in_class ? whitespace : "[#{whitespace}]"
|
|
21
|
+
when "S" then in_class ? "^#{whitespace}" : "[^#{whitespace}]"
|
|
22
|
+
else "\\#{character}"
|
|
23
|
+
end
|
|
24
|
+
escaped = false
|
|
25
|
+
elsif character == "\\"
|
|
26
|
+
escaped = true
|
|
27
|
+
elsif character == "["
|
|
28
|
+
in_class = true
|
|
29
|
+
translated << character
|
|
30
|
+
elsif character == "]"
|
|
31
|
+
in_class = false
|
|
32
|
+
translated << character
|
|
33
|
+
elsif character == "^" && !in_class
|
|
34
|
+
translated << "\\A"
|
|
35
|
+
elsif character == "$" && !in_class
|
|
36
|
+
translated << "\\z"
|
|
37
|
+
else
|
|
38
|
+
translated << character
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
translated << "\\" if escaped
|
|
42
|
+
Regexp.new(translated)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Schemurai
|
|
4
|
+
module Internal
|
|
5
|
+
# Builds validation messages shared by every evaluator backend.
|
|
6
|
+
module ErrorMessage
|
|
7
|
+
module_function def false_schema = "value is not allowed by this schema"
|
|
8
|
+
|
|
9
|
+
module_function def type(expected, value)
|
|
10
|
+
expected = Array(expected).map { |name| name.inspect }.join(" or ")
|
|
11
|
+
"value has type #{json_type(value).inspect}, but must have type #{expected}"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
module_function def enum = "value must equal one of the values defined by `enum`"
|
|
15
|
+
module_function def const = "value must equal the value defined by `const`"
|
|
16
|
+
|
|
17
|
+
module_function def numeric_limit(keyword, limit)
|
|
18
|
+
comparison = {
|
|
19
|
+
"maximum" => "less than or equal to",
|
|
20
|
+
"minimum" => "greater than or equal to",
|
|
21
|
+
"exclusiveMaximum" => "less than",
|
|
22
|
+
"exclusiveMinimum" => "greater than"
|
|
23
|
+
}.fetch(keyword)
|
|
24
|
+
"number must be #{comparison} #{display_number(limit)}"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
module_function def multiple_of(divisor) = "number must be a multiple of #{display_number(divisor)}"
|
|
28
|
+
|
|
29
|
+
module_function def size(keyword, limit, actual)
|
|
30
|
+
subject, unit, comparison = {
|
|
31
|
+
"maxLength" => ["string", "characters", "at most"],
|
|
32
|
+
"minLength" => ["string", "characters", "at least"],
|
|
33
|
+
"maxItems" => ["array", "items", "at most"],
|
|
34
|
+
"minItems" => ["array", "items", "at least"],
|
|
35
|
+
"maxProperties" => ["object", "properties", "at most"],
|
|
36
|
+
"minProperties" => ["object", "properties", "at least"]
|
|
37
|
+
}.fetch(keyword)
|
|
38
|
+
unit = {"characters" => "character", "items" => "item", "properties" => "property"}.fetch(unit) if limit == 1
|
|
39
|
+
"#{subject} must contain #{comparison} #{limit} #{unit} (found #{actual})"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
module_function def pattern(pattern) = "string must match pattern #{pattern.inspect}"
|
|
43
|
+
module_function def invalid_pattern(pattern) = "schema pattern #{pattern.inspect} is not a valid regular expression"
|
|
44
|
+
module_function def format(name) = "string must match the #{name} format"
|
|
45
|
+
module_function def content_encoding = "string must be valid base64"
|
|
46
|
+
module_function def content_media_type = "string must contain valid JSON"
|
|
47
|
+
module_function def unique_items = "array items must be unique"
|
|
48
|
+
|
|
49
|
+
module_function def contains(actual, minimum, maximum)
|
|
50
|
+
expected = if maximum.infinite?
|
|
51
|
+
"at least #{minimum}"
|
|
52
|
+
elsif minimum == maximum
|
|
53
|
+
"exactly #{minimum}"
|
|
54
|
+
elsif minimum.zero?
|
|
55
|
+
"at most #{maximum}"
|
|
56
|
+
else
|
|
57
|
+
"between #{minimum} and #{maximum}"
|
|
58
|
+
end
|
|
59
|
+
"array must contain #{expected} items matching `contains` (found #{actual})"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
module_function def required(name) = "object is missing required property #{name.inspect}"
|
|
63
|
+
|
|
64
|
+
module_function def dependent_required(name, required_name)
|
|
65
|
+
"property #{required_name.inspect} is required when property #{name.inspect} is present"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
module_function def any_of = "value must match at least one subschema"
|
|
69
|
+
module_function def one_of(matches) = "value must match exactly one subschema (matched #{matches})"
|
|
70
|
+
module_function def not = "value must not match the subschema"
|
|
71
|
+
|
|
72
|
+
module_function def json_type(value)
|
|
73
|
+
case value
|
|
74
|
+
when nil then "null"
|
|
75
|
+
when true, false then "boolean"
|
|
76
|
+
when Hash then "object"
|
|
77
|
+
when Array then "array"
|
|
78
|
+
when String then "string"
|
|
79
|
+
when Numeric
|
|
80
|
+
if value.is_a?(Complex)
|
|
81
|
+
value.class.name
|
|
82
|
+
elsif value.finite? && value.to_i == value
|
|
83
|
+
"integer"
|
|
84
|
+
else
|
|
85
|
+
"number"
|
|
86
|
+
end
|
|
87
|
+
else
|
|
88
|
+
value.class.name
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
private_class_method :json_type
|
|
92
|
+
|
|
93
|
+
module_function def display_number(value)
|
|
94
|
+
rational = value.is_a?(Rational) ? value : Rational(value.to_s)
|
|
95
|
+
return rational.numerator.to_s if rational.denominator == 1
|
|
96
|
+
|
|
97
|
+
rational.to_f.to_s
|
|
98
|
+
end
|
|
99
|
+
private_class_method :display_number
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Schemurai
|
|
4
|
+
# Internal result of evaluating one schema location. Draft 7 only needs the
|
|
5
|
+
# validity bit, but newer drafts also propagate the instance locations
|
|
6
|
+
# consumed by applicators for unevaluatedProperties and unevaluatedItems.
|
|
7
|
+
class Evaluation
|
|
8
|
+
EMPTY_LOCATIONS = [].freeze
|
|
9
|
+
|
|
10
|
+
attr_reader :evaluated_properties, :evaluated_items
|
|
11
|
+
|
|
12
|
+
def self.valid(evaluated_properties: EMPTY_LOCATIONS, evaluated_items: EMPTY_LOCATIONS)
|
|
13
|
+
if evaluated_properties.equal?(EMPTY_LOCATIONS) && evaluated_items.equal?(EMPTY_LOCATIONS)
|
|
14
|
+
return @valid ||= new(true, EMPTY_LOCATIONS, EMPTY_LOCATIONS)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
new(true, evaluated_properties, evaluated_items)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.invalid
|
|
21
|
+
@invalid ||= new(false, EMPTY_LOCATIONS, EMPTY_LOCATIONS)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def initialize(valid, evaluated_properties, evaluated_items)
|
|
25
|
+
@valid = valid
|
|
26
|
+
@evaluated_properties = evaluated_properties.freeze
|
|
27
|
+
@evaluated_items = evaluated_items.freeze
|
|
28
|
+
freeze
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def valid?
|
|
32
|
+
@valid
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def merge(other)
|
|
36
|
+
return self.class.invalid unless valid? && other.valid?
|
|
37
|
+
return self if other.evaluated_properties.empty? && other.evaluated_items.empty?
|
|
38
|
+
return other if evaluated_properties.empty? && evaluated_items.empty?
|
|
39
|
+
|
|
40
|
+
properties = if evaluated_properties.empty?
|
|
41
|
+
other.evaluated_properties
|
|
42
|
+
elsif other.evaluated_properties.empty?
|
|
43
|
+
evaluated_properties
|
|
44
|
+
else
|
|
45
|
+
evaluated_properties | other.evaluated_properties
|
|
46
|
+
end
|
|
47
|
+
items = if evaluated_items.empty?
|
|
48
|
+
other.evaluated_items
|
|
49
|
+
elsif other.evaluated_items.empty?
|
|
50
|
+
evaluated_items
|
|
51
|
+
else
|
|
52
|
+
evaluated_items | other.evaluated_items
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
self.class.valid(
|
|
56
|
+
evaluated_properties: properties,
|
|
57
|
+
evaluated_items: items
|
|
58
|
+
)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private_constant :Evaluation
|
|
63
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "limit_budget"
|
|
4
|
+
|
|
5
|
+
module Schemurai
|
|
6
|
+
module Internal
|
|
7
|
+
# Limit policy shared by evaluators; error storage and scalar state stay local.
|
|
8
|
+
module EvaluationLimits
|
|
9
|
+
def validate(instance, max_errors: nil, timeout: nil, budget: nil)
|
|
10
|
+
with_budget(max_errors, timeout, budget) { super(instance) }
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def valid?(instance, max_errors: nil, timeout: nil, budget: nil)
|
|
14
|
+
with_budget(max_errors, timeout, budget) { super(instance) }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private def with_budget(max_errors, timeout, budget)
|
|
18
|
+
@budget = budget || LimitBudget.new(max_errors: max_errors, timeout: timeout)
|
|
19
|
+
result = yield
|
|
20
|
+
@budget.check_deadline!
|
|
21
|
+
result
|
|
22
|
+
ensure
|
|
23
|
+
@budget = nil
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private def evaluate(node, instance)
|
|
27
|
+
@budget.checkpoint
|
|
28
|
+
super
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private def evaluate_valid(node, instance)
|
|
32
|
+
@budget.checkpoint
|
|
33
|
+
super
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private def valid_string?(node, value)
|
|
37
|
+
@budget.check_deadline!
|
|
38
|
+
result = super
|
|
39
|
+
@budget.check_deadline!
|
|
40
|
+
result
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private def check_string(node, value)
|
|
44
|
+
@budget.check_deadline!
|
|
45
|
+
result = super
|
|
46
|
+
@budget.check_deadline!
|
|
47
|
+
result
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private def valid_content?(schema, value)
|
|
51
|
+
@budget.check_deadline!
|
|
52
|
+
result = super
|
|
53
|
+
@budget.check_deadline!
|
|
54
|
+
result
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private def check_content(schema, value)
|
|
58
|
+
@budget.check_deadline!
|
|
59
|
+
result = super
|
|
60
|
+
@budget.check_deadline!
|
|
61
|
+
result
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "set" # rubocop:disable Lint/RedundantRequireStatement -- Required on Ruby 3.4.
|
|
4
|
+
require_relative "evaluation"
|
|
5
|
+
|
|
6
|
+
module Schemurai
|
|
7
|
+
module Internal
|
|
8
|
+
class EvaluationBuffer
|
|
9
|
+
attr_reader :evaluated_properties, :evaluated_items
|
|
10
|
+
|
|
11
|
+
def initialize
|
|
12
|
+
# Applicators can overlap. Array membership makes collecting and merging
|
|
13
|
+
# their annotations quadratic in the number of instance locations.
|
|
14
|
+
@evaluated_properties = Set.new
|
|
15
|
+
@evaluated_items = Set.new
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def valid? = true
|
|
19
|
+
|
|
20
|
+
def reset
|
|
21
|
+
@evaluated_properties.clear
|
|
22
|
+
@evaluated_items.clear
|
|
23
|
+
self
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def record_property(name)
|
|
27
|
+
@evaluated_properties.add(name)
|
|
28
|
+
self
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def record_item(index)
|
|
32
|
+
@evaluated_items.add(index)
|
|
33
|
+
self
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def merge(other)
|
|
37
|
+
return Evaluation.invalid unless other.valid?
|
|
38
|
+
|
|
39
|
+
properties = other.evaluated_properties
|
|
40
|
+
items = other.evaluated_items
|
|
41
|
+
@evaluated_properties.merge(properties) unless properties.empty?
|
|
42
|
+
@evaluated_items.merge(items) unless items.empty?
|
|
43
|
+
self
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
class EvaluationPool
|
|
48
|
+
def initialize
|
|
49
|
+
@buffers = []
|
|
50
|
+
@index = 0
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def reset
|
|
54
|
+
@index = 0
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def used? = !@buffers.empty?
|
|
58
|
+
|
|
59
|
+
def acquire
|
|
60
|
+
index = @index
|
|
61
|
+
@index += 1
|
|
62
|
+
(@buffers[index] ||= EvaluationBuffer.new).reset
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|