jsi 0.2.0 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.yardopts +1 -1
- data/CHANGELOG.md +36 -0
- data/LICENSE.md +613 -0
- data/README.md +153 -52
- data/lib/jsi/base.rb +485 -338
- data/lib/jsi/jsi_coder.rb +24 -18
- data/lib/jsi/metaschema.rb +7 -0
- data/lib/jsi/metaschema_node/bootstrap_schema.rb +100 -0
- data/lib/jsi/metaschema_node.rb +245 -0
- data/lib/jsi/pathed_node.rb +49 -46
- 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 +528 -233
- data/lib/jsi/schema_classes.rb +238 -51
- data/lib/jsi/schema_registry.rb +141 -0
- data/lib/jsi/schema_set.rb +141 -0
- data/lib/jsi/simple_wrap.rb +8 -3
- data/lib/jsi/typelike_modules.rb +75 -68
- data/lib/jsi/util/attr_struct.rb +106 -0
- data/lib/jsi/util.rb +167 -64
- 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 +72 -9
- data/lib/schemas/json-schema.org/draft-04/schema.rb +12 -0
- data/lib/schemas/json-schema.org/draft-06/schema.rb +12 -0
- 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 +80 -107
- data/.simplecov +0 -1
- data/LICENSE.txt +0 -21
- data/Rakefile.rb +0 -9
- data/jsi.gemspec +0 -31
- data/lib/jsi/base/to_rb.rb +0 -126
- data/lib/jsi/json/node.rb +0 -243
- data/lib/jsi/json/pointer.rb +0 -330
- data/lib/jsi/json-schema-fragments.rb +0 -59
- data/lib/jsi/json.rb +0 -8
- data/test/base_array_test.rb +0 -209
- data/test/base_hash_test.rb +0 -204
- data/test/base_test.rb +0 -422
- 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 -310
- data/test/jsi_json_pointer_test.rb +0 -106
- data/test/jsi_test.rb +0 -11
- data/test/jsi_typelike_as_json_test.rb +0 -53
- data/test/schema_test.rb +0 -196
- data/test/spreedly_openapi_test.rb +0 -8
- data/test/test_helper.rb +0 -63
- data/test/util_test.rb +0 -62
data/lib/jsi/typelike_modules.rb
CHANGED
@@ -1,10 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module JSI
|
2
4
|
# a module relating to objects that act like Hash or Array instances
|
3
5
|
module Typelike
|
4
|
-
# yields the content of the given param `object`. for objects which have a
|
5
|
-
#
|
6
|
-
#
|
7
|
-
# 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.
|
8
9
|
#
|
9
10
|
# the given block must result in a modified copy of its block parameter
|
10
11
|
# (not destructively modifying the yielded content).
|
@@ -13,8 +14,8 @@ module JSI
|
|
13
14
|
# in a (nondestructively) modified copy of this.
|
14
15
|
# @return [object.class] modified copy of the given object
|
15
16
|
def self.modified_copy(object, &block)
|
16
|
-
if object.respond_to?(:
|
17
|
-
object.
|
17
|
+
if object.respond_to?(:jsi_modified_copy)
|
18
|
+
object.jsi_modified_copy(&block)
|
18
19
|
else
|
19
20
|
return yield(object)
|
20
21
|
end
|
@@ -25,8 +26,8 @@ module JSI
|
|
25
26
|
# will raise TypeError if an object is given that is not a type that seems
|
26
27
|
# to be expressable as json.
|
27
28
|
#
|
28
|
-
# similar effect could be achieved by requiring 'json/add/core' and using
|
29
|
-
#
|
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
|
30
31
|
# not naturally expressable in JSON, and prefer not to load its
|
31
32
|
# monkey-patching.
|
32
33
|
#
|
@@ -37,7 +38,7 @@ module JSI
|
|
37
38
|
# array of object) cannot be expressed as json
|
38
39
|
def self.as_json(object, *opt)
|
39
40
|
if object.is_a?(JSI::PathedNode)
|
40
|
-
as_json(object.
|
41
|
+
as_json(object.jsi_node_content, *opt)
|
41
42
|
elsif object.respond_to?(:to_hash)
|
42
43
|
(object.respond_to?(:map) ? object : object.to_hash).map do |k, v|
|
43
44
|
unless k.is_a?(Symbol) || k.respond_to?(:to_str)
|
@@ -84,7 +85,7 @@ module JSI
|
|
84
85
|
end
|
85
86
|
safe_modified_copy_methods.each do |method_name|
|
86
87
|
define_method(method_name) do |*a, &b|
|
87
|
-
|
88
|
+
jsi_modified_copy do |object_to_modify|
|
88
89
|
responsive_object = object_to_modify.respond_to?(method_name) ? object_to_modify : object_to_modify.to_hash
|
89
90
|
responsive_object.public_send(method_name, *a, &b)
|
90
91
|
end
|
@@ -92,19 +93,19 @@ module JSI
|
|
92
93
|
end
|
93
94
|
safe_kv_block_modified_copy_methods.each do |method_name|
|
94
95
|
define_method(method_name) do |*a, &b|
|
95
|
-
|
96
|
+
jsi_modified_copy do |object_to_modify|
|
96
97
|
responsive_object = object_to_modify.respond_to?(method_name) ? object_to_modify : object_to_modify.to_hash
|
97
|
-
responsive_object.public_send(method_name
|
98
|
-
b.call(k, self[k])
|
98
|
+
responsive_object.public_send(method_name) do |k, _v|
|
99
|
+
b.call(k, self[k, *a])
|
99
100
|
end
|
100
101
|
end
|
101
102
|
end
|
102
103
|
end
|
103
104
|
|
104
|
-
#
|
105
|
+
# like [Hash#update](https://ruby-doc.org/core/Hash.html#method-i-update)
|
105
106
|
# @param other [#to_hash] the other hash to update this hash from
|
106
107
|
# @yield [key, oldval, newval] for entries with duplicate keys, the value of each duplicate key
|
107
|
-
# 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.
|
108
109
|
# @return self, updated with other
|
109
110
|
# @raise [TypeError] when `other` does not respond to #to_hash
|
110
111
|
def update(other, &block)
|
@@ -113,7 +114,7 @@ module JSI
|
|
113
114
|
end
|
114
115
|
self_respondingto_key = self.respond_to?(:key?) ? self : to_hash
|
115
116
|
other.to_hash.each_pair do |key, value|
|
116
|
-
if
|
117
|
+
if block && self_respondingto_key.key?(key)
|
117
118
|
value = yield(key, self[key], value)
|
118
119
|
end
|
119
120
|
self[key] = value
|
@@ -123,49 +124,45 @@ module JSI
|
|
123
124
|
|
124
125
|
alias_method :merge!, :update
|
125
126
|
|
126
|
-
#
|
127
|
+
# like [Hash#merge](https://ruby-doc.org/core/Hash.html#method-i-merge)
|
127
128
|
# @param other [#to_hash] the other hash to merge into this
|
128
129
|
# @yield [key, oldval, newval] for entries with duplicate keys, the value of each duplicate key
|
129
|
-
# 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.
|
130
131
|
# @return duplicate of this hash with the other hash merged in
|
131
132
|
# @raise [TypeError] when `other` does not respond to #to_hash
|
132
133
|
def merge(other, &block)
|
133
134
|
dup.update(other, &block)
|
134
135
|
end
|
135
136
|
|
136
|
-
#
|
137
|
-
#
|
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]
|
138
140
|
def inspect
|
139
|
-
|
140
|
-
"\#{<#{
|
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(',')}}"
|
141
143
|
end
|
142
144
|
|
143
|
-
|
144
|
-
def to_s
|
145
|
-
inspect
|
146
|
-
end
|
145
|
+
alias_method :to_s, :inspect
|
147
146
|
|
148
|
-
# pretty-prints a representation this
|
147
|
+
# pretty-prints a representation of this hashlike to the given printer
|
149
148
|
# @return [void]
|
150
149
|
def pretty_print(q)
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
pp v
|
162
|
-
}
|
150
|
+
object_group_str = (respond_to?(:jsi_object_group_text) ? jsi_object_group_text : [self.class]).join(' ')
|
151
|
+
q.text "\#{<#{object_group_str}>"
|
152
|
+
q.group_sub {
|
153
|
+
q.nest(2) {
|
154
|
+
q.breakable(empty? ? '' : ' ')
|
155
|
+
q.seplist(self, nil, :each_pair) { |k, v|
|
156
|
+
q.group {
|
157
|
+
q.pp k
|
158
|
+
q.text ' => '
|
159
|
+
q.pp v
|
163
160
|
}
|
164
161
|
}
|
165
162
|
}
|
166
|
-
|
167
|
-
|
168
|
-
|
163
|
+
}
|
164
|
+
q.breakable ''
|
165
|
+
q.text '}'
|
169
166
|
end
|
170
167
|
end
|
171
168
|
|
@@ -180,7 +177,7 @@ module JSI
|
|
180
177
|
# methods which do not need to access the element.
|
181
178
|
SAFE_INDEX_ONLY_METHODS = %w(each_index empty? length size)
|
182
179
|
# there are some ambiguous ones that are omitted, like #sort, #map / #collect.
|
183
|
-
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)
|
184
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)
|
185
182
|
|
186
183
|
# methods (well, method) that returns a modified copy and doesn't need any handling of block variable(s)
|
@@ -196,7 +193,7 @@ module JSI
|
|
196
193
|
end
|
197
194
|
safe_modified_copy_methods.each do |method_name|
|
198
195
|
define_method(method_name) do |*a, &b|
|
199
|
-
|
196
|
+
jsi_modified_copy do |object_to_modify|
|
200
197
|
responsive_object = object_to_modify.respond_to?(method_name) ? object_to_modify : object_to_modify.to_ary
|
201
198
|
responsive_object.public_send(method_name, *a, &b)
|
202
199
|
end
|
@@ -204,45 +201,55 @@ module JSI
|
|
204
201
|
end
|
205
202
|
safe_el_block_methods.each do |method_name|
|
206
203
|
define_method(method_name) do |*a, &b|
|
207
|
-
|
204
|
+
jsi_modified_copy do |object_to_modify|
|
208
205
|
i = 0
|
209
206
|
responsive_object = object_to_modify.respond_to?(method_name) ? object_to_modify : object_to_modify.to_ary
|
210
|
-
responsive_object.public_send(method_name
|
211
|
-
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 }
|
212
209
|
end
|
213
210
|
end
|
214
211
|
end
|
215
212
|
end
|
216
213
|
|
217
|
-
#
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
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 }
|
222
226
|
end
|
223
227
|
|
224
|
-
#
|
225
|
-
|
226
|
-
|
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]
|
231
|
+
def inspect
|
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(',')}]"
|
227
234
|
end
|
228
235
|
|
229
|
-
|
236
|
+
alias_method :to_s, :inspect
|
237
|
+
|
238
|
+
# pretty-prints a representation of this arraylike to the given printer
|
230
239
|
# @return [void]
|
231
240
|
def pretty_print(q)
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
pp e
|
240
|
-
}
|
241
|
+
object_group_str = (respond_to?(:jsi_object_group_text) ? jsi_object_group_text : [self.class]).join(' ')
|
242
|
+
q.text "\#[<#{object_group_str}>"
|
243
|
+
q.group_sub {
|
244
|
+
q.nest(2) {
|
245
|
+
q.breakable(empty? ? '' : ' ')
|
246
|
+
q.seplist(self, nil, :each) { |e|
|
247
|
+
q.pp e
|
241
248
|
}
|
242
249
|
}
|
243
|
-
|
244
|
-
|
245
|
-
|
250
|
+
}
|
251
|
+
q.breakable ''
|
252
|
+
q.text ']'
|
246
253
|
end
|
247
254
|
end
|
248
255
|
end
|
@@ -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,9 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module JSI
|
4
|
+
# JSI::Util classes, modules, constants, and methods are internal, and will be added and removed without warning.
|
5
|
+
#
|
6
|
+
# @api private
|
2
7
|
module Util
|
3
|
-
|
4
|
-
NOOP = -> (*_) { }
|
8
|
+
autoload :AttrStruct, 'jsi/util/attr_struct'
|
5
9
|
|
6
|
-
#
|
10
|
+
# a hash copied from the given hashlike, in which any symbol keys are
|
7
11
|
# converted to strings. behavior on collisions is undefined (but in the
|
8
12
|
# future could take a block like
|
9
13
|
# ActiveSupport::HashWithIndifferentAccess#update)
|
@@ -14,108 +18,207 @@ module JSI
|
|
14
18
|
# the return if you need to ensure it is not the same instance as the
|
15
19
|
# argument instance.
|
16
20
|
#
|
17
|
-
# @param
|
21
|
+
# @param hashlike [#to_hash] the hash from which to convert symbol keys to strings
|
18
22
|
# @return [same class as the param `hash`, or Hash if the former cannot be done] a
|
19
23
|
# hash(-like) instance containing no symbol keys
|
20
|
-
def stringify_symbol_keys(
|
21
|
-
unless
|
22
|
-
raise(ArgumentError, "expected argument to be a hash; got #{
|
24
|
+
def stringify_symbol_keys(hashlike)
|
25
|
+
unless hashlike.respond_to?(:to_hash)
|
26
|
+
raise(ArgumentError, "expected argument to be a hash; got #{hashlike.class.inspect}: #{hashlike.pretty_inspect.chomp}")
|
23
27
|
end
|
24
|
-
JSI::Typelike.modified_copy(
|
25
|
-
changed = false
|
28
|
+
JSI::Typelike.modified_copy(hashlike) do |hash|
|
26
29
|
out = {}
|
27
|
-
|
28
|
-
|
29
|
-
changed = true
|
30
|
-
k = k.to_s
|
31
|
-
end
|
32
|
-
out[k] = v
|
30
|
+
hash.each do |k, v|
|
31
|
+
out[k.is_a?(Symbol) ? k.to_s : k] = v
|
33
32
|
end
|
34
|
-
|
33
|
+
out
|
35
34
|
end
|
36
35
|
end
|
37
36
|
|
38
37
|
def deep_stringify_symbol_keys(object)
|
39
38
|
if object.respond_to?(:to_hash)
|
40
39
|
JSI::Typelike.modified_copy(object) do |hash|
|
41
|
-
changed = false
|
42
40
|
out = {}
|
43
41
|
(hash.respond_to?(:each) ? hash : hash.to_hash).each do |k, v|
|
44
|
-
|
45
|
-
changed = true
|
46
|
-
k = k.to_s
|
47
|
-
end
|
48
|
-
out_k = deep_stringify_symbol_keys(k)
|
49
|
-
out_v = deep_stringify_symbol_keys(v)
|
50
|
-
changed = true if out_k.object_id != k.object_id
|
51
|
-
changed = true if out_v.object_id != v.object_id
|
52
|
-
out[out_k] = out_v
|
42
|
+
out[k.is_a?(Symbol) ? k.to_s : deep_stringify_symbol_keys(k)] = deep_stringify_symbol_keys(v)
|
53
43
|
end
|
54
|
-
|
44
|
+
out
|
55
45
|
end
|
56
46
|
elsif object.respond_to?(:to_ary)
|
57
47
|
JSI::Typelike.modified_copy(object) do |ary|
|
58
|
-
|
59
|
-
|
60
|
-
out_e = deep_stringify_symbol_keys(e)
|
61
|
-
changed = true if out_e.object_id != e.object_id
|
62
|
-
out_e
|
48
|
+
(ary.respond_to?(:each) ? ary : ary.to_ary).map do |e|
|
49
|
+
deep_stringify_symbol_keys(e)
|
63
50
|
end
|
64
|
-
changed ? out : ary
|
65
51
|
end
|
66
52
|
else
|
67
53
|
object
|
68
54
|
end
|
69
55
|
end
|
70
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
|
+
|
71
93
|
# this is the Y-combinator, which allows anonymous recursive functions. for a simple example,
|
72
94
|
# to define a recursive function to return the length of an array:
|
73
95
|
#
|
74
|
-
#
|
75
|
-
#
|
76
|
-
#
|
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
|
77
102
|
#
|
78
|
-
# see https://
|
79
|
-
# 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
|
80
106
|
def ycomb
|
81
107
|
proc { |f| f.call(f) }.call(proc { |f| yield proc { |*x| f.call(f).call(*x) } })
|
82
108
|
end
|
83
|
-
module_function :ycomb
|
84
|
-
end
|
85
|
-
public
|
86
|
-
extend Util
|
87
109
|
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
110
|
+
module FingerprintHash
|
111
|
+
# overrides BasicObject#==
|
112
|
+
def ==(other)
|
113
|
+
__id__ == other.__id__ || (other.respond_to?(:jsi_fingerprint) && other.jsi_fingerprint == self.jsi_fingerprint)
|
114
|
+
end
|
92
115
|
|
93
|
-
|
116
|
+
alias_method :eql?, :==
|
94
117
|
|
95
|
-
|
96
|
-
|
118
|
+
# overrides Kernel#hash
|
119
|
+
def hash
|
120
|
+
jsi_fingerprint.hash
|
121
|
+
end
|
97
122
|
end
|
98
|
-
end
|
99
123
|
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
124
|
+
class MemoMap
|
125
|
+
Result = Util::AttrStruct[*%w(
|
126
|
+
value
|
127
|
+
inputs
|
128
|
+
inputs_hash
|
129
|
+
)]
|
130
|
+
|
131
|
+
class Result
|
105
132
|
end
|
106
|
-
@memos[key][args_]
|
107
|
-
end
|
108
133
|
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
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)
|
114
149
|
else
|
115
|
-
|
150
|
+
key = inputs
|
151
|
+
end
|
152
|
+
result_mutex = @result_mutexes_mutex.synchronize do
|
153
|
+
@result_mutexes[key] ||= Mutex.new
|
116
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
|
160
|
+
else
|
161
|
+
value = @block.call(*inputs)
|
162
|
+
@results[key] = Result.new(value: value, inputs: inputs, inputs_hash: inputs_hash)
|
163
|
+
value
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
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]
|
117
196
|
end
|
118
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
|
119
221
|
end
|
120
|
-
|
222
|
+
public
|
223
|
+
extend Util
|
121
224
|
end
|