jsi 0.2.1 → 0.3.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/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
|