jsi 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.yardopts +1 -1
- data/CHANGELOG.md +15 -0
- data/README.md +105 -38
- data/lib/jsi/base.rb +349 -155
- data/lib/jsi/jsi_coder.rb +5 -4
- data/lib/jsi/metaschema_node/bootstrap_schema.rb +100 -0
- data/lib/jsi/metaschema_node.rb +156 -129
- data/lib/jsi/pathed_node.rb +47 -49
- data/lib/jsi/ptr.rb +292 -0
- data/lib/jsi/schema/application/child_application/contains.rb +16 -0
- data/lib/jsi/schema/application/child_application/draft04.rb +22 -0
- data/lib/jsi/schema/application/child_application/draft06.rb +29 -0
- data/lib/jsi/schema/application/child_application/draft07.rb +29 -0
- data/lib/jsi/schema/application/child_application/items.rb +18 -0
- data/lib/jsi/schema/application/child_application/properties.rb +25 -0
- data/lib/jsi/schema/application/child_application.rb +40 -0
- data/lib/jsi/schema/application/draft04.rb +8 -0
- data/lib/jsi/schema/application/draft06.rb +8 -0
- data/lib/jsi/schema/application/draft07.rb +8 -0
- data/lib/jsi/schema/application/inplace_application/dependencies.rb +28 -0
- data/lib/jsi/schema/application/inplace_application/draft04.rb +26 -0
- data/lib/jsi/schema/application/inplace_application/draft06.rb +27 -0
- data/lib/jsi/schema/application/inplace_application/draft07.rb +33 -0
- data/lib/jsi/schema/application/inplace_application/ifthenelse.rb +20 -0
- data/lib/jsi/schema/application/inplace_application/ref.rb +18 -0
- data/lib/jsi/schema/application/inplace_application/someof.rb +29 -0
- data/lib/jsi/schema/application/inplace_application.rb +46 -0
- data/lib/jsi/schema/application.rb +12 -0
- data/lib/jsi/schema/draft04.rb +14 -0
- data/lib/jsi/schema/draft06.rb +14 -0
- data/lib/jsi/schema/draft07.rb +14 -0
- data/lib/jsi/schema/issue.rb +36 -0
- data/lib/jsi/schema/ref.rb +159 -0
- data/lib/jsi/schema/schema_ancestor_node.rb +119 -0
- data/lib/jsi/schema/validation/array.rb +69 -0
- data/lib/jsi/schema/validation/const.rb +20 -0
- data/lib/jsi/schema/validation/contains.rb +25 -0
- data/lib/jsi/schema/validation/core.rb +39 -0
- data/lib/jsi/schema/validation/dependencies.rb +49 -0
- data/lib/jsi/schema/validation/draft04/minmax.rb +91 -0
- data/lib/jsi/schema/validation/draft04.rb +112 -0
- data/lib/jsi/schema/validation/draft06.rb +122 -0
- data/lib/jsi/schema/validation/draft07.rb +159 -0
- data/lib/jsi/schema/validation/enum.rb +25 -0
- data/lib/jsi/schema/validation/ifthenelse.rb +46 -0
- data/lib/jsi/schema/validation/items.rb +54 -0
- data/lib/jsi/schema/validation/not.rb +20 -0
- data/lib/jsi/schema/validation/numeric.rb +121 -0
- data/lib/jsi/schema/validation/object.rb +45 -0
- data/lib/jsi/schema/validation/pattern.rb +34 -0
- data/lib/jsi/schema/validation/properties.rb +101 -0
- data/lib/jsi/schema/validation/property_names.rb +32 -0
- data/lib/jsi/schema/validation/ref.rb +40 -0
- data/lib/jsi/schema/validation/required.rb +27 -0
- data/lib/jsi/schema/validation/someof.rb +90 -0
- data/lib/jsi/schema/validation/string.rb +47 -0
- data/lib/jsi/schema/validation/type.rb +49 -0
- data/lib/jsi/schema/validation.rb +51 -0
- data/lib/jsi/schema.rb +486 -133
- data/lib/jsi/schema_classes.rb +157 -42
- data/lib/jsi/schema_registry.rb +141 -0
- data/lib/jsi/schema_set.rb +141 -0
- data/lib/jsi/simple_wrap.rb +2 -2
- data/lib/jsi/typelike_modules.rb +52 -37
- data/lib/jsi/util/attr_struct.rb +106 -0
- data/lib/jsi/util.rb +141 -25
- data/lib/jsi/validation/error.rb +34 -0
- data/lib/jsi/validation/result.rb +210 -0
- data/lib/jsi/validation.rb +15 -0
- data/lib/jsi/version.rb +3 -1
- data/lib/jsi.rb +55 -9
- data/lib/schemas/json-schema.org/draft-04/schema.rb +8 -3
- data/lib/schemas/json-schema.org/draft-06/schema.rb +8 -3
- data/lib/schemas/json-schema.org/draft-07/schema.rb +12 -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 +69 -118
- data/.simplecov +0 -3
- data/Rakefile.rb +0 -9
- data/jsi.gemspec +0 -28
- data/lib/jsi/base/to_rb.rb +0 -128
- data/lib/jsi/json/node.rb +0 -203
- data/lib/jsi/json/pointer.rb +0 -419
- data/lib/jsi/json-schema-fragments.rb +0 -61
- data/lib/jsi/json.rb +0 -10
- data/resources/icons/AGPL-3.0.png +0 -0
- data/test/base_array_test.rb +0 -323
- data/test/base_hash_test.rb +0 -337
- data/test/base_test.rb +0 -486
- data/test/jsi_coder_test.rb +0 -85
- data/test/jsi_json_arraynode_test.rb +0 -150
- data/test/jsi_json_hashnode_test.rb +0 -132
- data/test/jsi_json_node_test.rb +0 -257
- data/test/jsi_json_pointer_test.rb +0 -102
- data/test/jsi_test.rb +0 -11
- data/test/jsi_typelike_as_json_test.rb +0 -53
- data/test/metaschema_node_test.rb +0 -19
- data/test/schema_module_test.rb +0 -21
- data/test/schema_test.rb +0 -208
- data/test/spreedly_openapi_test.rb +0 -8
- data/test/test_helper.rb +0 -97
- data/test/util_test.rb +0 -62
data/lib/jsi/typelike_modules.rb
CHANGED
|
@@ -3,10 +3,9 @@
|
|
|
3
3
|
module JSI
|
|
4
4
|
# a module relating to objects that act like Hash or Array instances
|
|
5
5
|
module Typelike
|
|
6
|
-
# yields the content of the given param `object`. for objects which have a
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
# is yielded.
|
|
6
|
+
# yields the content of the given param `object`. for objects which have a #jsi_modified_copy
|
|
7
|
+
# method of their own (JSI::Base, JSI::MetaschemaNode) that method is invoked with the given
|
|
8
|
+
# block. otherwise the given object itself is yielded.
|
|
10
9
|
#
|
|
11
10
|
# the given block must result in a modified copy of its block parameter
|
|
12
11
|
# (not destructively modifying the yielded content).
|
|
@@ -15,8 +14,8 @@ module JSI
|
|
|
15
14
|
# in a (nondestructively) modified copy of this.
|
|
16
15
|
# @return [object.class] modified copy of the given object
|
|
17
16
|
def self.modified_copy(object, &block)
|
|
18
|
-
if object.respond_to?(:
|
|
19
|
-
object.
|
|
17
|
+
if object.respond_to?(:jsi_modified_copy)
|
|
18
|
+
object.jsi_modified_copy(&block)
|
|
20
19
|
else
|
|
21
20
|
return yield(object)
|
|
22
21
|
end
|
|
@@ -27,8 +26,8 @@ module JSI
|
|
|
27
26
|
# will raise TypeError if an object is given that is not a type that seems
|
|
28
27
|
# to be expressable as json.
|
|
29
28
|
#
|
|
30
|
-
# similar effect could be achieved by requiring 'json/add/core' and using
|
|
31
|
-
#
|
|
29
|
+
# similar effect could be achieved by requiring 'json/add/core' and using #as_json,
|
|
30
|
+
# but I don't much care for how it represents classes that are
|
|
32
31
|
# not naturally expressable in JSON, and prefer not to load its
|
|
33
32
|
# monkey-patching.
|
|
34
33
|
#
|
|
@@ -39,7 +38,7 @@ module JSI
|
|
|
39
38
|
# array of object) cannot be expressed as json
|
|
40
39
|
def self.as_json(object, *opt)
|
|
41
40
|
if object.is_a?(JSI::PathedNode)
|
|
42
|
-
as_json(object.
|
|
41
|
+
as_json(object.jsi_node_content, *opt)
|
|
43
42
|
elsif object.respond_to?(:to_hash)
|
|
44
43
|
(object.respond_to?(:map) ? object : object.to_hash).map do |k, v|
|
|
45
44
|
unless k.is_a?(Symbol) || k.respond_to?(:to_str)
|
|
@@ -86,7 +85,7 @@ module JSI
|
|
|
86
85
|
end
|
|
87
86
|
safe_modified_copy_methods.each do |method_name|
|
|
88
87
|
define_method(method_name) do |*a, &b|
|
|
89
|
-
|
|
88
|
+
jsi_modified_copy do |object_to_modify|
|
|
90
89
|
responsive_object = object_to_modify.respond_to?(method_name) ? object_to_modify : object_to_modify.to_hash
|
|
91
90
|
responsive_object.public_send(method_name, *a, &b)
|
|
92
91
|
end
|
|
@@ -94,19 +93,19 @@ module JSI
|
|
|
94
93
|
end
|
|
95
94
|
safe_kv_block_modified_copy_methods.each do |method_name|
|
|
96
95
|
define_method(method_name) do |*a, &b|
|
|
97
|
-
|
|
96
|
+
jsi_modified_copy do |object_to_modify|
|
|
98
97
|
responsive_object = object_to_modify.respond_to?(method_name) ? object_to_modify : object_to_modify.to_hash
|
|
99
|
-
responsive_object.public_send(method_name
|
|
100
|
-
b.call(k, self[k])
|
|
98
|
+
responsive_object.public_send(method_name) do |k, _v|
|
|
99
|
+
b.call(k, self[k, *a])
|
|
101
100
|
end
|
|
102
101
|
end
|
|
103
102
|
end
|
|
104
103
|
end
|
|
105
104
|
|
|
106
|
-
#
|
|
105
|
+
# like [Hash#update](https://ruby-doc.org/core/Hash.html#method-i-update)
|
|
107
106
|
# @param other [#to_hash] the other hash to update this hash from
|
|
108
107
|
# @yield [key, oldval, newval] for entries with duplicate keys, the value of each duplicate key
|
|
109
|
-
# is determined by calling the block with the key, its value in
|
|
108
|
+
# is determined by calling the block with the key, its value in self and its value in other.
|
|
110
109
|
# @return self, updated with other
|
|
111
110
|
# @raise [TypeError] when `other` does not respond to #to_hash
|
|
112
111
|
def update(other, &block)
|
|
@@ -115,7 +114,7 @@ module JSI
|
|
|
115
114
|
end
|
|
116
115
|
self_respondingto_key = self.respond_to?(:key?) ? self : to_hash
|
|
117
116
|
other.to_hash.each_pair do |key, value|
|
|
118
|
-
if
|
|
117
|
+
if block && self_respondingto_key.key?(key)
|
|
119
118
|
value = yield(key, self[key], value)
|
|
120
119
|
end
|
|
121
120
|
self[key] = value
|
|
@@ -125,33 +124,34 @@ module JSI
|
|
|
125
124
|
|
|
126
125
|
alias_method :merge!, :update
|
|
127
126
|
|
|
128
|
-
#
|
|
127
|
+
# like [Hash#merge](https://ruby-doc.org/core/Hash.html#method-i-merge)
|
|
129
128
|
# @param other [#to_hash] the other hash to merge into this
|
|
130
129
|
# @yield [key, oldval, newval] for entries with duplicate keys, the value of each duplicate key
|
|
131
|
-
# is determined by calling the block with the key, its value in
|
|
130
|
+
# is determined by calling the block with the key, its value in self and its value in other.
|
|
132
131
|
# @return duplicate of this hash with the other hash merged in
|
|
133
132
|
# @raise [TypeError] when `other` does not respond to #to_hash
|
|
134
133
|
def merge(other, &block)
|
|
135
134
|
dup.update(other, &block)
|
|
136
135
|
end
|
|
137
136
|
|
|
138
|
-
#
|
|
139
|
-
#
|
|
137
|
+
# basically the same #inspect as Hash, but has the class name and, if responsive,
|
|
138
|
+
# self's #jsi_object_group_text
|
|
139
|
+
# @return [String]
|
|
140
140
|
def inspect
|
|
141
|
-
object_group_str = (respond_to?(:
|
|
142
|
-
"\#{<#{object_group_str}>#{
|
|
141
|
+
object_group_str = (respond_to?(:jsi_object_group_text) ? self.jsi_object_group_text : [self.class]).join(' ')
|
|
142
|
+
"\#{<#{object_group_str}>#{self.map { |k, v| " #{k.inspect} => #{v.inspect}" }.join(',')}}"
|
|
143
143
|
end
|
|
144
144
|
|
|
145
145
|
alias_method :to_s, :inspect
|
|
146
146
|
|
|
147
|
-
# pretty-prints a representation this
|
|
147
|
+
# pretty-prints a representation of this hashlike to the given printer
|
|
148
148
|
# @return [void]
|
|
149
149
|
def pretty_print(q)
|
|
150
|
-
object_group_str = (respond_to?(:
|
|
150
|
+
object_group_str = (respond_to?(:jsi_object_group_text) ? jsi_object_group_text : [self.class]).join(' ')
|
|
151
151
|
q.text "\#{<#{object_group_str}>"
|
|
152
152
|
q.group_sub {
|
|
153
153
|
q.nest(2) {
|
|
154
|
-
q.breakable(
|
|
154
|
+
q.breakable(empty? ? '' : ' ')
|
|
155
155
|
q.seplist(self, nil, :each_pair) { |k, v|
|
|
156
156
|
q.group {
|
|
157
157
|
q.pp k
|
|
@@ -177,7 +177,7 @@ module JSI
|
|
|
177
177
|
# methods which do not need to access the element.
|
|
178
178
|
SAFE_INDEX_ONLY_METHODS = %w(each_index empty? length size)
|
|
179
179
|
# there are some ambiguous ones that are omitted, like #sort, #map / #collect.
|
|
180
|
-
SAFE_INDEX_ELEMENT_METHODS = %w(| & * + - <=> abbrev
|
|
180
|
+
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)
|
|
181
181
|
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)
|
|
182
182
|
|
|
183
183
|
# methods (well, method) that returns a modified copy and doesn't need any handling of block variable(s)
|
|
@@ -193,7 +193,7 @@ module JSI
|
|
|
193
193
|
end
|
|
194
194
|
safe_modified_copy_methods.each do |method_name|
|
|
195
195
|
define_method(method_name) do |*a, &b|
|
|
196
|
-
|
|
196
|
+
jsi_modified_copy do |object_to_modify|
|
|
197
197
|
responsive_object = object_to_modify.respond_to?(method_name) ? object_to_modify : object_to_modify.to_ary
|
|
198
198
|
responsive_object.public_send(method_name, *a, &b)
|
|
199
199
|
end
|
|
@@ -201,33 +201,48 @@ module JSI
|
|
|
201
201
|
end
|
|
202
202
|
safe_el_block_methods.each do |method_name|
|
|
203
203
|
define_method(method_name) do |*a, &b|
|
|
204
|
-
|
|
204
|
+
jsi_modified_copy do |object_to_modify|
|
|
205
205
|
i = 0
|
|
206
206
|
responsive_object = object_to_modify.respond_to?(method_name) ? object_to_modify : object_to_modify.to_ary
|
|
207
|
-
responsive_object.public_send(method_name
|
|
208
|
-
b.call(self[i]).tap { i += 1 }
|
|
207
|
+
responsive_object.public_send(method_name) do |_e|
|
|
208
|
+
b.call(self[i, *a]).tap { i += 1 }
|
|
209
209
|
end
|
|
210
210
|
end
|
|
211
211
|
end
|
|
212
212
|
end
|
|
213
213
|
|
|
214
|
-
#
|
|
215
|
-
|
|
214
|
+
# see [Array#assoc](https://ruby-doc.org/core/Array.html#method-i-assoc)
|
|
215
|
+
def assoc(obj)
|
|
216
|
+
# note: assoc implemented here (instead of delegated) due to inconsistencies in whether
|
|
217
|
+
# other implementations expect each element to be an Array or to respond to #to_ary
|
|
218
|
+
detect { |e| e.respond_to?(:to_ary) and e[0] == obj }
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# see [Array#rassoc](https://ruby-doc.org/core/Array.html#method-i-rassoc)
|
|
222
|
+
def rassoc(obj)
|
|
223
|
+
# note: rassoc implemented here (instead of delegated) due to inconsistencies in whether
|
|
224
|
+
# other implementations expect each element to be an Array or to respond to #to_ary
|
|
225
|
+
detect { |e| e.respond_to?(:to_ary) and e[1] == obj }
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# basically the same #inspect as Array, but has the class name and, if responsive,
|
|
229
|
+
# self's #jsi_object_group_text
|
|
230
|
+
# @return [String]
|
|
216
231
|
def inspect
|
|
217
|
-
object_group_str = (respond_to?(:
|
|
218
|
-
"\#[<#{object_group_str}>#{
|
|
232
|
+
object_group_str = (respond_to?(:jsi_object_group_text) ? jsi_object_group_text : [self.class]).join(' ')
|
|
233
|
+
"\#[<#{object_group_str}>#{self.map { |e| ' ' + e.inspect }.join(',')}]"
|
|
219
234
|
end
|
|
220
235
|
|
|
221
236
|
alias_method :to_s, :inspect
|
|
222
237
|
|
|
223
|
-
# pretty-prints a representation this
|
|
238
|
+
# pretty-prints a representation of this arraylike to the given printer
|
|
224
239
|
# @return [void]
|
|
225
240
|
def pretty_print(q)
|
|
226
|
-
object_group_str = (respond_to?(:
|
|
241
|
+
object_group_str = (respond_to?(:jsi_object_group_text) ? jsi_object_group_text : [self.class]).join(' ')
|
|
227
242
|
q.text "\#[<#{object_group_str}>"
|
|
228
243
|
q.group_sub {
|
|
229
244
|
q.nest(2) {
|
|
230
|
-
q.breakable(
|
|
245
|
+
q.breakable(empty? ? '' : ' ')
|
|
231
246
|
q.seplist(self, nil, :each) { |e|
|
|
232
247
|
q.pp e
|
|
233
248
|
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JSI
|
|
4
|
+
module Util
|
|
5
|
+
# like a Struct, but stores all the attributes in one @attributes Hash, instead of individual instance
|
|
6
|
+
# variables for each attribute.
|
|
7
|
+
# this tends to be easier to work with and more flexible. keys which are symbols are converted to strings.
|
|
8
|
+
class AttrStruct
|
|
9
|
+
class AttrStructError < StandardError
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
class UndefinedAttributeKey < AttrStructError
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
class << self
|
|
16
|
+
# creates a AttrStruct subclass with the given attribute keys.
|
|
17
|
+
# @param attribute_keys [Enumerable<String, Symbol>]
|
|
18
|
+
def [](*attribute_keys)
|
|
19
|
+
unless self == AttrStruct
|
|
20
|
+
# :nocov:
|
|
21
|
+
raise(NotImplementedError, "AttrStruct multiple inheritance not supported")
|
|
22
|
+
# :nocov:
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
bad = attribute_keys.reject { |key| key.respond_to?(:to_str) || key.is_a?(Symbol) }
|
|
26
|
+
unless bad.empty?
|
|
27
|
+
raise ArgumentError, "attribute keys must be String or Symbol; got keys: #{bad.map(&:inspect).join(', ')}"
|
|
28
|
+
end
|
|
29
|
+
attribute_keys = attribute_keys.map { |key| key.is_a?(Symbol) ? key.to_s : key }
|
|
30
|
+
|
|
31
|
+
Class.new(AttrStruct).tap do |klass|
|
|
32
|
+
klass.define_singleton_method(:attribute_keys) { attribute_keys }
|
|
33
|
+
klass.send(:define_method, :attribute_keys) { attribute_keys }
|
|
34
|
+
attribute_keys.each do |attribute_key|
|
|
35
|
+
# reader
|
|
36
|
+
klass.send(:define_method, attribute_key) do
|
|
37
|
+
@attributes[attribute_key]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# writer
|
|
41
|
+
klass.send(:define_method, "#{attribute_key}=") do |value|
|
|
42
|
+
@attributes[attribute_key] = value
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def initialize(attributes = {})
|
|
50
|
+
unless attributes.respond_to?(:to_hash)
|
|
51
|
+
raise(TypeError, "expected attributes to be a Hash; got: #{attributes.inspect}")
|
|
52
|
+
end
|
|
53
|
+
attributes = attributes.map { |k, v| {k.is_a?(Symbol) ? k.to_s : k => v} }.inject({}, &:update)
|
|
54
|
+
bad = attributes.keys.reject { |k| self.attribute_keys.include?(k) }
|
|
55
|
+
unless bad.empty?
|
|
56
|
+
raise UndefinedAttributeKey, "undefined attribute keys: #{bad.map(&:inspect).join(', ')}"
|
|
57
|
+
end
|
|
58
|
+
@attributes = attributes
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def [](key)
|
|
62
|
+
key = key.to_s if key.is_a?(Symbol)
|
|
63
|
+
@attributes[key]
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def []=(key, value)
|
|
67
|
+
key = key.to_s if key.is_a?(Symbol)
|
|
68
|
+
unless self.attribute_keys.include?(key)
|
|
69
|
+
raise UndefinedAttributeKey, "undefined attribute key: #{key.inspect}"
|
|
70
|
+
end
|
|
71
|
+
@attributes[key] = value
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# @return [String]
|
|
75
|
+
def inspect
|
|
76
|
+
"\#<#{self.class.name}#{@attributes.empty? ? '' : ' '}#{@attributes.map { |k, v| "#{k}: #{v.inspect}" }.join(', ')}>"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# pretty-prints a representation of self to the given printer
|
|
80
|
+
# @return [void]
|
|
81
|
+
def pretty_print(q)
|
|
82
|
+
q.text '#<'
|
|
83
|
+
q.text self.class.name
|
|
84
|
+
q.group_sub {
|
|
85
|
+
q.nest(2) {
|
|
86
|
+
q.breakable(@attributes.empty? ? '' : ' ')
|
|
87
|
+
q.seplist(@attributes, nil, :each_pair) { |k, v|
|
|
88
|
+
q.group {
|
|
89
|
+
q.text k
|
|
90
|
+
q.text ': '
|
|
91
|
+
q.pp v
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
q.breakable ''
|
|
97
|
+
q.text '>'
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
include FingerprintHash
|
|
101
|
+
def jsi_fingerprint
|
|
102
|
+
{class: self.class, attributes: @attributes}
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
data/lib/jsi/util.rb
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module JSI
|
|
4
|
-
# JSI::Util classes, modules, constants, and methods are
|
|
5
|
-
#
|
|
4
|
+
# JSI::Util classes, modules, constants, and methods are internal, and will be added and removed without warning.
|
|
5
|
+
#
|
|
6
|
+
# @api private
|
|
6
7
|
module Util
|
|
7
|
-
|
|
8
|
-
NOOP = -> (*_) { }
|
|
8
|
+
autoload :AttrStruct, 'jsi/util/attr_struct'
|
|
9
9
|
|
|
10
|
-
#
|
|
10
|
+
# a hash copied from the given hashlike, in which any symbol keys are
|
|
11
11
|
# converted to strings. behavior on collisions is undefined (but in the
|
|
12
12
|
# future could take a block like
|
|
13
13
|
# ActiveSupport::HashWithIndifferentAccess#update)
|
|
@@ -54,24 +54,63 @@ module JSI
|
|
|
54
54
|
end
|
|
55
55
|
end
|
|
56
56
|
|
|
57
|
+
# ensures the given param becomes a frozen Set of Modules.
|
|
58
|
+
# returns the param if it is already that, otherwise initializes and freezes such a Set.
|
|
59
|
+
#
|
|
60
|
+
# @param modules [Set, Enumerable] the object to ensure becomes a frozen Set of Modules
|
|
61
|
+
# @return [Set] frozen Set containing the given modules
|
|
62
|
+
# @raise [ArgumentError] when the modules param is not an Enumerable
|
|
63
|
+
# @raise [Schema::NotASchemaError] when the modules param contains objects which are not Schemas
|
|
64
|
+
def ensure_module_set(modules)
|
|
65
|
+
if modules.is_a?(Set) && modules.frozen?
|
|
66
|
+
set = modules
|
|
67
|
+
else
|
|
68
|
+
set = Set.new(modules).freeze
|
|
69
|
+
end
|
|
70
|
+
not_modules = set.reject { |s| s.is_a?(Module) }
|
|
71
|
+
if !not_modules.empty?
|
|
72
|
+
raise(TypeError, [
|
|
73
|
+
"ensure_module_set given non-Module objects:",
|
|
74
|
+
*not_modules.map { |ns| ns.pretty_inspect.chomp },
|
|
75
|
+
].join("\n"))
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
set
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# is the given name ok to use as a ruby method name?
|
|
82
|
+
def ok_ruby_method_name?(name)
|
|
83
|
+
# must be a string
|
|
84
|
+
return false unless name.respond_to?(:to_str)
|
|
85
|
+
# must not begin with a digit
|
|
86
|
+
return false if name =~ /\A[0-9]/
|
|
87
|
+
# must not contain characters special to ruby syntax
|
|
88
|
+
return false if name =~ /[\\\s\#;\.,\(\)\[\]\{\}'"`%\+\-\/\*\^\|&=<>\?:!@\$~]/
|
|
89
|
+
|
|
90
|
+
return true
|
|
91
|
+
end
|
|
92
|
+
|
|
57
93
|
# this is the Y-combinator, which allows anonymous recursive functions. for a simple example,
|
|
58
94
|
# to define a recursive function to return the length of an array:
|
|
59
95
|
#
|
|
60
|
-
#
|
|
61
|
-
#
|
|
62
|
-
#
|
|
96
|
+
# length = ycomb do |len|
|
|
97
|
+
# proc { |list| list == [] ? 0 : 1 + len.call(list[1..-1]) }
|
|
98
|
+
# end
|
|
99
|
+
#
|
|
100
|
+
# length.call([0])
|
|
101
|
+
# # => 1
|
|
63
102
|
#
|
|
64
|
-
# see https://
|
|
65
|
-
# and chapter 9 of the little schemer, available as the sample chapter at
|
|
103
|
+
# see https://en.wikipedia.org/wiki/Fixed-point_combinator#Y_combinator
|
|
104
|
+
# and chapter 9 of the little schemer, available as the sample chapter at
|
|
105
|
+
# https://felleisen.org/matthias/BTLS-index.html
|
|
66
106
|
def ycomb
|
|
67
107
|
proc { |f| f.call(f) }.call(proc { |f| yield proc { |*x| f.call(f).call(*x) } })
|
|
68
108
|
end
|
|
69
|
-
module_function :ycomb
|
|
70
109
|
|
|
71
110
|
module FingerprintHash
|
|
72
111
|
# overrides BasicObject#==
|
|
73
112
|
def ==(other)
|
|
74
|
-
|
|
113
|
+
__id__ == other.__id__ || (other.respond_to?(:jsi_fingerprint) && other.jsi_fingerprint == self.jsi_fingerprint)
|
|
75
114
|
end
|
|
76
115
|
|
|
77
116
|
alias_method :eql?, :==
|
|
@@ -82,26 +121,103 @@ module JSI
|
|
|
82
121
|
end
|
|
83
122
|
end
|
|
84
123
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
124
|
+
class MemoMap
|
|
125
|
+
Result = Util::AttrStruct[*%w(
|
|
126
|
+
value
|
|
127
|
+
inputs
|
|
128
|
+
inputs_hash
|
|
129
|
+
)]
|
|
130
|
+
|
|
131
|
+
class Result
|
|
92
132
|
end
|
|
93
133
|
|
|
94
|
-
def
|
|
95
|
-
@
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
134
|
+
def initialize(key_by: nil, &block)
|
|
135
|
+
@key_by = key_by
|
|
136
|
+
@block = block
|
|
137
|
+
|
|
138
|
+
# each result has its own mutex to update its memoized value thread-safely
|
|
139
|
+
@result_mutexes = {}
|
|
140
|
+
# another mutex to thread-safely initialize each result mutex
|
|
141
|
+
@result_mutexes_mutex = Mutex.new
|
|
142
|
+
|
|
143
|
+
@results = {}
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def [](*inputs)
|
|
147
|
+
if @key_by
|
|
148
|
+
key = @key_by.call(*inputs)
|
|
149
|
+
else
|
|
150
|
+
key = inputs
|
|
151
|
+
end
|
|
152
|
+
result_mutex = @result_mutexes_mutex.synchronize do
|
|
153
|
+
@result_mutexes[key] ||= Mutex.new
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
result_mutex.synchronize do
|
|
157
|
+
inputs_hash = inputs.hash
|
|
158
|
+
if @results.key?(key) && inputs_hash == @results[key].inputs_hash && inputs == @results[key].inputs
|
|
159
|
+
@results[key].value
|
|
99
160
|
else
|
|
100
|
-
@
|
|
161
|
+
value = @block.call(*inputs)
|
|
162
|
+
@results[key] = Result.new(value: value, inputs: inputs, inputs_hash: inputs_hash)
|
|
163
|
+
value
|
|
101
164
|
end
|
|
102
165
|
end
|
|
103
166
|
end
|
|
104
167
|
end
|
|
168
|
+
|
|
169
|
+
module Memoize
|
|
170
|
+
def self.extended(object)
|
|
171
|
+
object.send(:jsi_initialize_memos)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
private
|
|
175
|
+
|
|
176
|
+
def jsi_initialize_memos
|
|
177
|
+
@jsi_memomaps_mutex = Mutex.new
|
|
178
|
+
@jsi_memomaps = {}
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# @return [Util::MemoMap]
|
|
182
|
+
def jsi_memomap(name, **options, &block)
|
|
183
|
+
raise(Bug, 'must jsi_initialize_memos') unless @jsi_memomaps
|
|
184
|
+
unless @jsi_memomaps.key?(name)
|
|
185
|
+
@jsi_memomaps_mutex.synchronize do
|
|
186
|
+
# note: this ||= appears redundant with `unless @jsi_memomaps.key?(name)`,
|
|
187
|
+
# but that check is not thread safe. this check is.
|
|
188
|
+
@jsi_memomaps[name] ||= Util::MemoMap.new(**options, &block)
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
@jsi_memomaps[name]
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def jsi_memoize(name, *inputs, &block)
|
|
195
|
+
jsi_memomap(name, &block)[*inputs]
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
module Virtual
|
|
200
|
+
class InstantiationError < StandardError
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# this virtual class is not intended to be instantiated except by its subclasses, which override #initialize
|
|
204
|
+
def initialize
|
|
205
|
+
# :nocov:
|
|
206
|
+
raise(InstantiationError, "cannot instantiate virtual class #{self.class}")
|
|
207
|
+
# :nocov:
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# virtual_method is used to indicate that the method calling it must be implemented on the (non-virtual) subclass
|
|
211
|
+
def virtual_method
|
|
212
|
+
# :nocov:
|
|
213
|
+
raise(Bug, "class #{self.class} must implement #{caller_locations.first.label}")
|
|
214
|
+
# :nocov:
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
public
|
|
219
|
+
|
|
220
|
+
extend self
|
|
105
221
|
end
|
|
106
222
|
public
|
|
107
223
|
extend Util
|
|
@@ -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
|