jsi 0.2.1 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -0
- data/LICENSE.md +613 -0
- data/README.md +62 -37
- data/jsi.gemspec +8 -12
- data/lib/jsi.rb +11 -0
- data/lib/jsi/base.rb +196 -258
- data/lib/jsi/base/to_rb.rb +2 -0
- data/lib/jsi/jsi_coder.rb +20 -15
- data/lib/jsi/json-schema-fragments.rb +2 -0
- data/lib/jsi/json.rb +2 -0
- data/lib/jsi/json/node.rb +45 -88
- data/lib/jsi/json/pointer.rb +102 -5
- data/lib/jsi/metaschema.rb +7 -0
- data/lib/jsi/metaschema_node.rb +217 -0
- data/lib/jsi/pathed_node.rb +5 -0
- data/lib/jsi/schema.rb +146 -169
- data/lib/jsi/schema_classes.rb +112 -47
- data/lib/jsi/simple_wrap.rb +8 -3
- data/lib/jsi/typelike_modules.rb +31 -39
- data/lib/jsi/util.rb +27 -47
- data/lib/jsi/version.rb +1 -1
- data/lib/schemas/json-schema.org/draft-04/schema.rb +7 -0
- data/lib/schemas/json-schema.org/draft-06/schema.rb +7 -0
- data/resources/icons/AGPL-3.0.png +0 -0
- data/test/base_array_test.rb +174 -60
- data/test/base_hash_test.rb +179 -46
- data/test/base_test.rb +163 -94
- data/test/jsi_coder_test.rb +14 -14
- data/test/jsi_json_arraynode_test.rb +10 -10
- data/test/jsi_json_hashnode_test.rb +14 -14
- data/test/jsi_json_node_test.rb +83 -136
- data/test/jsi_typelike_as_json_test.rb +1 -1
- data/test/metaschema_node_test.rb +19 -0
- data/test/schema_module_test.rb +21 -0
- data/test/schema_test.rb +40 -50
- data/test/test_helper.rb +35 -3
- data/test/util_test.rb +8 -8
- metadata +24 -16
- data/LICENSE.txt +0 -21
@@ -0,0 +1,217 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JSI
|
4
|
+
# a MetaschemaNode is a PathedNode whose node_document contains a metaschema.
|
5
|
+
# as with any PathedNode the node_ptr points to the content of a node.
|
6
|
+
# the root of the metaschema is pointed to by metaschema_root_ptr.
|
7
|
+
# the schema of the root of the document is pointed to by root_schema_ptr.
|
8
|
+
#
|
9
|
+
# like JSI::Base, this class represents an instance of a schema, an instance
|
10
|
+
# which may itself be a schema. unlike JSI::Base, the document containing the
|
11
|
+
# schema and the instance is the same, and a schema may be an instance of itself.
|
12
|
+
#
|
13
|
+
# the document containing the metaschema, its subschemas, and instances of those
|
14
|
+
# subschemas is the node_document.
|
15
|
+
#
|
16
|
+
# the schema instance is the content in the document pointed to by the MetaschemaNode's node_ptr.
|
17
|
+
#
|
18
|
+
# unlike with JSI::Base, the schema is not part of the class, since a metaschema
|
19
|
+
# needs the ability to have its schema be the instance itself.
|
20
|
+
#
|
21
|
+
# if the MetaschemaNode's schema is its self, it will be extended with JSI::Metaschema.
|
22
|
+
#
|
23
|
+
# a MetaschemaNode is extended with JSI::Schema when it represents a schema - this is the case when
|
24
|
+
# its schema is the metaschema.
|
25
|
+
class MetaschemaNode
|
26
|
+
include PathedNode
|
27
|
+
include Memoize
|
28
|
+
|
29
|
+
# not every MetaschemaNode is actually an Enumerable, but it's better to include Enumerable on
|
30
|
+
# the class than to conditionally extend the instance.
|
31
|
+
include Enumerable
|
32
|
+
|
33
|
+
# @param node_document the document containing the metaschema
|
34
|
+
# @param node_ptr [JSI::JSON::Pointer] ptr to this MetaschemaNode in node_document
|
35
|
+
# @param metaschema_root_ptr [JSI::JSON::Pointer] ptr to the root of the metaschema in node_document
|
36
|
+
# @param root_schema_ptr [JSI::JSON::Pointer] ptr to the schema of the root of the node_document
|
37
|
+
def initialize(node_document, node_ptr: JSI::JSON::Pointer[], metaschema_root_ptr: JSI::JSON::Pointer[], root_schema_ptr: JSI::JSON::Pointer[])
|
38
|
+
@node_document = node_document
|
39
|
+
@node_ptr = node_ptr
|
40
|
+
@metaschema_root_ptr = metaschema_root_ptr
|
41
|
+
@root_schema_ptr = root_schema_ptr
|
42
|
+
|
43
|
+
node_content = self.node_content
|
44
|
+
|
45
|
+
if node_content.respond_to?(:to_hash)
|
46
|
+
extend PathedHashNode
|
47
|
+
elsif node_content.respond_to?(:to_ary)
|
48
|
+
extend PathedArrayNode
|
49
|
+
end
|
50
|
+
|
51
|
+
instance_for_schema = node_document
|
52
|
+
schema_ptr = node_ptr.reference_tokens.inject(root_schema_ptr) do |ptr0, tok|
|
53
|
+
if instance_for_schema.respond_to?(:to_ary)
|
54
|
+
ptr1 = ptr0 && ptr0.schema_subschema_ptr_for_index(node_document, tok)
|
55
|
+
else
|
56
|
+
ptr1 = ptr0 && ptr0.schema_subschema_ptr_for_property_name(node_document, tok)
|
57
|
+
end
|
58
|
+
instance_for_schema = instance_for_schema[tok]
|
59
|
+
ptr2 = ptr1 && ptr1.deref(node_document)
|
60
|
+
ptr3 = ptr2 && ptr2.schema_match_ptr_to_instance(node_document, instance_for_schema)
|
61
|
+
ptr3 # only God may judge my variable names
|
62
|
+
end
|
63
|
+
|
64
|
+
@schema = if schema_ptr
|
65
|
+
if schema_ptr == node_ptr
|
66
|
+
self
|
67
|
+
else
|
68
|
+
new_node(node_ptr: schema_ptr)
|
69
|
+
end
|
70
|
+
else
|
71
|
+
nil
|
72
|
+
end
|
73
|
+
|
74
|
+
if schema_ptr
|
75
|
+
if schema_ptr == metaschema_root_ptr
|
76
|
+
extend JSI::Schema
|
77
|
+
end
|
78
|
+
if schema_ptr == node_ptr
|
79
|
+
extend Metaschema
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
if @schema
|
84
|
+
extend(JSI::SchemaClasses.accessor_module_for_schema(@schema, conflicting_modules: [Metaschema, Schema, MetaschemaNode, PathedArrayNode, PathedHashNode]))
|
85
|
+
end
|
86
|
+
|
87
|
+
# workarounds
|
88
|
+
begin # draft 4 boolean schema workaround
|
89
|
+
# in draft 4, boolean schemas are not described in the root, but on anyOf schemas on
|
90
|
+
# properties/additionalProperties and properties/additionalItems.
|
91
|
+
# we need to extend those as DescribesSchema.
|
92
|
+
addtlPropsanyOf = metaschema_root_ptr["properties"]["additionalProperties"]["anyOf"]
|
93
|
+
addtlItemsanyOf = metaschema_root_ptr["properties"]["additionalItems"]["anyOf"]
|
94
|
+
|
95
|
+
if !node_ptr.root? && [addtlPropsanyOf, addtlItemsanyOf].include?(node_ptr.parent)
|
96
|
+
extend JSI::Schema::DescribesSchema
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# document containing the metaschema. see PathedNode#node_document.
|
102
|
+
attr_reader :node_document
|
103
|
+
# ptr to this metaschema node. see PathedNode#node_ptr.
|
104
|
+
attr_reader :node_ptr
|
105
|
+
# ptr to the root of the metaschema in the node_document
|
106
|
+
attr_reader :metaschema_root_ptr
|
107
|
+
# ptr to the schema of the root of the node_document
|
108
|
+
attr_reader :root_schema_ptr
|
109
|
+
# a JSI::Schema describing this MetaschemaNode
|
110
|
+
attr_reader :schema
|
111
|
+
|
112
|
+
# @return [MetaschemaNode] document root MetaschemaNode
|
113
|
+
def document_root_node
|
114
|
+
new_node(node_ptr: JSI::JSON::Pointer[])
|
115
|
+
end
|
116
|
+
|
117
|
+
# @return [MetaschemaNode] parent MetaschemaNode
|
118
|
+
def parent_node
|
119
|
+
new_node(node_ptr: node_ptr.parent)
|
120
|
+
end
|
121
|
+
|
122
|
+
# @param token [String, Integer, Object] the token to subscript
|
123
|
+
# @return [MetaschemaNode, Object] the node content's subscript value at the given token.
|
124
|
+
# if there is a subschema defined for that token on this MetaschemaNode's schema,
|
125
|
+
# returns that value as a MetaschemaNode instantiation of that subschema.
|
126
|
+
def [](token)
|
127
|
+
if respond_to?(:to_hash)
|
128
|
+
token_in_range = node_content_hash_pubsend(:key?, token)
|
129
|
+
value = node_content_hash_pubsend(:[], token)
|
130
|
+
elsif respond_to?(:to_ary)
|
131
|
+
token_in_range = node_content_ary_pubsend(:each_index).include?(token)
|
132
|
+
value = node_content_ary_pubsend(:[], token)
|
133
|
+
else
|
134
|
+
raise(NoMethodError, "cannot subcript (using token: #{token.inspect}) from content: #{node_content.pretty_inspect.chomp}")
|
135
|
+
end
|
136
|
+
|
137
|
+
jsi_memoize(:[], token, value, token_in_range) do |token, value, token_in_range|
|
138
|
+
if token_in_range
|
139
|
+
value_node = new_node(node_ptr: node_ptr[token])
|
140
|
+
|
141
|
+
if value_node.is_a?(Schema) || value.respond_to?(:to_hash) || value.respond_to?(:to_ary)
|
142
|
+
value_node
|
143
|
+
else
|
144
|
+
value
|
145
|
+
end
|
146
|
+
else
|
147
|
+
# I think I will not support Hash#default/#default_proc in this case.
|
148
|
+
nil
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
# if this MetaschemaNode is a $ref then the $ref is followed. otherwise this MetaschemaNode is returned.
|
154
|
+
# @return [MetaschemaNode]
|
155
|
+
def deref(&block)
|
156
|
+
node_ptr_deref do |deref_ptr|
|
157
|
+
return new_node(node_ptr: deref_ptr).tap(&(block || Util::NOOP))
|
158
|
+
end
|
159
|
+
return self
|
160
|
+
end
|
161
|
+
|
162
|
+
# @yield [Object] the node content of the instance. the block should result
|
163
|
+
# in a (nondestructively) modified copy of this.
|
164
|
+
# @return [MetaschemaNode] modified copy of self
|
165
|
+
def modified_copy(&block)
|
166
|
+
MetaschemaNode.new(node_ptr.modified_document_copy(node_document, &block), our_initialize_params)
|
167
|
+
end
|
168
|
+
|
169
|
+
# @return [String]
|
170
|
+
def inspect
|
171
|
+
"\#<#{object_group_text.join(' ')} #{node_content.inspect}>"
|
172
|
+
end
|
173
|
+
|
174
|
+
def pretty_print(q)
|
175
|
+
q.text '#<'
|
176
|
+
q.text object_group_text.join(' ')
|
177
|
+
q.group_sub {
|
178
|
+
q.nest(2) {
|
179
|
+
q.breakable ' '
|
180
|
+
q.pp node_content
|
181
|
+
}
|
182
|
+
}
|
183
|
+
q.breakable ''
|
184
|
+
q.text '>'
|
185
|
+
end
|
186
|
+
|
187
|
+
# @return [Array<String>]
|
188
|
+
def object_group_text
|
189
|
+
if schema
|
190
|
+
class_n_schema = "#{self.class} (#{schema.node_ptr.fragment})"
|
191
|
+
else
|
192
|
+
class_n_schema = self.class.to_s
|
193
|
+
end
|
194
|
+
[
|
195
|
+
class_n_schema,
|
196
|
+
is_a?(Metaschema) ? "Metaschema" : is_a?(Schema) ? "Schema" : nil,
|
197
|
+
*(node_content.respond_to?(:object_group_text) ? node_content.object_group_text : []),
|
198
|
+
].compact
|
199
|
+
end
|
200
|
+
|
201
|
+
# @return [Object] an opaque fingerprint of this MetaschemaNode for FingerprintHash
|
202
|
+
def jsi_fingerprint
|
203
|
+
{class: self.class, node_document: node_document}.merge(our_initialize_params)
|
204
|
+
end
|
205
|
+
include FingerprintHash
|
206
|
+
|
207
|
+
private
|
208
|
+
|
209
|
+
def our_initialize_params
|
210
|
+
{node_ptr: node_ptr, metaschema_root_ptr: metaschema_root_ptr, root_schema_ptr: root_schema_ptr}
|
211
|
+
end
|
212
|
+
|
213
|
+
def new_node(params)
|
214
|
+
MetaschemaNode.new(node_document, our_initialize_params.merge(params))
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
data/lib/jsi/pathed_node.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module JSI
|
2
4
|
# including class MUST define
|
3
5
|
# - #node_document [Object] returning the document
|
@@ -10,11 +12,14 @@ module JSI
|
|
10
12
|
#
|
11
13
|
# the node content (#node_content) is the result of evaluating the node document at the path.
|
12
14
|
module PathedNode
|
15
|
+
# @return [Object] the content of this node
|
13
16
|
def node_content
|
14
17
|
content = node_ptr.evaluate(node_document)
|
15
18
|
content
|
16
19
|
end
|
17
20
|
|
21
|
+
# @yield [JSI::JSON::Pointer] if a block is given (optional), this will yield a deref'd pointer
|
22
|
+
# @return [JSI::JSON::Pointer] our node_ptr, derefed against our node_document
|
18
23
|
def node_ptr_deref(&block)
|
19
24
|
node_ptr.deref(node_document, &block)
|
20
25
|
end
|
data/lib/jsi/schema.rb
CHANGED
@@ -1,80 +1,101 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module JSI
|
4
4
|
# JSI::Schema represents a JSON Schema. initialized from a Hash-like schema
|
5
5
|
# object, JSI::Schema is a relatively simple class to abstract useful methods
|
6
6
|
# applied to a JSON Schema.
|
7
|
-
|
7
|
+
module Schema
|
8
|
+
class Error < StandardError
|
9
|
+
end
|
10
|
+
|
11
|
+
# an exception raised when a thing is expected to be a JSI::Schema, but is not
|
12
|
+
class NotASchemaError < Error
|
13
|
+
end
|
14
|
+
|
8
15
|
include Memoize
|
9
16
|
|
17
|
+
# JSI::Schema::DescribesSchema: a schema which describes another schema. this module
|
18
|
+
# extends a JSI::Schema instance and indicates that JSIs which instantiate the schema
|
19
|
+
# are themselves also schemas.
|
20
|
+
#
|
21
|
+
# examples of a schema which describes a schema include the draft JSON Schema metaschemas and
|
22
|
+
# the OpenAPI schema definition which describes "A deterministic version of a JSON Schema object."
|
23
|
+
module DescribesSchema
|
24
|
+
end
|
25
|
+
|
10
26
|
class << self
|
27
|
+
# @return [JSI::Schema] the default metaschema
|
28
|
+
def default_metaschema
|
29
|
+
JSI::JSONSchemaOrgDraft06.schema
|
30
|
+
end
|
31
|
+
|
32
|
+
# @return [Array<JSI::Schema>] supported metaschemas
|
33
|
+
def supported_metaschemas
|
34
|
+
[
|
35
|
+
JSI::JSONSchemaOrgDraft04.schema,
|
36
|
+
JSI::JSONSchemaOrgDraft06.schema,
|
37
|
+
]
|
38
|
+
end
|
39
|
+
|
40
|
+
# instantiates a given schema object as a JSI::Schema.
|
41
|
+
#
|
42
|
+
# schemas are instantiated according to their '$schema' property if specified. otherwise their schema
|
43
|
+
# will be the {JSI::Schema.default_metaschema}.
|
44
|
+
#
|
45
|
+
# if the given schema_object is a JSI::Base but not already a JSI::Schema, an error
|
46
|
+
# will be raised. JSI::Base _should_ already extend a given instance with JSI::Schema
|
47
|
+
# when its schema describes a schema (by extending with JSI::Schema::DescribesSchema).
|
48
|
+
#
|
11
49
|
# @param schema_object [#to_hash, Boolean, JSI::Schema] an object to be instantiated as a schema.
|
12
50
|
# if it's already a schema, it is returned as-is.
|
13
|
-
# @return [JSI::Schema]
|
51
|
+
# @return [JSI::Schema] a JSI::Schema representing the given schema_object
|
14
52
|
def from_object(schema_object)
|
15
53
|
if schema_object.is_a?(Schema)
|
16
54
|
schema_object
|
55
|
+
elsif schema_object.is_a?(JSI::Base)
|
56
|
+
raise(NotASchemaError, "the given schema_object is a JSI::Base, but is not a JSI::Schema: #{schema_object.pretty_inspect.chomp}")
|
57
|
+
elsif schema_object.respond_to?(:to_hash)
|
58
|
+
schema_object = JSI.deep_stringify_symbol_keys(schema_object)
|
59
|
+
if schema_object.key?('$schema') && schema_object['$schema'].respond_to?(:to_str)
|
60
|
+
if schema_object['$schema'] == schema_object['$id'] || schema_object['$schema'] == schema_object['id']
|
61
|
+
MetaschemaNode.new(schema_object)
|
62
|
+
else
|
63
|
+
metaschema = supported_metaschemas.detect { |ms| schema_object['$schema'] == ms['$id'] || schema_object['$schema'] == ms['id'] }
|
64
|
+
unless metaschema
|
65
|
+
raise(NotImplementedError, "metaschema not supported: #{schema_object['$schema']}")
|
66
|
+
end
|
67
|
+
metaschema.new_jsi(schema_object)
|
68
|
+
end
|
69
|
+
else
|
70
|
+
default_metaschema.new_jsi(schema_object)
|
71
|
+
end
|
72
|
+
elsif [true, false].include?(schema_object)
|
73
|
+
default_metaschema.new_jsi(schema_object)
|
17
74
|
else
|
18
|
-
|
75
|
+
raise(TypeError, "cannot instantiate Schema from: #{schema_object.pretty_inspect.chomp}")
|
19
76
|
end
|
20
77
|
end
|
21
|
-
end
|
22
|
-
|
23
|
-
# initializes a schema from a given JSI::Base, JSI::JSON::Node, or hash. Boolean schemas are
|
24
|
-
# instantiated as their equivalent hash ({} for true and {"not" => {}} for false).
|
25
|
-
#
|
26
|
-
# @param schema_object [JSI::Base, #to_hash, Boolean] the schema
|
27
|
-
def initialize(schema_object)
|
28
|
-
if schema_object.is_a?(JSI::Schema)
|
29
|
-
raise(TypeError, "will not instantiate Schema from another Schema: #{schema_object.pretty_inspect.chomp}")
|
30
|
-
elsif schema_object.is_a?(JSI::PathedNode)
|
31
|
-
@schema_node = JSI.deep_stringify_symbol_keys(schema_object.deref)
|
32
|
-
elsif schema_object.respond_to?(:to_hash)
|
33
|
-
@schema_node = JSI::JSON::Node.new_doc(JSI.deep_stringify_symbol_keys(schema_object))
|
34
|
-
elsif schema_object == true
|
35
|
-
@schema_node = JSI::JSON::Node.new_doc({})
|
36
|
-
elsif schema_object == false
|
37
|
-
@schema_node = JSI::JSON::Node.new_doc({"not" => {}})
|
38
|
-
else
|
39
|
-
raise(TypeError, "cannot instantiate Schema from: #{schema_object.pretty_inspect.chomp}")
|
40
|
-
end
|
41
|
-
end
|
42
78
|
|
43
|
-
|
44
|
-
attr_reader :schema_node
|
45
|
-
|
46
|
-
alias_method :schema_object, :schema_node
|
47
|
-
|
48
|
-
# @return [JSI::Base, JSI::JSON::Node, Object] property value from the schema_object
|
49
|
-
# @param property_name [String, Object] property name to access from the schema_object
|
50
|
-
def [](property_name)
|
51
|
-
schema_object[property_name]
|
79
|
+
alias_method :new, :from_object
|
52
80
|
end
|
53
81
|
|
54
|
-
# @return [String] an absolute id for the schema, with a json pointer fragment
|
82
|
+
# @return [String, nil] an absolute id for the schema, with a json pointer fragment. nil if
|
83
|
+
# no parent of this schema defines an id.
|
55
84
|
def schema_id
|
56
|
-
@schema_id
|
57
|
-
|
85
|
+
return @schema_id if instance_variable_defined?(:@schema_id)
|
86
|
+
@schema_id = begin
|
87
|
+
# start from self and ascend parents looking for an 'id' property.
|
58
88
|
# append a fragment to that id (appending to an existing fragment if there
|
59
89
|
# is one) consisting of the path from that parent to our schema_node.
|
60
|
-
node_for_id =
|
90
|
+
node_for_id = self
|
61
91
|
path_from_id_node = []
|
62
92
|
done = false
|
63
93
|
|
64
94
|
while !done
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
if node_for_id.node_ptr.root? || node_for_id.object_id == schema_node.object_id
|
70
|
-
# I'm only looking at 'id' for the document root and the schema node
|
71
|
-
# until I track what parents are schemas.
|
72
|
-
parent_id = node_for_id['$id'] || node_for_id['id']
|
73
|
-
else
|
74
|
-
# will look at '$id' everywhere since it is less likely to show up outside schemas than
|
75
|
-
# 'id', but it will be better to only look at parents that are schemas for this too.
|
76
|
-
parent_id = node_for_id['$id']
|
77
|
-
end
|
95
|
+
node_content_for_id = node_for_id.node_content
|
96
|
+
if node_for_id.is_a?(JSI::Schema) && node_content_for_id.respond_to?(:to_hash)
|
97
|
+
parent_id = node_content_for_id.key?('$id') && node_content_for_id['$id'].respond_to?(:to_str) ? node_content_for_id['$id'].to_str :
|
98
|
+
node_content_for_id.key?('id') && node_content_for_id['id'].respond_to?(:to_str) ? node_content_for_id['id'].to_str : nil
|
78
99
|
end
|
79
100
|
|
80
101
|
if parent_id || node_for_id.node_ptr.root?
|
@@ -86,40 +107,45 @@ module JSI
|
|
86
107
|
end
|
87
108
|
if parent_id
|
88
109
|
parent_auri = Addressable::URI.parse(parent_id)
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
# add onto the fragment
|
97
|
-
parent_id_path = JSI::JSON::Pointer.from_fragment('#' + parent_auri.fragment).reference_tokens
|
98
|
-
path_from_id_node = parent_id_path + path_from_id_node
|
99
|
-
parent_auri.fragment = nil
|
100
|
-
#else: no fragment so parent_id good as is
|
101
|
-
end
|
110
|
+
if parent_auri.fragment
|
111
|
+
# add onto the fragment
|
112
|
+
parent_id_path = JSI::JSON::Pointer.from_fragment('#' + parent_auri.fragment).reference_tokens
|
113
|
+
path_from_id_node = parent_id_path + path_from_id_node
|
114
|
+
parent_auri.fragment = nil
|
115
|
+
#else: no fragment so parent_id good as is
|
116
|
+
end
|
102
117
|
|
103
|
-
|
104
|
-
|
118
|
+
fragment = JSI::JSON::Pointer.new(path_from_id_node).fragment
|
119
|
+
schema_id = parent_auri.to_s + fragment
|
105
120
|
|
106
|
-
|
121
|
+
schema_id
|
122
|
+
else
|
123
|
+
nil
|
124
|
+
end
|
107
125
|
end
|
108
126
|
end
|
109
127
|
|
128
|
+
# @return [Module] a module representing this schema. see {JSI::SchemaClasses.module_for_schema}.
|
129
|
+
def jsi_schema_module
|
130
|
+
JSI::SchemaClasses.module_for_schema(self)
|
131
|
+
end
|
132
|
+
|
110
133
|
# @return [Class subclassing JSI::Base] shortcut for JSI.class_for_schema(schema)
|
111
134
|
def jsi_schema_class
|
112
135
|
JSI.class_for_schema(self)
|
113
136
|
end
|
114
137
|
|
115
|
-
alias_method :schema_class, :jsi_schema_class
|
116
|
-
|
117
138
|
# calls #new on the class for this schema with the given arguments. for parameters,
|
118
139
|
# see JSI::Base#initialize documentation.
|
119
140
|
#
|
120
141
|
# @return [JSI::Base] a JSI whose schema is this schema and whose instance is the given instance
|
121
142
|
def new_jsi(other_instance, *a, &b)
|
122
|
-
|
143
|
+
JSI.class_for_schema(match_to_instance(other_instance)).new(other_instance, *a, &b)
|
144
|
+
end
|
145
|
+
|
146
|
+
# @return [Boolean] does this schema itself describe a schema?
|
147
|
+
def describes_schema?
|
148
|
+
is_a?(JSI::Schema::DescribesSchema)
|
123
149
|
end
|
124
150
|
|
125
151
|
# if this schema is a oneOf, allOf, anyOf schema, #match_to_instance finds
|
@@ -129,60 +155,42 @@ module JSI
|
|
129
155
|
# @param other_instance [Object] the instance to which to attempt to match *Of subschemas
|
130
156
|
# @return [JSI::Schema] a matched subschema, or this schema (self)
|
131
157
|
def match_to_instance(other_instance)
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
return someof_schema.match_to_instance(other_instance)
|
140
|
-
end
|
141
|
-
end
|
158
|
+
ptr = node_ptr
|
159
|
+
ptr = ptr.deref(node_document)
|
160
|
+
ptr = ptr.schema_match_ptr_to_instance(node_document, other_instance)
|
161
|
+
if ptr
|
162
|
+
ptr.evaluate(document_root_node).tap { |subschema| jsi_ensure_subschema_is_schema(subschema, ptr) }
|
163
|
+
else
|
164
|
+
self
|
142
165
|
end
|
143
|
-
return self
|
144
166
|
end
|
145
167
|
|
146
|
-
# @param
|
147
|
-
# @return [JSI::Schema, nil] a subschema from `properties`,
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
168
|
+
# @param property_name [String] the property name for which to find a subschema
|
169
|
+
# @return [JSI::Schema, nil] a subschema from `properties`, `patternProperties`, or `additionalProperties` for the given token
|
170
|
+
def subschema_for_property(property_name)
|
171
|
+
jsi_memoize(:subschema_for_property, property_name) do |property_name|
|
172
|
+
ptr = node_ptr
|
173
|
+
ptr = ptr.deref(node_document)
|
174
|
+
ptr = ptr.schema_subschema_ptr_for_property_name(node_document, property_name)
|
175
|
+
if ptr
|
176
|
+
ptr = ptr.deref(node_document)
|
177
|
+
ptr.evaluate(document_root_node).tap { |subschema| jsi_ensure_subschema_is_schema(subschema, ptr) }
|
154
178
|
else
|
155
|
-
|
156
|
-
_, pattern_schema_object = schema_object['patternProperties'].detect do |pattern, _|
|
157
|
-
property_name.to_s =~ Regexp.new(pattern) # TODO map pattern to ruby syntax
|
158
|
-
end
|
159
|
-
end
|
160
|
-
if pattern_schema_object
|
161
|
-
self.class.new(pattern_schema_object)
|
162
|
-
else
|
163
|
-
if schema_object['additionalProperties'].respond_to?(:to_hash)
|
164
|
-
self.class.new(schema_object['additionalProperties'])
|
165
|
-
else
|
166
|
-
nil
|
167
|
-
end
|
168
|
-
end
|
179
|
+
nil
|
169
180
|
end
|
170
181
|
end
|
171
182
|
end
|
172
183
|
|
173
|
-
# @param
|
174
|
-
# @return [JSI::Schema, nil] a subschema from `items` or
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
end
|
184
|
-
elsif schema_object['items'].respond_to?(:to_hash)
|
185
|
-
self.class.new(schema_object['items'])
|
184
|
+
# @param index [Integer] the array index for which to find a subschema
|
185
|
+
# @return [JSI::Schema, nil] a subschema from `items` or `additionalItems` for the given token
|
186
|
+
def subschema_for_index(index)
|
187
|
+
jsi_memoize(:subschema_for_index, index) do |index|
|
188
|
+
ptr = node_ptr
|
189
|
+
ptr = ptr.deref(node_document)
|
190
|
+
ptr = ptr.schema_subschema_ptr_for_index(node_document, index)
|
191
|
+
if ptr
|
192
|
+
ptr = ptr.deref(node_document)
|
193
|
+
ptr.evaluate(document_root_node).tap { |subschema| jsi_ensure_subschema_is_schema(subschema, ptr) }
|
186
194
|
else
|
187
195
|
nil
|
188
196
|
end
|
@@ -195,33 +203,33 @@ module JSI
|
|
195
203
|
# "required" property keys. if this schema has allOf subschemas, those
|
196
204
|
# schemas are checked (recursively) for their described object property names.
|
197
205
|
def described_object_property_names
|
198
|
-
|
206
|
+
jsi_memoize(:described_object_property_names) do
|
199
207
|
Set.new.tap do |property_names|
|
200
|
-
if
|
201
|
-
property_names.merge(
|
208
|
+
if node_content.respond_to?(:to_hash) && node_content['properties'].respond_to?(:to_hash)
|
209
|
+
property_names.merge(node_content['properties'].keys)
|
202
210
|
end
|
203
|
-
if
|
204
|
-
property_names.merge(
|
211
|
+
if node_content.respond_to?(:to_hash) && node_content['required'].respond_to?(:to_ary)
|
212
|
+
property_names.merge(node_content['required'].to_ary)
|
205
213
|
end
|
206
214
|
# we should look at dependencies (TODO).
|
207
|
-
if
|
208
|
-
|
209
|
-
property_names.merge(
|
215
|
+
if respond_to?(:to_hash) && self['allOf'].respond_to?(:to_ary)
|
216
|
+
self['allOf'].select{ |s| s.is_a?(JSI::Schema) }.map(&:deref).map do |allOf_schema|
|
217
|
+
property_names.merge(allOf_schema.described_object_property_names)
|
210
218
|
end
|
211
219
|
end
|
212
220
|
end
|
213
221
|
end
|
214
222
|
end
|
215
223
|
|
216
|
-
# @return [Array
|
224
|
+
# @return [Array] array of schema validation errors for
|
217
225
|
# the given instance against this schema
|
218
|
-
def fully_validate_instance(other_instance)
|
219
|
-
::JSON::Validator.fully_validate(JSI::Typelike.as_json(
|
226
|
+
def fully_validate_instance(other_instance, errors_as_objects: false)
|
227
|
+
::JSON::Validator.fully_validate(JSI::Typelike.as_json(node_document), JSI::Typelike.as_json(other_instance), fragment: node_ptr.fragment, errors_as_objects: errors_as_objects)
|
220
228
|
end
|
221
229
|
|
222
230
|
# @return [true, false] whether the given instance validates against this schema
|
223
231
|
def validate_instance(other_instance)
|
224
|
-
::JSON::Validator.validate(JSI::Typelike.as_json(
|
232
|
+
::JSON::Validator.validate(JSI::Typelike.as_json(node_document), JSI::Typelike.as_json(other_instance), fragment: node_ptr.fragment)
|
225
233
|
end
|
226
234
|
|
227
235
|
# @return [true] if this method does not raise, it returns true to
|
@@ -229,19 +237,19 @@ module JSI
|
|
229
237
|
# @raise [::JSON::Schema::ValidationError] raises if the instance has
|
230
238
|
# validation errors against this schema
|
231
239
|
def validate_instance!(other_instance)
|
232
|
-
::JSON::Validator.validate!(JSI::Typelike.as_json(
|
240
|
+
::JSON::Validator.validate!(JSI::Typelike.as_json(node_document), JSI::Typelike.as_json(other_instance), fragment: node_ptr.fragment)
|
233
241
|
end
|
234
242
|
|
235
|
-
# @return [Array
|
243
|
+
# @return [Array] array of schema validation errors for
|
236
244
|
# this schema, validated against its metaschema. a default metaschema
|
237
245
|
# is assumed if the schema does not specify a $schema.
|
238
|
-
def fully_validate_schema
|
239
|
-
::JSON::Validator.fully_validate(JSI::Typelike.as_json(
|
246
|
+
def fully_validate_schema(errors_as_objects: false)
|
247
|
+
::JSON::Validator.fully_validate(JSI::Typelike.as_json(node_document), [], fragment: node_ptr.fragment, validate_schema: true, list: true, errors_as_objects: errors_as_objects)
|
240
248
|
end
|
241
249
|
|
242
250
|
# @return [true, false] whether this schema validates against its metaschema
|
243
251
|
def validate_schema
|
244
|
-
::JSON::Validator.validate(JSI::Typelike.as_json(
|
252
|
+
::JSON::Validator.validate(JSI::Typelike.as_json(node_document), [], fragment: node_ptr.fragment, validate_schema: true, list: true)
|
245
253
|
end
|
246
254
|
|
247
255
|
# @return [true] if this method does not raise, it returns true to
|
@@ -249,45 +257,14 @@ module JSI
|
|
249
257
|
# @raise [::JSON::Schema::ValidationError] raises if this schema has
|
250
258
|
# validation errors against its metaschema
|
251
259
|
def validate_schema!
|
252
|
-
::JSON::Validator.validate!(JSI::Typelike.as_json(
|
253
|
-
end
|
254
|
-
|
255
|
-
# @return [String] a string for #instance and #pretty_print including the schema_id
|
256
|
-
def object_group_text
|
257
|
-
"schema_id=#{schema_id}"
|
258
|
-
end
|
259
|
-
|
260
|
-
# @return [String] a string representing this Schema
|
261
|
-
def inspect
|
262
|
-
"\#<#{self.class.inspect} #{object_group_text} #{schema_object.inspect}>"
|
260
|
+
::JSON::Validator.validate!(JSI::Typelike.as_json(node_document), [], fragment: node_ptr.fragment, validate_schema: true, list: true)
|
263
261
|
end
|
264
|
-
alias_method :to_s, :inspect
|
265
262
|
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
text "\#<#{obj.class.inspect} #{obj.object_group_text}"
|
271
|
-
group_sub {
|
272
|
-
nest(2) {
|
273
|
-
breakable ' '
|
274
|
-
pp obj.schema_object
|
275
|
-
}
|
276
|
-
}
|
277
|
-
breakable ''
|
278
|
-
text '>'
|
263
|
+
private
|
264
|
+
def jsi_ensure_subschema_is_schema(subschema, ptr)
|
265
|
+
unless subschema.is_a?(JSI::Schema)
|
266
|
+
raise(NotASchemaError, "subschema not a schema at ptr #{ptr.inspect}: #{subschema.pretty_inspect.chomp}")
|
279
267
|
end
|
280
268
|
end
|
281
|
-
|
282
|
-
# @return [Object] returns a jsonifiable representation of this schema
|
283
|
-
def as_json(*opt)
|
284
|
-
Typelike.as_json(schema_object, *opt)
|
285
|
-
end
|
286
|
-
|
287
|
-
# @return [Object] an opaque fingerprint of this Schema for FingerprintHash
|
288
|
-
def fingerprint
|
289
|
-
{class: self.class, schema_ptr: schema_node.node_ptr, schema_document: JSI::Typelike.as_json(schema_node.node_document)}
|
290
|
-
end
|
291
|
-
include FingerprintHash
|
292
269
|
end
|
293
270
|
end
|