jsi 0.2.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 +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
|