jsi 0.4.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.yardopts +1 -1
- data/CHANGELOG.md +33 -0
- data/LICENSE.md +1 -1
- data/README.md +114 -42
- data/jsi.gemspec +14 -12
- data/lib/jsi/base/node.rb +183 -0
- data/lib/jsi/base.rb +388 -220
- data/lib/jsi/jsi_coder.rb +8 -7
- data/lib/jsi/metaschema.rb +0 -1
- data/lib/jsi/metaschema_node/bootstrap_schema.rb +101 -0
- data/lib/jsi/metaschema_node.rb +159 -135
- data/lib/jsi/ptr.rb +303 -0
- data/lib/jsi/schema/application/child_application/contains.rb +25 -0
- data/lib/jsi/schema/application/child_application/draft04.rb +22 -0
- data/lib/jsi/schema/application/child_application/draft06.rb +29 -0
- data/lib/jsi/schema/application/child_application/draft07.rb +29 -0
- data/lib/jsi/schema/application/child_application/items.rb +18 -0
- data/lib/jsi/schema/application/child_application/properties.rb +25 -0
- data/lib/jsi/schema/application/child_application.rb +38 -0
- data/lib/jsi/schema/application/draft04.rb +8 -0
- data/lib/jsi/schema/application/draft06.rb +8 -0
- data/lib/jsi/schema/application/draft07.rb +8 -0
- data/lib/jsi/schema/application/inplace_application/dependencies.rb +28 -0
- data/lib/jsi/schema/application/inplace_application/draft04.rb +26 -0
- data/lib/jsi/schema/application/inplace_application/draft06.rb +27 -0
- data/lib/jsi/schema/application/inplace_application/draft07.rb +33 -0
- data/lib/jsi/schema/application/inplace_application/ifthenelse.rb +20 -0
- data/lib/jsi/schema/application/inplace_application/ref.rb +18 -0
- data/lib/jsi/schema/application/inplace_application/someof.rb +44 -0
- data/lib/jsi/schema/application/inplace_application.rb +41 -0
- data/lib/jsi/schema/application.rb +12 -0
- data/lib/jsi/schema/draft04.rb +14 -0
- data/lib/jsi/schema/draft06.rb +14 -0
- data/lib/jsi/schema/draft07.rb +14 -0
- data/lib/jsi/schema/issue.rb +36 -0
- data/lib/jsi/schema/ref.rb +160 -0
- data/lib/jsi/schema/schema_ancestor_node.rb +113 -0
- data/lib/jsi/schema/validation/array.rb +69 -0
- data/lib/jsi/schema/validation/const.rb +20 -0
- data/lib/jsi/schema/validation/contains.rb +25 -0
- data/lib/jsi/schema/validation/core.rb +39 -0
- data/lib/jsi/schema/validation/dependencies.rb +49 -0
- data/lib/jsi/schema/validation/draft04/minmax.rb +91 -0
- data/lib/jsi/schema/validation/draft04.rb +112 -0
- data/lib/jsi/schema/validation/draft06.rb +122 -0
- data/lib/jsi/schema/validation/draft07.rb +159 -0
- data/lib/jsi/schema/validation/enum.rb +25 -0
- data/lib/jsi/schema/validation/ifthenelse.rb +46 -0
- data/lib/jsi/schema/validation/items.rb +54 -0
- data/lib/jsi/schema/validation/not.rb +20 -0
- data/lib/jsi/schema/validation/numeric.rb +121 -0
- data/lib/jsi/schema/validation/object.rb +45 -0
- data/lib/jsi/schema/validation/pattern.rb +34 -0
- data/lib/jsi/schema/validation/properties.rb +101 -0
- data/lib/jsi/schema/validation/property_names.rb +32 -0
- data/lib/jsi/schema/validation/ref.rb +40 -0
- data/lib/jsi/schema/validation/required.rb +27 -0
- data/lib/jsi/schema/validation/someof.rb +90 -0
- data/lib/jsi/schema/validation/string.rb +47 -0
- data/lib/jsi/schema/validation/type.rb +49 -0
- data/lib/jsi/schema/validation.rb +51 -0
- data/lib/jsi/schema.rb +508 -149
- data/lib/jsi/schema_classes.rb +199 -59
- data/lib/jsi/schema_registry.rb +151 -0
- data/lib/jsi/schema_set.rb +181 -0
- data/lib/jsi/simple_wrap.rb +23 -4
- data/lib/jsi/util/private/attr_struct.rb +127 -0
- data/lib/jsi/util/private.rb +204 -0
- data/lib/jsi/util/typelike.rb +229 -0
- data/lib/jsi/util.rb +89 -53
- data/lib/jsi/validation/error.rb +34 -0
- data/lib/jsi/validation/result.rb +210 -0
- data/lib/jsi/validation.rb +15 -0
- data/lib/jsi/version.rb +3 -1
- data/lib/jsi.rb +44 -14
- data/lib/schemas/json-schema.org/draft-04/schema.rb +10 -3
- data/lib/schemas/json-schema.org/draft-06/schema.rb +10 -3
- data/lib/schemas/json-schema.org/draft-07/schema.rb +14 -0
- data/readme.rb +138 -0
- data/{resources}/schemas/json-schema.org/draft-04/schema.json +149 -0
- data/{resources}/schemas/json-schema.org/draft-06/schema.json +154 -0
- data/{resources}/schemas/json-schema.org/draft-07/schema.json +168 -0
- metadata +75 -122
- data/.simplecov +0 -3
- data/Rakefile.rb +0 -9
- data/lib/jsi/base/to_rb.rb +0 -128
- data/lib/jsi/json/node.rb +0 -203
- data/lib/jsi/json/pointer.rb +0 -419
- data/lib/jsi/json-schema-fragments.rb +0 -61
- data/lib/jsi/json.rb +0 -10
- data/lib/jsi/pathed_node.rb +0 -118
- data/lib/jsi/typelike_modules.rb +0 -240
- data/resources/icons/AGPL-3.0.png +0 -0
- data/test/base_array_test.rb +0 -323
- data/test/base_hash_test.rb +0 -337
- data/test/base_test.rb +0 -486
- data/test/jsi_coder_test.rb +0 -85
- data/test/jsi_json_arraynode_test.rb +0 -150
- data/test/jsi_json_hashnode_test.rb +0 -132
- data/test/jsi_json_node_test.rb +0 -257
- data/test/jsi_json_pointer_test.rb +0 -102
- data/test/jsi_test.rb +0 -11
- data/test/jsi_typelike_as_json_test.rb +0 -53
- data/test/metaschema_node_test.rb +0 -19
- data/test/schema_module_test.rb +0 -21
- data/test/schema_test.rb +0 -208
- data/test/spreedly_openapi_test.rb +0 -8
- data/test/test_helper.rb +0 -97
- data/test/util_test.rb +0 -62
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JSI
|
|
4
|
+
module Schema::Application::InplaceApplication::Ref
|
|
5
|
+
# @private
|
|
6
|
+
def internal_applicate_ref(instance, visited_refs, throw_done: false, &block)
|
|
7
|
+
if keyword?('$ref') && schema_content['$ref'].respond_to?(:to_str)
|
|
8
|
+
ref = jsi_memoize(:ref) { Schema::Ref.new(schema_content['$ref'], self) }
|
|
9
|
+
unless visited_refs.include?(ref)
|
|
10
|
+
ref.deref_schema.each_inplace_applicator_schema(instance, visited_refs: visited_refs + [ref], &block)
|
|
11
|
+
if throw_done
|
|
12
|
+
throw(:jsi_application_done)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JSI
|
|
4
|
+
module Schema::Application::InplaceApplication::SomeOf
|
|
5
|
+
# @private
|
|
6
|
+
def internal_applicate_someOf(instance, visited_refs, &block)
|
|
7
|
+
if keyword?('allOf') && schema_content['allOf'].respond_to?(:to_ary)
|
|
8
|
+
schema_content['allOf'].each_index do |i|
|
|
9
|
+
subschema(['allOf', i]).each_inplace_applicator_schema(instance, visited_refs: visited_refs, &block)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
if keyword?('anyOf') && schema_content['anyOf'].respond_to?(:to_ary)
|
|
13
|
+
anyOf = schema_content['anyOf'].each_index.map { |i| subschema(['anyOf', i]) }
|
|
14
|
+
validOf = anyOf.select { |schema| schema.instance_valid?(instance) }
|
|
15
|
+
if !validOf.empty?
|
|
16
|
+
applicators = validOf
|
|
17
|
+
else
|
|
18
|
+
# invalid application: if none of the anyOf were valid, we apply them all
|
|
19
|
+
applicators = anyOf
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
applicators.each do |applicator|
|
|
23
|
+
applicator.each_inplace_applicator_schema(instance, visited_refs: visited_refs, &block)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
if keyword?('oneOf') && schema_content['oneOf'].respond_to?(:to_ary)
|
|
27
|
+
oneOf_idxs = schema_content['oneOf'].each_index
|
|
28
|
+
subschema_idx_valid = Hash.new { |h, i| h[i] = subschema(['oneOf', i]).instance_valid?(instance) }
|
|
29
|
+
# count up to 2 `oneOf` subschemas which `instance` validates against
|
|
30
|
+
nvalid = oneOf_idxs.inject(0) { |n, i| n > 1 ? n : subschema_idx_valid[i] ? n + 1 : n }
|
|
31
|
+
if nvalid == 1
|
|
32
|
+
applicator_idxs = oneOf_idxs.select { |i| subschema_idx_valid[i] }
|
|
33
|
+
else
|
|
34
|
+
# invalid application: if none or multiple of the oneOf were valid, we apply them all
|
|
35
|
+
applicator_idxs = oneOf_idxs
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
applicator_idxs.each do |i|
|
|
39
|
+
subschema(['oneOf', i]).each_inplace_applicator_schema(instance, visited_refs: visited_refs, &block)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JSI
|
|
4
|
+
module Schema::Application::InplaceApplication
|
|
5
|
+
autoload :Draft04, 'jsi/schema/application/inplace_application/draft04'
|
|
6
|
+
autoload :Draft06, 'jsi/schema/application/inplace_application/draft06'
|
|
7
|
+
autoload :Draft07, 'jsi/schema/application/inplace_application/draft07'
|
|
8
|
+
|
|
9
|
+
autoload :Ref, 'jsi/schema/application/inplace_application/ref'
|
|
10
|
+
autoload :SomeOf, 'jsi/schema/application/inplace_application/someof'
|
|
11
|
+
autoload :IfThenElse, 'jsi/schema/application/inplace_application/ifthenelse'
|
|
12
|
+
autoload :Dependencies, 'jsi/schema/application/inplace_application/dependencies'
|
|
13
|
+
|
|
14
|
+
# a set of inplace applicator schemas of this schema (from $ref, allOf, etc.) which apply to the
|
|
15
|
+
# given instance.
|
|
16
|
+
#
|
|
17
|
+
# the returned set will contain this schema itself, unless this schema contains a $ref keyword.
|
|
18
|
+
#
|
|
19
|
+
# @param instance [Object] the instance to check any applicators against
|
|
20
|
+
# @return [JSI::SchemaSet] matched applicator schemas
|
|
21
|
+
def inplace_applicator_schemas(instance)
|
|
22
|
+
SchemaSet.new(each_inplace_applicator_schema(instance))
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# yields each inplace applicator schema which applies to the given instance.
|
|
26
|
+
#
|
|
27
|
+
# @param instance (see #inplace_applicator_schemas)
|
|
28
|
+
# @param visited_refs [Enumerable<JSI::Schema::Ref>]
|
|
29
|
+
# @yield [JSI::Schema]
|
|
30
|
+
# @return [nil, Enumerator] an Enumerator if invoked without a block; otherwise nil
|
|
31
|
+
def each_inplace_applicator_schema(instance, visited_refs: [], &block)
|
|
32
|
+
return to_enum(__method__, instance, visited_refs: visited_refs) unless block
|
|
33
|
+
|
|
34
|
+
catch(:jsi_application_done) do
|
|
35
|
+
internal_inplace_applicate_keywords(instance, visited_refs, &block)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
nil
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JSI
|
|
4
|
+
module Schema::Application
|
|
5
|
+
autoload :InplaceApplication, 'jsi/schema/application/inplace_application'
|
|
6
|
+
autoload :ChildApplication, 'jsi/schema/application/child_application'
|
|
7
|
+
|
|
8
|
+
autoload :Draft04, 'jsi/schema/application/draft04'
|
|
9
|
+
autoload :Draft06, 'jsi/schema/application/draft06'
|
|
10
|
+
autoload :Draft07, 'jsi/schema/application/draft07'
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JSI
|
|
4
|
+
module Schema
|
|
5
|
+
module Draft04
|
|
6
|
+
include Schema
|
|
7
|
+
include OldId
|
|
8
|
+
include IdWithAnchor
|
|
9
|
+
include IntegerDisallows0Fraction
|
|
10
|
+
include Schema::Application::Draft04
|
|
11
|
+
include Schema::Validation::Draft04
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JSI
|
|
4
|
+
module Schema
|
|
5
|
+
module Draft06
|
|
6
|
+
include Schema
|
|
7
|
+
include BigMoneyId
|
|
8
|
+
include IdWithAnchor
|
|
9
|
+
include IntegerAllows0Fraction
|
|
10
|
+
include Schema::Application::Draft06
|
|
11
|
+
include Schema::Validation::Draft06
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JSI
|
|
4
|
+
module Schema
|
|
5
|
+
module Draft07
|
|
6
|
+
include Schema
|
|
7
|
+
include BigMoneyId
|
|
8
|
+
include IdWithAnchor
|
|
9
|
+
include IntegerAllows0Fraction
|
|
10
|
+
include Schema::Application::Draft07
|
|
11
|
+
include Schema::Validation::Draft07
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JSI
|
|
4
|
+
module Schema
|
|
5
|
+
Issue = Util::AttrStruct[*%w(
|
|
6
|
+
level
|
|
7
|
+
message
|
|
8
|
+
keyword
|
|
9
|
+
schema
|
|
10
|
+
)]
|
|
11
|
+
|
|
12
|
+
# an issue or problem with a schema.
|
|
13
|
+
#
|
|
14
|
+
# when the `level` is `:error`, the schema is invalid according to its specification,
|
|
15
|
+
# violating some "MUST" or "MUST NOT".
|
|
16
|
+
#
|
|
17
|
+
# when the `level` is `:warning`, the issue does not mean the schema is invalid, but contains something
|
|
18
|
+
# that does not make sense. for example, specifying `additionalItems` without an adjacent `items` has
|
|
19
|
+
# no effect (in specifications which define `additionalItems`), but is not an invalid schema.
|
|
20
|
+
#
|
|
21
|
+
# @!attribute level
|
|
22
|
+
# :error or :warning
|
|
23
|
+
# @return [Symbol]
|
|
24
|
+
# @!attribute message
|
|
25
|
+
# a message describing the issue
|
|
26
|
+
# @return [String]
|
|
27
|
+
# @!attribute keyword
|
|
28
|
+
# the keyword of the schema that has an issue
|
|
29
|
+
# @return [String]
|
|
30
|
+
# @!attribute schema
|
|
31
|
+
# the schema that has an issue
|
|
32
|
+
# @return [JSI::Schema]
|
|
33
|
+
class Issue
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JSI
|
|
4
|
+
# JSI::Schema::Ref is a reference to another schema (the result of #deref_schema), resolved using a ref URI
|
|
5
|
+
# from a ref schema (the ref URI typically the contents of the ref_schema's "$ref" keyword)
|
|
6
|
+
class Schema::Ref
|
|
7
|
+
# @param ref [String] a reference URI
|
|
8
|
+
# @param ref_schema [JSI::Schema] a schema from which the reference originated
|
|
9
|
+
def initialize(ref, ref_schema = nil)
|
|
10
|
+
raise(ArgumentError, "ref is not a string") unless ref.respond_to?(:to_str)
|
|
11
|
+
@ref = ref
|
|
12
|
+
@ref_uri = Addressable::URI.parse(ref)
|
|
13
|
+
@ref_schema = ref_schema ? Schema.ensure_schema(ref_schema) : nil
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
attr_reader :ref
|
|
17
|
+
|
|
18
|
+
attr_reader :ref_uri
|
|
19
|
+
|
|
20
|
+
attr_reader :ref_schema
|
|
21
|
+
|
|
22
|
+
# finds the schema this ref points to
|
|
23
|
+
# @return [JSI::Schema]
|
|
24
|
+
# @raise [JSI::Schema::NotASchemaError] when the thing this ref points to is not a schema
|
|
25
|
+
# @raise [JSI::Schema::ReferenceError] when this reference cannot be resolved
|
|
26
|
+
def deref_schema
|
|
27
|
+
return @deref_schema if instance_variable_defined?(:@deref_schema)
|
|
28
|
+
|
|
29
|
+
schema_resource_root = nil
|
|
30
|
+
check_schema_resource_root = -> {
|
|
31
|
+
unless schema_resource_root
|
|
32
|
+
raise(Schema::ReferenceError, [
|
|
33
|
+
"cannot find schema by ref: #{ref}",
|
|
34
|
+
("from: #{ref_schema.pretty_inspect.chomp}" if ref_schema),
|
|
35
|
+
].compact.join("\n"))
|
|
36
|
+
end
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
ref_uri_nofrag = ref_uri.merge(fragment: nil)
|
|
40
|
+
|
|
41
|
+
if ref_uri_nofrag.empty?
|
|
42
|
+
unless ref_schema
|
|
43
|
+
raise(Schema::ReferenceError, [
|
|
44
|
+
"cannot find schema by ref: #{ref}",
|
|
45
|
+
"with no ref schema",
|
|
46
|
+
].join("\n"))
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# the URI only consists of a fragment (or is empty).
|
|
50
|
+
# for a fragment pointer, resolve using Schema#resource_root_subschema on the ref_schema.
|
|
51
|
+
# for a fragment anchor, bootstrap does not support anchors; otherwise use the ref_schema's schema_resource_root.
|
|
52
|
+
schema_resource_root = ref_schema.is_a?(MetaschemaNode::BootstrapSchema) ? nil : ref_schema.schema_resource_root
|
|
53
|
+
resolve_fragment_ptr = ref_schema.method(:resource_root_subschema)
|
|
54
|
+
else
|
|
55
|
+
# find the schema_resource_root from the non-fragment URI. we will resolve any fragment, either pointer or anchor, from there.
|
|
56
|
+
|
|
57
|
+
if ref_uri_nofrag.absolute?
|
|
58
|
+
ref_abs_uri = ref_uri_nofrag
|
|
59
|
+
elsif ref_schema && ref_schema.jsi_resource_ancestor_uri
|
|
60
|
+
ref_abs_uri = ref_schema.jsi_resource_ancestor_uri.join(ref_uri_nofrag)
|
|
61
|
+
else
|
|
62
|
+
ref_abs_uri = nil
|
|
63
|
+
end
|
|
64
|
+
if ref_abs_uri
|
|
65
|
+
schema_resource_root = JSI.schema_registry.find(ref_abs_uri)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
unless schema_resource_root
|
|
69
|
+
# HAX for how google does refs and ids
|
|
70
|
+
if ref_schema && ref_schema.jsi_document.respond_to?(:to_hash) && ref_schema.jsi_document['schemas'].respond_to?(:to_hash)
|
|
71
|
+
ref_schema.jsi_document['schemas'].each_key do |k|
|
|
72
|
+
if Addressable::URI.parse(ref_schema.jsi_document['schemas'][k]['id']) == ref_uri_nofrag
|
|
73
|
+
schema_resource_root = ref_schema.resource_root_subschema(['schemas', k])
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
check_schema_resource_root.call
|
|
80
|
+
|
|
81
|
+
if schema_resource_root.is_a?(Schema)
|
|
82
|
+
resolve_fragment_ptr = schema_resource_root.method(:resource_root_subschema)
|
|
83
|
+
else
|
|
84
|
+
# Note: Schema#resource_root_subschema will reinstantiate nonschemas as schemas.
|
|
85
|
+
# not implemented for remote refs when the schema_resource_root is not a schema.
|
|
86
|
+
resolve_fragment_ptr = -> (ptr) { schema_resource_root.jsi_descendent_node(ptr) }
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
fragment = ref_uri.fragment
|
|
91
|
+
|
|
92
|
+
if fragment
|
|
93
|
+
begin
|
|
94
|
+
ptr_from_fragment = Ptr.from_fragment(fragment)
|
|
95
|
+
rescue Ptr::PointerSyntaxError
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
if ptr_from_fragment
|
|
100
|
+
begin
|
|
101
|
+
result_schema = resolve_fragment_ptr.call(ptr_from_fragment)
|
|
102
|
+
rescue Ptr::ResolutionError
|
|
103
|
+
raise(Schema::ReferenceError, [
|
|
104
|
+
"could not resolve pointer: #{ptr_from_fragment.pointer.inspect}",
|
|
105
|
+
("from: #{ref_schema.pretty_inspect.chomp}" if ref_schema),
|
|
106
|
+
("in schema resource root: #{schema_resource_root.pretty_inspect.chomp}" if schema_resource_root),
|
|
107
|
+
].compact.join("\n"))
|
|
108
|
+
end
|
|
109
|
+
elsif fragment.nil?
|
|
110
|
+
check_schema_resource_root.call
|
|
111
|
+
result_schema = schema_resource_root
|
|
112
|
+
else
|
|
113
|
+
check_schema_resource_root.call
|
|
114
|
+
|
|
115
|
+
# find an anchor that resembles the fragment
|
|
116
|
+
result_schemas = schema_resource_root.jsi_anchor_subschemas(fragment)
|
|
117
|
+
|
|
118
|
+
if result_schemas.size == 1
|
|
119
|
+
result_schema = result_schemas.first
|
|
120
|
+
elsif result_schemas.size == 0
|
|
121
|
+
raise(Schema::ReferenceError, [
|
|
122
|
+
"could not find schema by fragment: #{fragment.inspect}",
|
|
123
|
+
"in schema resource root: #{schema_resource_root.pretty_inspect.chomp}",
|
|
124
|
+
].join("\n"))
|
|
125
|
+
else
|
|
126
|
+
raise(Schema::ReferenceError, [
|
|
127
|
+
"found multiple schemas for plain name fragment #{fragment.inspect}:",
|
|
128
|
+
*result_schemas.map { |s| s.pretty_inspect.chomp },
|
|
129
|
+
].join("\n"))
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
Schema.ensure_schema(result_schema, msg: "object identified by uri #{ref} is not a schema:")
|
|
134
|
+
return @deref_schema = result_schema
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# @return [String]
|
|
138
|
+
def inspect
|
|
139
|
+
%Q(\#<#{self.class.name} #{ref}>)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
alias_method :to_s, :inspect
|
|
143
|
+
|
|
144
|
+
# pretty-prints a representation of self to the given printer
|
|
145
|
+
# @return [void]
|
|
146
|
+
def pretty_print(q)
|
|
147
|
+
q.text '#<'
|
|
148
|
+
q.text self.class.name
|
|
149
|
+
q.text ' '
|
|
150
|
+
q.text ref
|
|
151
|
+
q.text '>'
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# @private
|
|
155
|
+
def jsi_fingerprint
|
|
156
|
+
{class: self.class, ref: ref, ref_schema: ref_schema}
|
|
157
|
+
end
|
|
158
|
+
include Util::FingerprintHash
|
|
159
|
+
end
|
|
160
|
+
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JSI
|
|
4
|
+
# a node in a document which may contain a schema somewhere within is extended with SchemaAncestorNode, for
|
|
5
|
+
# tracking things necessary for a schema to function correctly
|
|
6
|
+
module Schema::SchemaAncestorNode
|
|
7
|
+
# the base URI used to resolve the ids of schemas at or below this JSI.
|
|
8
|
+
# this is always an absolute URI (with no fragment).
|
|
9
|
+
# this may be the absolute schema URI of a parent schema or the URI from which the document was retrieved.
|
|
10
|
+
# @api private
|
|
11
|
+
# @return [Addressable::URI, nil]
|
|
12
|
+
attr_reader :jsi_schema_base_uri
|
|
13
|
+
|
|
14
|
+
# resources which are ancestors of this JSI in the document. this does not include self.
|
|
15
|
+
# @api private
|
|
16
|
+
# @return [Array<JSI::Schema>]
|
|
17
|
+
def jsi_schema_resource_ancestors
|
|
18
|
+
return @jsi_schema_resource_ancestors if instance_variable_defined?(:@jsi_schema_resource_ancestors)
|
|
19
|
+
Util::EMPTY_ARY
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# the URI of the resource containing this node.
|
|
23
|
+
# this is always an absolute URI (with no fragment).
|
|
24
|
+
# if this node is a schema with an id, this is its absolute URI; otherwise a parent resource's URI,
|
|
25
|
+
# or nil if not contained by a resource with a URI.
|
|
26
|
+
# @return [Addressable::URI, nil]
|
|
27
|
+
def jsi_resource_ancestor_uri
|
|
28
|
+
(is_a?(Schema) && schema_absolute_uri) || jsi_schema_base_uri
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# a schema at or below this node with the given anchor.
|
|
32
|
+
#
|
|
33
|
+
# @return [JSI::Schema, nil]
|
|
34
|
+
def jsi_anchor_subschema(anchor)
|
|
35
|
+
subschemas = jsi_anchor_subschemas_map[anchor: anchor]
|
|
36
|
+
if subschemas.size == 1
|
|
37
|
+
subschemas.first
|
|
38
|
+
else
|
|
39
|
+
nil
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# schemas at or below node with the given anchor.
|
|
44
|
+
#
|
|
45
|
+
# @return [Array<JSI::Schema>]
|
|
46
|
+
def jsi_anchor_subschemas(anchor)
|
|
47
|
+
jsi_anchor_subschemas_map[anchor: anchor]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def jsi_document=(jsi_document)
|
|
53
|
+
@jsi_document = jsi_document
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def jsi_ptr=(jsi_ptr)
|
|
57
|
+
raise(Bug, "jsi_ptr not #{Ptr}: #{jsi_ptr.inspect}") unless jsi_ptr.is_a?(Ptr)
|
|
58
|
+
@jsi_ptr = jsi_ptr
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def jsi_schema_base_uri=(jsi_schema_base_uri)
|
|
62
|
+
if jsi_schema_base_uri
|
|
63
|
+
unless jsi_schema_base_uri.respond_to?(:to_str)
|
|
64
|
+
raise(TypeError, "jsi_schema_base_uri must be string or Addressable::URI; got: #{jsi_schema_base_uri.inspect}")
|
|
65
|
+
end
|
|
66
|
+
@jsi_schema_base_uri = Addressable::URI.parse(jsi_schema_base_uri).freeze
|
|
67
|
+
unless @jsi_schema_base_uri.absolute? && !@jsi_schema_base_uri.fragment
|
|
68
|
+
raise(ArgumentError, "jsi_schema_base_uri must be an absolute URI with no fragment; got: #{jsi_schema_base_uri.inspect}")
|
|
69
|
+
end
|
|
70
|
+
else
|
|
71
|
+
@jsi_schema_base_uri = nil
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def jsi_schema_resource_ancestors=(jsi_schema_resource_ancestors)
|
|
76
|
+
if jsi_schema_resource_ancestors
|
|
77
|
+
unless jsi_schema_resource_ancestors.respond_to?(:to_ary)
|
|
78
|
+
raise(TypeError, "jsi_schema_resource_ancestors must be an array; got: #{jsi_schema_resource_ancestors.inspect}")
|
|
79
|
+
end
|
|
80
|
+
jsi_schema_resource_ancestors.each { |a| Schema.ensure_schema(a) }
|
|
81
|
+
# sanity check the ancestors are in order
|
|
82
|
+
last_anc_ptr = nil
|
|
83
|
+
jsi_schema_resource_ancestors.each do |anc|
|
|
84
|
+
if last_anc_ptr.nil?
|
|
85
|
+
# pass
|
|
86
|
+
elsif last_anc_ptr == anc.jsi_ptr
|
|
87
|
+
raise(Bug, "duplicate ancestors in #{jsi_schema_resource_ancestors.pretty_inspect}")
|
|
88
|
+
elsif !last_anc_ptr.contains?(anc.jsi_ptr)
|
|
89
|
+
raise(Bug, "ancestor ptr #{anc.jsi_ptr} not contained by previous: #{last_anc_ptr} in #{jsi_schema_resource_ancestors.pretty_inspect}")
|
|
90
|
+
end
|
|
91
|
+
if anc.jsi_ptr == jsi_ptr
|
|
92
|
+
raise(Bug, "ancestor is self")
|
|
93
|
+
elsif !anc.jsi_ptr.contains?(jsi_ptr)
|
|
94
|
+
raise(Bug, "ancestor does not contain self")
|
|
95
|
+
end
|
|
96
|
+
last_anc_ptr = anc.jsi_ptr
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
@jsi_schema_resource_ancestors = jsi_schema_resource_ancestors.to_ary.freeze
|
|
100
|
+
else
|
|
101
|
+
@jsi_schema_resource_ancestors = Util::EMPTY_ARY
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def jsi_anchor_subschemas_map
|
|
106
|
+
jsi_memomap(__method__) do |anchor: |
|
|
107
|
+
jsi_each_descendent_node.select do |node|
|
|
108
|
+
node.is_a?(Schema) && node.respond_to?(:anchor) && node.anchor == anchor
|
|
109
|
+
end.freeze
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JSI
|
|
4
|
+
module Schema::Validation::ArrayLength
|
|
5
|
+
# @private
|
|
6
|
+
def internal_validate_maxItems(result_builder)
|
|
7
|
+
if keyword?('maxItems')
|
|
8
|
+
value = schema_content['maxItems']
|
|
9
|
+
# The value of this keyword MUST be a non-negative integer.
|
|
10
|
+
if internal_integer?(value) && value >= 0
|
|
11
|
+
if result_builder.instance.respond_to?(:to_ary)
|
|
12
|
+
# An array instance is valid against "maxItems" if its size is less than, or equal to, the value of this keyword.
|
|
13
|
+
result_builder.validate(
|
|
14
|
+
result_builder.instance.to_ary.size <= value,
|
|
15
|
+
'instance array size is greater than `maxItems` value',
|
|
16
|
+
keyword: 'maxItems',
|
|
17
|
+
)
|
|
18
|
+
end
|
|
19
|
+
else
|
|
20
|
+
result_builder.schema_error('`maxItems` is not a non-negative integer', 'maxItems')
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# @private
|
|
26
|
+
def internal_validate_minItems(result_builder)
|
|
27
|
+
if keyword?('minItems')
|
|
28
|
+
value = schema_content['minItems']
|
|
29
|
+
# The value of this keyword MUST be a non-negative integer.
|
|
30
|
+
if internal_integer?(value) && value >= 0
|
|
31
|
+
if result_builder.instance.respond_to?(:to_ary)
|
|
32
|
+
# An array instance is valid against "minItems" if its size is greater than, or equal to, the value of this keyword.
|
|
33
|
+
result_builder.validate(
|
|
34
|
+
result_builder.instance.to_ary.size >= value,
|
|
35
|
+
'instance array size is less than `minItems` value',
|
|
36
|
+
keyword: 'minItems',
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
else
|
|
40
|
+
result_builder.schema_error('`minItems` is not a non-negative integer', 'minItems')
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
module Schema::Validation::UniqueItems
|
|
46
|
+
# @private
|
|
47
|
+
def internal_validate_uniqueItems(result_builder)
|
|
48
|
+
if keyword?('uniqueItems')
|
|
49
|
+
value = schema_content['uniqueItems']
|
|
50
|
+
# The value of this keyword MUST be a boolean.
|
|
51
|
+
if value == false
|
|
52
|
+
# If this keyword has boolean value false, the instance validates successfully.
|
|
53
|
+
# (noop)
|
|
54
|
+
elsif value == true
|
|
55
|
+
if result_builder.instance.respond_to?(:to_ary)
|
|
56
|
+
# If it has boolean value true, the instance validates successfully if all of its elements are unique.
|
|
57
|
+
result_builder.validate(
|
|
58
|
+
result_builder.instance.uniq.size == result_builder.instance.size,
|
|
59
|
+
"instance array items' uniqueness does not match `uniqueItems` value",
|
|
60
|
+
keyword: 'uniqueItems',
|
|
61
|
+
)
|
|
62
|
+
end
|
|
63
|
+
else
|
|
64
|
+
result_builder.schema_error('`uniqueItems` is not a boolean', 'uniqueItems')
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JSI
|
|
4
|
+
module Schema::Validation::Const
|
|
5
|
+
# @private
|
|
6
|
+
def internal_validate_const(result_builder)
|
|
7
|
+
if keyword?('const')
|
|
8
|
+
value = schema_content['const']
|
|
9
|
+
# The value of this keyword MAY be of any type, including null.
|
|
10
|
+
# An instance validates successfully against this keyword if its value is equal to the value of
|
|
11
|
+
# the keyword.
|
|
12
|
+
result_builder.validate(
|
|
13
|
+
result_builder.instance == value,
|
|
14
|
+
'instance is not equal to `const` value',
|
|
15
|
+
keyword: 'const',
|
|
16
|
+
)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JSI
|
|
4
|
+
module Schema::Validation::Contains
|
|
5
|
+
# @private
|
|
6
|
+
def internal_validate_contains(result_builder)
|
|
7
|
+
if keyword?('contains')
|
|
8
|
+
# An array instance is valid against "contains" if at least one of its elements is valid against
|
|
9
|
+
# the given schema.
|
|
10
|
+
if result_builder.instance.respond_to?(:to_ary)
|
|
11
|
+
results = {}
|
|
12
|
+
result_builder.instance.each_index do |i|
|
|
13
|
+
results[i] = result_builder.child_subschema_validate(['contains'], [i])
|
|
14
|
+
end
|
|
15
|
+
result_builder.validate(
|
|
16
|
+
results.values.any?(&:valid?),
|
|
17
|
+
'instance array does not contain any items valid against `contains` schema value',
|
|
18
|
+
keyword: 'contains',
|
|
19
|
+
results: results.values,
|
|
20
|
+
)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JSI
|
|
4
|
+
module Schema::Validation::Core
|
|
5
|
+
# validates the given instance against this schema
|
|
6
|
+
#
|
|
7
|
+
# @private
|
|
8
|
+
# @param instance_ptr [JSI::Ptr] a pointer to the instance to validate against the schema, in the instance_document
|
|
9
|
+
# @param instance_document [#to_hash, #to_ary, Object] document containing the instance instance_ptr pointer points to
|
|
10
|
+
# @param validate_only [Boolean] whether to return a full schema validation result or a simple, validation-only result
|
|
11
|
+
# @param visited_refs [Enumerable<JSI::Schema::Ref>]
|
|
12
|
+
# @return [JSI::Validation::Result]
|
|
13
|
+
def internal_validate_instance(instance_ptr, instance_document, validate_only: false, visited_refs: [])
|
|
14
|
+
if validate_only
|
|
15
|
+
result = JSI::Validation::VALID
|
|
16
|
+
else
|
|
17
|
+
result = JSI::Validation::FullResult.new
|
|
18
|
+
end
|
|
19
|
+
result_builder = result.builder(self, instance_ptr, instance_document, validate_only, visited_refs)
|
|
20
|
+
|
|
21
|
+
catch(:jsi_validation_result) do
|
|
22
|
+
# note: true/false are not valid as schemas in draft 4; they are only values of
|
|
23
|
+
# additionalProperties / additionalItems. since their behavior is undefined, though,
|
|
24
|
+
# it's fine for them to behave the same as boolean schemas in later drafts.
|
|
25
|
+
# I don't care about draft 4 to implement a different structuring for that.
|
|
26
|
+
if schema_content == true
|
|
27
|
+
# noop
|
|
28
|
+
elsif schema_content == false
|
|
29
|
+
result_builder.validate(false, 'instance is not valid against `false` schema')
|
|
30
|
+
elsif schema_content.respond_to?(:to_hash)
|
|
31
|
+
internal_validate_keywords(result_builder)
|
|
32
|
+
else
|
|
33
|
+
result_builder.schema_error('schema is not a boolean or a JSON object')
|
|
34
|
+
end
|
|
35
|
+
result
|
|
36
|
+
end.freeze
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JSI
|
|
4
|
+
module Schema::Validation::Dependencies
|
|
5
|
+
# @private
|
|
6
|
+
def internal_validate_dependencies(result_builder)
|
|
7
|
+
if keyword?('dependencies')
|
|
8
|
+
value = schema_content['dependencies']
|
|
9
|
+
# This keyword's value MUST be an object. Each property specifies a dependency. Each dependency
|
|
10
|
+
# value MUST be an array or a valid JSON Schema.
|
|
11
|
+
if value.respond_to?(:to_hash)
|
|
12
|
+
value.each_pair do |property_name, dependency|
|
|
13
|
+
if dependency.respond_to?(:to_ary)
|
|
14
|
+
# If the dependency value is an array, each element in the array, if
|
|
15
|
+
# any, MUST be a string, and MUST be unique. If the dependency key is
|
|
16
|
+
# a property in the instance, each of the items in the dependency value
|
|
17
|
+
# must be a property that exists in the instance.
|
|
18
|
+
if result_builder.instance.respond_to?(:to_hash) && result_builder.instance.key?(property_name)
|
|
19
|
+
missing_required = dependency.reject { |name| result_builder.instance.key?(name) }
|
|
20
|
+
# TODO include property_name / missing dependent required property names in the validation error
|
|
21
|
+
result_builder.validate(
|
|
22
|
+
missing_required.empty?,
|
|
23
|
+
'instance object does not contain all dependent required property names specified by `dependencies` value',
|
|
24
|
+
keyword: 'dependencies',
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
else
|
|
28
|
+
# If the dependency value is a subschema, and the dependency key is a
|
|
29
|
+
# property in the instance, the entire instance must validate against
|
|
30
|
+
# the dependency value.
|
|
31
|
+
if result_builder.instance.respond_to?(:to_hash) && result_builder.instance.key?(property_name)
|
|
32
|
+
dependency_result = result_builder.inplace_subschema_validate(['dependencies', property_name])
|
|
33
|
+
# TODO include property_name in the validation error
|
|
34
|
+
result_builder.validate(
|
|
35
|
+
dependency_result.valid?,
|
|
36
|
+
'instance object is not valid against the schema corresponding to a matched property name specified by `dependencies` value',
|
|
37
|
+
keyword: 'dependencies',
|
|
38
|
+
results: [dependency_result],
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
else
|
|
44
|
+
result_builder.schema_error('`dependencies` is not an object', 'dependencies')
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|