jsi-dev 0.0.0.pre.commonmarker
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.yardopts +8 -0
- data/CHANGELOG.md +101 -0
- data/LICENSE.md +613 -0
- data/README.md +303 -0
- data/docs/glossary.md +281 -0
- data/jsi.gemspec +30 -0
- data/lib/jsi/base/node.rb +373 -0
- data/lib/jsi/base.rb +738 -0
- data/lib/jsi/jsi_coder.rb +92 -0
- data/lib/jsi/metaschema.rb +6 -0
- data/lib/jsi/metaschema_node/bootstrap_schema.rb +126 -0
- data/lib/jsi/metaschema_node.rb +262 -0
- data/lib/jsi/ptr.rb +314 -0
- data/lib/jsi/schema/application/child_application/contains.rb +25 -0
- data/lib/jsi/schema/application/child_application/draft04.rb +21 -0
- data/lib/jsi/schema/application/child_application/draft06.rb +28 -0
- data/lib/jsi/schema/application/child_application/draft07.rb +28 -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 +13 -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 +25 -0
- data/lib/jsi/schema/application/inplace_application/draft06.rb +26 -0
- data/lib/jsi/schema/application/inplace_application/draft07.rb +32 -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 +14 -0
- data/lib/jsi/schema/application.rb +12 -0
- data/lib/jsi/schema/draft04.rb +13 -0
- data/lib/jsi/schema/draft06.rb +13 -0
- data/lib/jsi/schema/draft07.rb +13 -0
- data/lib/jsi/schema/issue.rb +36 -0
- data/lib/jsi/schema/ref.rb +183 -0
- data/lib/jsi/schema/schema_ancestor_node.rb +122 -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/dependencies.rb +49 -0
- data/lib/jsi/schema/validation/draft04/minmax.rb +91 -0
- data/lib/jsi/schema/validation/draft04.rb +110 -0
- data/lib/jsi/schema/validation/draft06.rb +120 -0
- data/lib/jsi/schema/validation/draft07.rb +157 -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 +49 -0
- data/lib/jsi/schema.rb +792 -0
- data/lib/jsi/schema_classes.rb +357 -0
- data/lib/jsi/schema_registry.rb +190 -0
- data/lib/jsi/schema_set.rb +219 -0
- data/lib/jsi/simple_wrap.rb +26 -0
- data/lib/jsi/util/private/attr_struct.rb +130 -0
- data/lib/jsi/util/private/memo_map.rb +75 -0
- data/lib/jsi/util/private.rb +202 -0
- data/lib/jsi/util/typelike.rb +225 -0
- data/lib/jsi/util.rb +227 -0
- data/lib/jsi/validation/error.rb +34 -0
- data/lib/jsi/validation/result.rb +212 -0
- data/lib/jsi/validation.rb +15 -0
- data/lib/jsi/version.rb +5 -0
- data/lib/jsi.rb +105 -0
- data/lib/schemas/json-schema.org/draft-04/schema.rb +169 -0
- data/lib/schemas/json-schema.org/draft-06/schema.rb +171 -0
- data/lib/schemas/json-schema.org/draft-07/schema.rb +198 -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 +155 -0
@@ -0,0 +1,225 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JSI
|
4
|
+
# a module of methods for objects which behave like Hash but are not Hash.
|
5
|
+
#
|
6
|
+
# this module is intended to be internal to JSI. no guarantees or API promises
|
7
|
+
# are made for non-JSI classes including this module.
|
8
|
+
module Util::Hashlike
|
9
|
+
# safe methods which can be delegated to #to_hash (which the includer is assumed to have defined).
|
10
|
+
# 'safe' means, in this context, nondestructive - methods which do not modify the receiver.
|
11
|
+
|
12
|
+
# methods which do not need to access the value.
|
13
|
+
SAFE_KEY_ONLY_METHODS = %w(each_key empty? has_key? include? key? keys length member? size).map(&:freeze).freeze
|
14
|
+
SAFE_KEY_VALUE_METHODS = %w(< <= > >= any? assoc compact dig each_pair each_value fetch fetch_values has_value? invert key merge rassoc reject select to_h to_proc transform_values value? values values_at).map(&:freeze).freeze
|
15
|
+
DESTRUCTIVE_METHODS = %w(clear delete delete_if keep_if reject! replace select! shift).map(&:freeze).freeze
|
16
|
+
# these return a modified copy
|
17
|
+
safe_modified_copy_methods = %w(compact)
|
18
|
+
# select and reject will return a modified copy but need the yielded block variable value from #[]
|
19
|
+
safe_kv_block_modified_copy_methods = %w(select reject)
|
20
|
+
SAFE_METHODS = SAFE_KEY_ONLY_METHODS | SAFE_KEY_VALUE_METHODS
|
21
|
+
custom_methods = %w(merge) # defined below
|
22
|
+
safe_to_hash_methods = SAFE_METHODS -
|
23
|
+
safe_modified_copy_methods -
|
24
|
+
safe_kv_block_modified_copy_methods -
|
25
|
+
custom_methods
|
26
|
+
safe_to_hash_methods.each do |method_name|
|
27
|
+
if Util::LAST_ARGUMENT_AS_KEYWORD_PARAMETERS
|
28
|
+
define_method(method_name) { |*a, &b| to_hash.public_send(method_name, *a, &b) }
|
29
|
+
else
|
30
|
+
define_method(method_name) { |*a, **kw, &b| to_hash.public_send(method_name, *a, **kw, &b) }
|
31
|
+
end
|
32
|
+
end
|
33
|
+
safe_modified_copy_methods.each do |method_name|
|
34
|
+
if Util::LAST_ARGUMENT_AS_KEYWORD_PARAMETERS
|
35
|
+
define_method(method_name) do |*a, &b|
|
36
|
+
jsi_modified_copy do |object_to_modify|
|
37
|
+
responsive_object = object_to_modify.respond_to?(method_name) ? object_to_modify : object_to_modify.to_hash
|
38
|
+
responsive_object.public_send(method_name, *a, &b)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
else
|
42
|
+
define_method(method_name) do |*a, **kw, &b|
|
43
|
+
jsi_modified_copy do |object_to_modify|
|
44
|
+
responsive_object = object_to_modify.respond_to?(method_name) ? object_to_modify : object_to_modify.to_hash
|
45
|
+
responsive_object.public_send(method_name, *a, **kw, &b)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
safe_kv_block_modified_copy_methods.each do |method_name|
|
51
|
+
define_method(method_name) do |**kw, &b|
|
52
|
+
jsi_modified_copy do |object_to_modify|
|
53
|
+
responsive_object = object_to_modify.respond_to?(method_name) ? object_to_modify : object_to_modify.to_hash
|
54
|
+
responsive_object.public_send(method_name) do |k, _v|
|
55
|
+
b.call(k, self[k, **kw])
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# like [Hash#update](https://ruby-doc.org/core/Hash.html#method-i-update)
|
62
|
+
# @param other [#to_hash] the other hash to update this hash from
|
63
|
+
# @yield [key, oldval, newval] for entries with duplicate keys, the value of each duplicate key
|
64
|
+
# is determined by calling the block with the key, its value in self and its value in other.
|
65
|
+
# @return self, updated with other
|
66
|
+
# @raise [TypeError] when `other` does not respond to #to_hash
|
67
|
+
def update(other, &block)
|
68
|
+
unless other.respond_to?(:to_hash)
|
69
|
+
raise(TypeError, "cannot update with argument that does not respond to #to_hash: #{other.pretty_inspect.chomp}")
|
70
|
+
end
|
71
|
+
other.to_hash.each_pair do |key, value|
|
72
|
+
if block && key?(key)
|
73
|
+
value = yield(key, self[key], value)
|
74
|
+
end
|
75
|
+
self[key] = value
|
76
|
+
end
|
77
|
+
self
|
78
|
+
end
|
79
|
+
|
80
|
+
alias_method :merge!, :update
|
81
|
+
|
82
|
+
# like [Hash#merge](https://ruby-doc.org/core/Hash.html#method-i-merge)
|
83
|
+
# @param other [#to_hash] the other hash to merge into this
|
84
|
+
# @yield [key, oldval, newval] for entries with duplicate keys, the value of each duplicate key
|
85
|
+
# is determined by calling the block with the key, its value in self and its value in other.
|
86
|
+
# @return duplicate of this hash with the other hash merged in
|
87
|
+
# @raise [TypeError] when `other` does not respond to #to_hash
|
88
|
+
def merge(other, &block)
|
89
|
+
jsi_modified_copy do |instance|
|
90
|
+
instance.merge(other.is_a?(Base) ? other.jsi_node_content : other, &block)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
# basically the same #inspect as Hash, but has the class name and, if responsive,
|
95
|
+
# self's #jsi_object_group_text
|
96
|
+
# @return [String]
|
97
|
+
def inspect
|
98
|
+
object_group_str = (respond_to?(:jsi_object_group_text, true) ? jsi_object_group_text : [self.class]).join(' ')
|
99
|
+
-"\#{<#{object_group_str}>#{map { |k, v| " #{k.inspect} => #{v.inspect}" }.join(',')}}"
|
100
|
+
end
|
101
|
+
|
102
|
+
alias_method :to_s, :inspect
|
103
|
+
|
104
|
+
# pretty-prints a representation of this hashlike to the given printer
|
105
|
+
# @return [void]
|
106
|
+
def pretty_print(q)
|
107
|
+
object_group_str = (respond_to?(:jsi_object_group_text, true) ? jsi_object_group_text : [self.class]).join(' ')
|
108
|
+
q.text "\#{<#{object_group_str}>"
|
109
|
+
q.group_sub {
|
110
|
+
q.nest(2) {
|
111
|
+
q.breakable ' ' if !empty?
|
112
|
+
q.seplist(self, nil, :each_pair) { |k, v|
|
113
|
+
q.group {
|
114
|
+
q.pp k
|
115
|
+
q.text ' => '
|
116
|
+
q.pp v
|
117
|
+
}
|
118
|
+
}
|
119
|
+
}
|
120
|
+
}
|
121
|
+
q.breakable '' if !empty?
|
122
|
+
q.text '}'
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# a module of methods for objects which behave like Array but are not Array.
|
127
|
+
#
|
128
|
+
# this module is intended to be internal to JSI. no guarantees or API promises
|
129
|
+
# are made for non-JSI classes including this module.
|
130
|
+
module Util::Arraylike
|
131
|
+
# safe methods which can be delegated to #to_ary (which the includer is assumed to have defined).
|
132
|
+
# 'safe' means, in this context, nondestructive - methods which do not modify the receiver.
|
133
|
+
|
134
|
+
# methods which do not need to access the element.
|
135
|
+
SAFE_INDEX_ONLY_METHODS = %w(each_index empty? length size).map(&:freeze).freeze
|
136
|
+
# there are some ambiguous ones that are omitted, like #sort, #map / #collect.
|
137
|
+
SAFE_INDEX_ELEMENT_METHODS = %w(| & * + - <=> abbrev at bsearch bsearch_index combination compact count cycle dig drop drop_while fetch find_index first include? index join last pack permutation product reject repeated_combination repeated_permutation reverse reverse_each rindex rotate sample select shelljoin shuffle slice sort take take_while transpose uniq values_at zip).map(&:freeze).freeze
|
138
|
+
DESTRUCTIVE_METHODS = %w(<< clear collect! compact! concat delete delete_at delete_if fill flatten! insert keep_if map! pop push reject! replace reverse! rotate! select! shift shuffle! slice! sort! sort_by! uniq! unshift).map(&:freeze).freeze
|
139
|
+
|
140
|
+
# methods (well, method) that returns a modified copy and doesn't need any handling of block variable(s)
|
141
|
+
safe_modified_copy_methods = %w(compact)
|
142
|
+
|
143
|
+
# methods that return a modified copy and do need handling of block variables
|
144
|
+
safe_el_block_methods = %w(reject select)
|
145
|
+
|
146
|
+
SAFE_METHODS = SAFE_INDEX_ONLY_METHODS | SAFE_INDEX_ELEMENT_METHODS
|
147
|
+
safe_to_ary_methods = SAFE_METHODS - safe_modified_copy_methods - safe_el_block_methods
|
148
|
+
safe_to_ary_methods.each do |method_name|
|
149
|
+
if Util::LAST_ARGUMENT_AS_KEYWORD_PARAMETERS
|
150
|
+
define_method(method_name) { |*a, &b| to_ary.public_send(method_name, *a, &b) }
|
151
|
+
else
|
152
|
+
define_method(method_name) { |*a, **kw, &b| to_ary.public_send(method_name, *a, **kw, &b) }
|
153
|
+
end
|
154
|
+
end
|
155
|
+
safe_modified_copy_methods.each do |method_name|
|
156
|
+
if Util::LAST_ARGUMENT_AS_KEYWORD_PARAMETERS
|
157
|
+
define_method(method_name) do |*a, &b|
|
158
|
+
jsi_modified_copy do |object_to_modify|
|
159
|
+
responsive_object = object_to_modify.respond_to?(method_name) ? object_to_modify : object_to_modify.to_ary
|
160
|
+
responsive_object.public_send(method_name, *a, &b)
|
161
|
+
end
|
162
|
+
end
|
163
|
+
else
|
164
|
+
define_method(method_name) do |*a, **kw, &b|
|
165
|
+
jsi_modified_copy do |object_to_modify|
|
166
|
+
responsive_object = object_to_modify.respond_to?(method_name) ? object_to_modify : object_to_modify.to_ary
|
167
|
+
responsive_object.public_send(method_name, *a, **kw, &b)
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
safe_el_block_methods.each do |method_name|
|
173
|
+
define_method(method_name) do |**kw, &b|
|
174
|
+
jsi_modified_copy do |object_to_modify|
|
175
|
+
i = 0
|
176
|
+
responsive_object = object_to_modify.respond_to?(method_name) ? object_to_modify : object_to_modify.to_ary
|
177
|
+
responsive_object.public_send(method_name) do |_e|
|
178
|
+
b.call(self[i, **kw]).tap { i += 1 }
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
# see [Array#assoc](https://ruby-doc.org/core/Array.html#method-i-assoc)
|
185
|
+
def assoc(obj)
|
186
|
+
# note: assoc implemented here (instead of delegated) due to inconsistencies in whether
|
187
|
+
# other implementations expect each element to be an Array or to respond to #to_ary
|
188
|
+
detect { |e| e.respond_to?(:to_ary) and e[0] == obj }
|
189
|
+
end
|
190
|
+
|
191
|
+
# see [Array#rassoc](https://ruby-doc.org/core/Array.html#method-i-rassoc)
|
192
|
+
def rassoc(obj)
|
193
|
+
# note: rassoc implemented here (instead of delegated) due to inconsistencies in whether
|
194
|
+
# other implementations expect each element to be an Array or to respond to #to_ary
|
195
|
+
detect { |e| e.respond_to?(:to_ary) and e[1] == obj }
|
196
|
+
end
|
197
|
+
|
198
|
+
# basically the same #inspect as Array, but has the class name and, if responsive,
|
199
|
+
# self's #jsi_object_group_text
|
200
|
+
# @return [String]
|
201
|
+
def inspect
|
202
|
+
object_group_str = (respond_to?(:jsi_object_group_text, true) ? jsi_object_group_text : [self.class]).join(' ')
|
203
|
+
-"\#[<#{object_group_str}>#{map { |e| ' ' + e.inspect }.join(',')}]"
|
204
|
+
end
|
205
|
+
|
206
|
+
alias_method :to_s, :inspect
|
207
|
+
|
208
|
+
# pretty-prints a representation of this arraylike to the given printer
|
209
|
+
# @return [void]
|
210
|
+
def pretty_print(q)
|
211
|
+
object_group_str = (respond_to?(:jsi_object_group_text, true) ? jsi_object_group_text : [self.class]).join(' ')
|
212
|
+
q.text "\#[<#{object_group_str}>"
|
213
|
+
q.group_sub {
|
214
|
+
q.nest(2) {
|
215
|
+
q.breakable ' ' if !empty?
|
216
|
+
q.seplist(self, nil, :each) { |e|
|
217
|
+
q.pp e
|
218
|
+
}
|
219
|
+
}
|
220
|
+
}
|
221
|
+
q.breakable '' if !empty?
|
222
|
+
q.text ']'
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
data/lib/jsi/util.rb
ADDED
@@ -0,0 +1,227 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JSI
|
4
|
+
# JSI::Util contains public utilities
|
5
|
+
module Util
|
6
|
+
autoload :Private, 'jsi/util/private'
|
7
|
+
|
8
|
+
include Private
|
9
|
+
|
10
|
+
extend self
|
11
|
+
|
12
|
+
autoload :Arraylike, 'jsi/util/typelike'
|
13
|
+
autoload :Hashlike, 'jsi/util/typelike'
|
14
|
+
|
15
|
+
# yields the content of the given param `object`. for objects which have a #jsi_modified_copy
|
16
|
+
# method of their own (JSI::Base, JSI::MetaschemaNode) that method is invoked with the given
|
17
|
+
# block. otherwise the given object itself is yielded.
|
18
|
+
#
|
19
|
+
# the given block must result in a modified copy of its block parameter
|
20
|
+
# (not destructively modifying the yielded content).
|
21
|
+
#
|
22
|
+
# @yield [Object] the content of the given object. the block should result
|
23
|
+
# in a (nondestructively) modified copy of this.
|
24
|
+
# @return [object.class] modified copy of the given object
|
25
|
+
def modified_copy(object, &block)
|
26
|
+
if object.respond_to?(:jsi_modified_copy)
|
27
|
+
object.jsi_modified_copy(&block)
|
28
|
+
else
|
29
|
+
yield(object)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# A structure like the given `object`, recursively coerced to JSON-compatible types.
|
34
|
+
#
|
35
|
+
# - Structures of Hash, Array, and basic types of String/number/boolean/nil are returned as-is.
|
36
|
+
# - If the object responds to `#as_json`, that method is used, passing any given options.
|
37
|
+
# - If the object supports [implicit conversion](https://docs.ruby-lang.org/en/master/implicit_conversion_rdoc.html)
|
38
|
+
# with `#to_hash`, `#to_ary`, `#to_str`, or `#to_int`, that is used.
|
39
|
+
# - Set becomes Array; Symbol becomes String.
|
40
|
+
# - Types with no known coersion to JSON-compatible raise TypeError.
|
41
|
+
#
|
42
|
+
# @param object [Object]
|
43
|
+
# @return [Array, Hash, String, Integer, Float, Boolean, NilClass] a JSON-compatible structure like the given `object`
|
44
|
+
# @raise [TypeError] If the object cannot be coerced to a JSON-compatible structure
|
45
|
+
def as_json(object, options = {})
|
46
|
+
type_err = proc { raise(TypeError, "cannot express object as json: #{object.pretty_inspect.chomp}") }
|
47
|
+
if object.respond_to?(:as_json)
|
48
|
+
options.empty? ? object.as_json : object.as_json(**options) # TODO remove eventually (keyword argument compatibility)
|
49
|
+
elsif object.is_a?(Addressable::URI)
|
50
|
+
object.to_s
|
51
|
+
elsif object.respond_to?(:to_hash) && (object_to_hash = object.to_hash).is_a?(Hash)
|
52
|
+
result = {}
|
53
|
+
object_to_hash.each_pair do |k, v|
|
54
|
+
ks = k.is_a?(String) ? k :
|
55
|
+
k.is_a?(Symbol) ? k.to_s :
|
56
|
+
k.respond_to?(:to_str) && (kstr = k.to_str).is_a?(String) ? kstr :
|
57
|
+
raise(TypeError, "json object (hash) cannot be keyed with: #{k.pretty_inspect.chomp}")
|
58
|
+
result[ks] = as_json(v, **options)
|
59
|
+
end
|
60
|
+
result
|
61
|
+
elsif object.respond_to?(:to_ary) && (object_to_ary = object.to_ary).is_a?(Array)
|
62
|
+
object_to_ary.map { |e| as_json(e, **options) }
|
63
|
+
elsif [String, Integer, TrueClass, FalseClass, NilClass].any? { |c| object.is_a?(c) }
|
64
|
+
object
|
65
|
+
elsif object.is_a?(Float)
|
66
|
+
type_err.call unless object.finite?
|
67
|
+
object
|
68
|
+
elsif object.is_a?(Symbol)
|
69
|
+
object.to_s
|
70
|
+
elsif object.is_a?(Set)
|
71
|
+
as_json(object.to_a, **options)
|
72
|
+
elsif object.respond_to?(:to_str) && (object_to_str = object.to_str).is_a?(String)
|
73
|
+
object_to_str
|
74
|
+
elsif object.respond_to?(:to_int) && (object_to_int = object.to_int).is_a?(Integer)
|
75
|
+
object_to_int
|
76
|
+
else
|
77
|
+
type_err.call
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# A JSON encoded string of the given object.
|
82
|
+
#
|
83
|
+
# - If the object has a `#to_json` method that isn't defined by the stdlib `json` gem,
|
84
|
+
# that method is used, passing any given options.
|
85
|
+
# - Otherwise, JSON is generated using {as_json} to coerce to compatible types.
|
86
|
+
# @return [String]
|
87
|
+
def to_json(object, options = {})
|
88
|
+
if USE_TO_JSON_METHOD[object.class]
|
89
|
+
options.empty? ? object.to_json : object.to_json(**options) # TODO remove eventually (keyword argument compatibility)
|
90
|
+
else
|
91
|
+
JSON.generate(as_json(object, **options))
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
# a hash copied from the given hashlike, in which any symbol keys are
|
96
|
+
# converted to strings. behavior on collisions is undefined (but in the
|
97
|
+
# future could take a block like
|
98
|
+
# ActiveSupport::HashWithIndifferentAccess#update)
|
99
|
+
#
|
100
|
+
# at the moment it is undefined whether the returned hash is the same
|
101
|
+
# instance as the `hash` param. if `hash` is already a hash which contains
|
102
|
+
# no symbol keys, this method MAY return that same instance. use #dup on
|
103
|
+
# the return if you need to ensure it is not the same instance as the
|
104
|
+
# argument instance.
|
105
|
+
#
|
106
|
+
# @param hashlike [#to_hash] the hash from which to convert symbol keys to strings
|
107
|
+
# @return [same class as the param `hash`, or Hash if the former cannot be done] a
|
108
|
+
# hash(-like) instance containing no symbol keys
|
109
|
+
def stringify_symbol_keys(hashlike)
|
110
|
+
unless hashlike.respond_to?(:to_hash)
|
111
|
+
raise(ArgumentError, "expected argument to be a hash; got #{hashlike.class.inspect}: #{hashlike.pretty_inspect.chomp}")
|
112
|
+
end
|
113
|
+
JSI::Util.modified_copy(hashlike) do |hash|
|
114
|
+
out = {}
|
115
|
+
hash.each do |k, v|
|
116
|
+
out[k.is_a?(Symbol) ? k.to_s : k] = v
|
117
|
+
end
|
118
|
+
out
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def deep_stringify_symbol_keys(object)
|
123
|
+
if object.respond_to?(:to_hash) && !object.is_a?(Addressable::URI)
|
124
|
+
JSI::Util.modified_copy(object) do |hash|
|
125
|
+
out = {}
|
126
|
+
(hash.respond_to?(:each) ? hash : hash.to_hash).each do |k, v|
|
127
|
+
out[k.is_a?(Symbol) ? k.to_s.freeze : deep_stringify_symbol_keys(k)] = deep_stringify_symbol_keys(v)
|
128
|
+
end
|
129
|
+
out
|
130
|
+
end
|
131
|
+
elsif object.respond_to?(:to_ary)
|
132
|
+
JSI::Util.modified_copy(object) do |ary|
|
133
|
+
(ary.respond_to?(:each) ? ary : ary.to_ary).map do |e|
|
134
|
+
deep_stringify_symbol_keys(e)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
else
|
138
|
+
object
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
# returns an object which is equal to the param object, and is recursively frozen.
|
143
|
+
# the given object is not modified.
|
144
|
+
def deep_to_frozen(object, not_implemented: nil)
|
145
|
+
dtf = proc { |o| deep_to_frozen(o, not_implemented: not_implemented) }
|
146
|
+
if object.instance_of?(Hash)
|
147
|
+
out = {}
|
148
|
+
identical = object.frozen?
|
149
|
+
object.each do |k, v|
|
150
|
+
fk = dtf[k]
|
151
|
+
fv = dtf[v]
|
152
|
+
identical &&= fk.__id__ == k.__id__
|
153
|
+
identical &&= fv.__id__ == v.__id__
|
154
|
+
out[fk] = fv
|
155
|
+
end
|
156
|
+
if !object.default.nil?
|
157
|
+
out.default = dtf[object.default]
|
158
|
+
identical &&= out.default.__id__ == object.default.__id__
|
159
|
+
end
|
160
|
+
if object.default_proc
|
161
|
+
raise(ArgumentError, "cannot make immutable copy of a Hash with default_proc")
|
162
|
+
end
|
163
|
+
if identical
|
164
|
+
object
|
165
|
+
else
|
166
|
+
out.freeze
|
167
|
+
end
|
168
|
+
elsif object.instance_of?(Array)
|
169
|
+
identical = object.frozen?
|
170
|
+
out = Array.new(object.size)
|
171
|
+
object.each_with_index do |e, i|
|
172
|
+
fe = dtf[e]
|
173
|
+
identical &&= fe.__id__ == e.__id__
|
174
|
+
out[i] = fe
|
175
|
+
end
|
176
|
+
if identical
|
177
|
+
object
|
178
|
+
else
|
179
|
+
out.freeze
|
180
|
+
end
|
181
|
+
elsif object.instance_of?(String)
|
182
|
+
if object.frozen?
|
183
|
+
object
|
184
|
+
else
|
185
|
+
object.dup.freeze
|
186
|
+
end
|
187
|
+
elsif CLASSES_ALWAYS_FROZEN.any? { |c| object.is_a?(c) } # note: `is_a?`, not `instance_of?`, here because instance_of?(Integer) is false until Fixnum/Bignum is gone. this is fine here; there is no concern of subclasses of CLASSES_ALWAYS_FROZEN duping/freezing differently (as with e.g. ActiveSupport::HashWithIndifferentAccess)
|
188
|
+
object
|
189
|
+
else
|
190
|
+
if not_implemented
|
191
|
+
not_implemented.call(object)
|
192
|
+
else
|
193
|
+
raise(NotImplementedError, [
|
194
|
+
"deep_to_frozen not implemented for class: #{object.class}",
|
195
|
+
"object: #{object.pretty_inspect.chomp}",
|
196
|
+
].join("\n"))
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
# ensures the given param becomes a frozen Set of Modules.
|
202
|
+
# returns the param if it is already that, otherwise initializes and freezes such a Set.
|
203
|
+
#
|
204
|
+
# @param modules [Set, Enumerable] the object to ensure becomes a frozen Set of Modules
|
205
|
+
# @return [Set] frozen Set containing the given modules
|
206
|
+
# @raise [ArgumentError] when the modules param is not an Enumerable
|
207
|
+
# @raise [Schema::NotASchemaError] when the modules param contains objects which are not Schemas
|
208
|
+
def ensure_module_set(modules)
|
209
|
+
if modules.is_a?(Set) && modules.frozen?
|
210
|
+
set = modules
|
211
|
+
elsif modules.is_a?(Enumerable)
|
212
|
+
set = Set.new(modules).freeze
|
213
|
+
else
|
214
|
+
raise(TypeError, "not given an Enumerable of Modules")
|
215
|
+
end
|
216
|
+
not_modules = set.reject { |s| s.is_a?(Module) }
|
217
|
+
if !not_modules.empty?
|
218
|
+
raise(TypeError, [
|
219
|
+
"ensure_module_set given non-Module objects:",
|
220
|
+
*not_modules.map { |ns| ns.pretty_inspect.chomp },
|
221
|
+
].join("\n"))
|
222
|
+
end
|
223
|
+
|
224
|
+
set
|
225
|
+
end
|
226
|
+
end
|
227
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JSI
|
4
|
+
module Validation
|
5
|
+
Error = Util::AttrStruct[*%w(
|
6
|
+
message
|
7
|
+
keyword
|
8
|
+
schema
|
9
|
+
instance_ptr
|
10
|
+
instance_document
|
11
|
+
)]
|
12
|
+
|
13
|
+
# a validation error of a schema instance against a schema
|
14
|
+
#
|
15
|
+
# @!attribute message
|
16
|
+
# a message describing the error
|
17
|
+
# @return [String]
|
18
|
+
# @!attribute keyword
|
19
|
+
# the keyword of the schema which failed to validate.
|
20
|
+
# this may be absent if the error is not from a schema keyword (i.e, `false` schema).
|
21
|
+
# @return [String]
|
22
|
+
# @!attribute schema
|
23
|
+
# the schema against which the instance failed to validate
|
24
|
+
# @return [JSI::Schema]
|
25
|
+
# @!attribute instance_ptr
|
26
|
+
# pointer to the instance in instance_document
|
27
|
+
# @return [JSI::Ptr]
|
28
|
+
# @!attribute instance_document
|
29
|
+
# document containing the instance at instance_ptr
|
30
|
+
# @return [Object]
|
31
|
+
class Error
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,212 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JSI
|
4
|
+
module Validation
|
5
|
+
# a result of validating an instance against schemas which describe it.
|
6
|
+
# virtual base class.
|
7
|
+
class Result
|
8
|
+
include Util::Virtual
|
9
|
+
|
10
|
+
Builder = Util::AttrStruct[*%w(
|
11
|
+
result
|
12
|
+
schema
|
13
|
+
instance_ptr
|
14
|
+
instance_document
|
15
|
+
validate_only
|
16
|
+
visited_refs
|
17
|
+
)]
|
18
|
+
|
19
|
+
# @private
|
20
|
+
# a structure used to build a Result. virtual base class.
|
21
|
+
class Builder
|
22
|
+
def instance
|
23
|
+
instance_ptr.evaluate(instance_document)
|
24
|
+
end
|
25
|
+
|
26
|
+
def schema_issue(*_)
|
27
|
+
virtual_method
|
28
|
+
end
|
29
|
+
|
30
|
+
def schema_error(message, keyword = nil)
|
31
|
+
schema_issue(:error, message, keyword)
|
32
|
+
end
|
33
|
+
|
34
|
+
def schema_warning(message, keyword = nil)
|
35
|
+
schema_issue(:warning, message, keyword)
|
36
|
+
end
|
37
|
+
|
38
|
+
# @param subschema_ptr [JSI::Ptr, #to_ary]
|
39
|
+
# @return [JSI::Validation::Result]
|
40
|
+
def inplace_subschema_validate(subschema_ptr)
|
41
|
+
subresult = schema.subschema(subschema_ptr).internal_validate_instance(
|
42
|
+
instance_ptr,
|
43
|
+
instance_document,
|
44
|
+
validate_only: validate_only,
|
45
|
+
visited_refs: visited_refs,
|
46
|
+
)
|
47
|
+
merge_schema_issues(subresult)
|
48
|
+
subresult
|
49
|
+
end
|
50
|
+
|
51
|
+
# @param instance_child_token [String, Integer]
|
52
|
+
# @param subschema_ptr [JSI::Ptr, #to_ary]
|
53
|
+
# @return [JSI::Validation::Result]
|
54
|
+
def child_subschema_validate(instance_child_token, subschema_ptr)
|
55
|
+
subresult = schema.subschema(subschema_ptr).internal_validate_instance(
|
56
|
+
instance_ptr[instance_child_token],
|
57
|
+
instance_document,
|
58
|
+
validate_only: validate_only,
|
59
|
+
)
|
60
|
+
merge_schema_issues(subresult)
|
61
|
+
subresult
|
62
|
+
end
|
63
|
+
|
64
|
+
# @param other_result [JSI::Validation::Result]
|
65
|
+
# @return [void]
|
66
|
+
def merge_schema_issues(other_result)
|
67
|
+
unless validate_only
|
68
|
+
# schema_issues are always merged from subschema results (not depending on validation results)
|
69
|
+
result.schema_issues.merge(other_result.schema_issues)
|
70
|
+
end
|
71
|
+
nil
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def builder(schema, instance_ptr, instance_document, validate_only, visited_refs)
|
76
|
+
self.class::Builder.new(
|
77
|
+
result: self,
|
78
|
+
schema: schema,
|
79
|
+
instance_ptr: instance_ptr,
|
80
|
+
instance_document: instance_document,
|
81
|
+
validate_only: validate_only,
|
82
|
+
visited_refs: visited_refs,
|
83
|
+
)
|
84
|
+
end
|
85
|
+
|
86
|
+
# is the instance valid against its schemas?
|
87
|
+
# @return [Boolean]
|
88
|
+
def valid?
|
89
|
+
# :nocov:
|
90
|
+
virtual_method
|
91
|
+
# :nocov:
|
92
|
+
end
|
93
|
+
|
94
|
+
include Util::FingerprintHash
|
95
|
+
end
|
96
|
+
|
97
|
+
# a full result of validating an instance against its schemas, with each validation error
|
98
|
+
class FullResult < Result
|
99
|
+
# @private
|
100
|
+
class Builder < Result::Builder
|
101
|
+
def validate(
|
102
|
+
valid,
|
103
|
+
message,
|
104
|
+
keyword: nil,
|
105
|
+
results: []
|
106
|
+
)
|
107
|
+
results.each { |res| result.schema_issues.merge(res.schema_issues) }
|
108
|
+
if !valid
|
109
|
+
results.each { |res| result.validation_errors.merge(res.validation_errors) }
|
110
|
+
result.validation_errors << Validation::Error.new({
|
111
|
+
message: message,
|
112
|
+
keyword: keyword,
|
113
|
+
schema: schema,
|
114
|
+
instance_ptr: instance_ptr,
|
115
|
+
instance_document: instance_document,
|
116
|
+
})
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def schema_issue(level, message, keyword = nil)
|
121
|
+
result.schema_issues << Schema::Issue.new({
|
122
|
+
level: level,
|
123
|
+
message: message,
|
124
|
+
keyword: keyword,
|
125
|
+
schema: schema,
|
126
|
+
})
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def initialize
|
131
|
+
@validation_errors = Set.new
|
132
|
+
@schema_issues = Set.new
|
133
|
+
end
|
134
|
+
|
135
|
+
attr_reader :validation_errors
|
136
|
+
attr_reader :schema_issues
|
137
|
+
|
138
|
+
def valid?
|
139
|
+
validation_errors.empty?
|
140
|
+
end
|
141
|
+
|
142
|
+
def freeze
|
143
|
+
@validation_errors.each(&:freeze)
|
144
|
+
@schema_issues.each(&:freeze)
|
145
|
+
@validation_errors.freeze
|
146
|
+
@schema_issues.freeze
|
147
|
+
super
|
148
|
+
end
|
149
|
+
|
150
|
+
def merge(result)
|
151
|
+
unless result.is_a?(FullResult)
|
152
|
+
raise(TypeError, "not a #{FullResult.name}: #{result.pretty_inspect.chomp}")
|
153
|
+
end
|
154
|
+
validation_errors.merge(result.validation_errors)
|
155
|
+
schema_issues.merge(result.schema_issues)
|
156
|
+
self
|
157
|
+
end
|
158
|
+
|
159
|
+
def +(result)
|
160
|
+
FullResult.new.merge(self).merge(result)
|
161
|
+
end
|
162
|
+
|
163
|
+
# see {Util::Private::FingerprintHash}
|
164
|
+
# @api private
|
165
|
+
def jsi_fingerprint
|
166
|
+
{
|
167
|
+
class: self.class,
|
168
|
+
validation_errors: validation_errors,
|
169
|
+
schema_issues: schema_issues,
|
170
|
+
}
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
# a result indicating only whether an instance is valid against its schemas
|
175
|
+
class ValidityResult < Result
|
176
|
+
# @private
|
177
|
+
class Builder < Result::Builder
|
178
|
+
def validate(
|
179
|
+
valid,
|
180
|
+
message,
|
181
|
+
keyword: nil,
|
182
|
+
results: []
|
183
|
+
)
|
184
|
+
if !valid
|
185
|
+
throw(:jsi_validation_result, INVALID)
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
def schema_issue(*_)
|
190
|
+
# noop
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
def initialize(valid)
|
195
|
+
@valid = valid
|
196
|
+
end
|
197
|
+
|
198
|
+
def valid?
|
199
|
+
@valid
|
200
|
+
end
|
201
|
+
|
202
|
+
# see {Util::Private::FingerprintHash}
|
203
|
+
# @api private
|
204
|
+
def jsi_fingerprint
|
205
|
+
{
|
206
|
+
class: self.class,
|
207
|
+
valid: valid?,
|
208
|
+
}
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|