jsi 0.0.1
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/.simplecov +1 -0
- data/CHANGELOG.md +3 -0
- data/LICENSE.txt +21 -0
- data/README.md +160 -0
- data/Rakefile.rb +9 -0
- data/jsi.gemspec +31 -0
- data/lib/jsi.rb +28 -0
- data/lib/jsi/base.rb +325 -0
- data/lib/jsi/base/to_rb.rb +127 -0
- data/lib/jsi/json-schema-fragments.rb +191 -0
- data/lib/jsi/json.rb +7 -0
- data/lib/jsi/json/node.rb +256 -0
- data/lib/jsi/schema.rb +249 -0
- data/lib/jsi/schema_instance_json_coder.rb +83 -0
- data/lib/jsi/struct_json_coder.rb +30 -0
- data/lib/jsi/typelike_modules.rb +164 -0
- data/lib/jsi/util.rb +103 -0
- data/lib/jsi/version.rb +3 -0
- data/test/base_array_test.rb +142 -0
- data/test/base_hash_test.rb +135 -0
- data/test/base_test.rb +395 -0
- data/test/jsi_json_arraynode_test.rb +133 -0
- data/test/jsi_json_hashnode_test.rb +117 -0
- data/test/jsi_json_node_test.rb +288 -0
- data/test/jsi_test.rb +11 -0
- data/test/schema_instance_json_coder_test.rb +122 -0
- data/test/struct_json_coder_test.rb +130 -0
- data/test/test_helper.rb +29 -0
- data/test/util_test.rb +62 -0
- metadata +155 -0
data/lib/jsi/schema.rb
ADDED
@@ -0,0 +1,249 @@
|
|
1
|
+
require 'jsi/json/node'
|
2
|
+
|
3
|
+
module JSI
|
4
|
+
class Schema
|
5
|
+
include Memoize
|
6
|
+
|
7
|
+
def initialize(schema_object)
|
8
|
+
if schema_object.is_a?(JSI::Schema)
|
9
|
+
raise(TypeError, "will not instantiate Schema from another Schema: #{schema_object.pretty_inspect.chomp}")
|
10
|
+
elsif schema_object.is_a?(JSI::Base)
|
11
|
+
@schema_object = JSI.deep_stringify_symbol_keys(schema_object.deref)
|
12
|
+
@schema_node = @schema_object.instance
|
13
|
+
elsif schema_object.is_a?(JSI::JSON::HashNode)
|
14
|
+
@schema_object = nil
|
15
|
+
@schema_node = JSI.deep_stringify_symbol_keys(schema_object.deref)
|
16
|
+
elsif schema_object.respond_to?(:to_hash)
|
17
|
+
@schema_object = nil
|
18
|
+
@schema_node = JSI::JSON::Node.new_by_type(JSI.deep_stringify_symbol_keys(schema_object), [])
|
19
|
+
else
|
20
|
+
raise(TypeError, "cannot instantiate Schema from: #{schema_object.pretty_inspect.chomp}")
|
21
|
+
end
|
22
|
+
if @schema_object
|
23
|
+
define_singleton_method(:instance) { schema_node } # aka schema_object.instance
|
24
|
+
define_singleton_method(:schema) { schema_object.schema }
|
25
|
+
extend BaseHash
|
26
|
+
else
|
27
|
+
define_singleton_method(:[]) { |*a, &b| schema_node.public_send(:[], *a, &b) }
|
28
|
+
end
|
29
|
+
end
|
30
|
+
attr_reader :schema_node
|
31
|
+
def schema_object
|
32
|
+
@schema_object || @schema_node
|
33
|
+
end
|
34
|
+
|
35
|
+
def schema_id
|
36
|
+
@schema_id ||= begin
|
37
|
+
# start from schema_node and ascend parents looking for an 'id' property.
|
38
|
+
# append a fragment to that id (appending to an existing fragment if there
|
39
|
+
# is one) consisting of the path from that parent to our schema_node.
|
40
|
+
node_for_id = schema_node
|
41
|
+
path_from_id_node = []
|
42
|
+
done = false
|
43
|
+
|
44
|
+
while !done
|
45
|
+
# TODO: track what parents are schemas. somehow.
|
46
|
+
# look at 'id' if node_for_id is a schema, or the document root.
|
47
|
+
# decide whether to look at '$id' for all parent nodes or also just schemas.
|
48
|
+
if node_for_id.respond_to?(:to_hash)
|
49
|
+
if node_for_id.path.empty? || node_for_id.object_id == schema_node.object_id
|
50
|
+
# I'm only looking at 'id' for the document root and the schema node
|
51
|
+
# until I track what parents are schemas.
|
52
|
+
parent_id = node_for_id['$id'] || node_for_id['id']
|
53
|
+
else
|
54
|
+
# will look at '$id' everywhere since it is less likely to show up outside schemas than
|
55
|
+
# 'id', but it will be better to only look at parents that are schemas for this too.
|
56
|
+
parent_id = node_for_id['$id']
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
if parent_id || node_for_id.path.empty?
|
61
|
+
done = true
|
62
|
+
else
|
63
|
+
path_from_id_node.unshift(node_for_id.path.last)
|
64
|
+
node_for_id = node_for_id.parent_node
|
65
|
+
end
|
66
|
+
end
|
67
|
+
if parent_id
|
68
|
+
parent_auri = Addressable::URI.parse(parent_id)
|
69
|
+
else
|
70
|
+
node_for_id = schema_node.document_node
|
71
|
+
validator = ::JSON::Validator.new(node_for_id.content, nil)
|
72
|
+
# TODO not good instance_exec'ing into another library's ivars
|
73
|
+
parent_auri = validator.instance_exec { @base_schema }.uri
|
74
|
+
end
|
75
|
+
if parent_auri.fragment
|
76
|
+
# add onto the fragment
|
77
|
+
parent_id_path = ::JSON::Schema::Pointer.new(:fragment, '#' + parent_auri.fragment).reference_tokens
|
78
|
+
path_from_id_node = parent_id_path + path_from_id_node
|
79
|
+
parent_auri.fragment = nil
|
80
|
+
#else: no fragment so parent_id good as is
|
81
|
+
end
|
82
|
+
|
83
|
+
fragment = ::JSON::Schema::Pointer.new(:reference_tokens, path_from_id_node).fragment
|
84
|
+
schema_id = parent_auri.to_s + fragment
|
85
|
+
|
86
|
+
schema_id
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def schema_class
|
91
|
+
JSI.class_for_schema(self)
|
92
|
+
end
|
93
|
+
|
94
|
+
def match_to_instance(instance)
|
95
|
+
# matching oneOf is good here. one schema for one instance.
|
96
|
+
# matching anyOf is okay. there could be more than one schema matched. it's often just one. if more
|
97
|
+
# than one is a match, the problems of allOf occur.
|
98
|
+
# matching allOf is questionable. all of the schemas must be matched but we just return the first match.
|
99
|
+
# there isn't really a better answer with the current implementation. merging the schemas together
|
100
|
+
# is a thought but is not practical.
|
101
|
+
%w(oneOf allOf anyOf).select { |k| schema_node[k].respond_to?(:to_ary) }.each do |someof_key|
|
102
|
+
schema_node[someof_key].map(&:deref).map do |someof_node|
|
103
|
+
someof_schema = self.class.new(someof_node)
|
104
|
+
if someof_schema.validate(instance)
|
105
|
+
return someof_schema.match_to_instance(instance)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
return self
|
110
|
+
end
|
111
|
+
|
112
|
+
def subschema_for_property(property_name_)
|
113
|
+
memoize(:subschema_for_property, property_name_) do |property_name|
|
114
|
+
if schema_object['properties'].respond_to?(:to_hash) && schema_object['properties'][property_name].respond_to?(:to_hash)
|
115
|
+
self.class.new(schema_object['properties'][property_name])
|
116
|
+
else
|
117
|
+
if schema_object['patternProperties'].respond_to?(:to_hash)
|
118
|
+
_, pattern_schema_object = schema_object['patternProperties'].detect do |pattern, _|
|
119
|
+
property_name.to_s =~ Regexp.new(pattern) # TODO map pattern to ruby syntax
|
120
|
+
end
|
121
|
+
end
|
122
|
+
if pattern_schema_object
|
123
|
+
self.class.new(pattern_schema_object)
|
124
|
+
else
|
125
|
+
if schema_object['additionalProperties'].respond_to?(:to_hash)
|
126
|
+
self.class.new(schema_object['additionalProperties'])
|
127
|
+
else
|
128
|
+
nil
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def subschema_for_index(index_)
|
136
|
+
memoize(:subschema_for_index, index_) do |index|
|
137
|
+
if schema_object['items'].respond_to?(:to_ary)
|
138
|
+
if index < schema_object['items'].size
|
139
|
+
self.class.new(schema_object['items'][index])
|
140
|
+
elsif schema_object['additionalItems'].respond_to?(:to_hash)
|
141
|
+
self.class.new(schema_object['additionalItems'])
|
142
|
+
end
|
143
|
+
elsif schema_object['items'].respond_to?(:to_hash)
|
144
|
+
self.class.new(schema_object['items'])
|
145
|
+
else
|
146
|
+
nil
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def describes_array?
|
152
|
+
memoize(:describes_array?) do
|
153
|
+
schema_node['type'] == 'array' ||
|
154
|
+
schema_node['items'] ||
|
155
|
+
schema_node['additionalItems'] ||
|
156
|
+
schema_node['default'].respond_to?(:to_ary) || # TODO make sure this is right
|
157
|
+
(schema_node['enum'].respond_to?(:to_ary) && schema_node['enum'].all? { |enum| enum.respond_to?(:to_ary) }) ||
|
158
|
+
schema_node['maxItems'] ||
|
159
|
+
schema_node['minItems'] ||
|
160
|
+
schema_node.key?('uniqueItems') ||
|
161
|
+
schema_node['oneOf'].respond_to?(:to_ary) &&
|
162
|
+
schema_node['oneOf'].all? { |someof_node| self.class.new(someof_node).describes_array? } ||
|
163
|
+
schema_node['allOf'].respond_to?(:to_ary) &&
|
164
|
+
schema_node['allOf'].all? { |someof_node| self.class.new(someof_node).describes_array? } ||
|
165
|
+
schema_node['anyOf'].respond_to?(:to_ary) &&
|
166
|
+
schema_node['anyOf'].all? { |someof_node| self.class.new(someof_node).describes_array? }
|
167
|
+
end
|
168
|
+
end
|
169
|
+
def describes_hash?
|
170
|
+
memoize(:describes_hash?) do
|
171
|
+
schema_node['type'] == 'object' ||
|
172
|
+
schema_node['required'].respond_to?(:to_ary) ||
|
173
|
+
schema_node['properties'].respond_to?(:to_hash) ||
|
174
|
+
schema_node['additionalProperties'] ||
|
175
|
+
schema_node['patternProperties'] ||
|
176
|
+
schema_node['default'].respond_to?(:to_hash) ||
|
177
|
+
(schema_node['enum'].respond_to?(:to_ary) && schema_node['enum'].all? { |enum| enum.respond_to?(:to_hash) }) ||
|
178
|
+
schema_node['oneOf'].respond_to?(:to_ary) &&
|
179
|
+
schema_node['oneOf'].all? { |someof_node| self.class.new(someof_node).describes_hash? } ||
|
180
|
+
schema_node['allOf'].respond_to?(:to_ary) &&
|
181
|
+
schema_node['allOf'].all? { |someof_node| self.class.new(someof_node).describes_hash? } ||
|
182
|
+
schema_node['anyOf'].respond_to?(:to_ary) &&
|
183
|
+
schema_node['anyOf'].all? { |someof_node| self.class.new(someof_node).describes_hash? }
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
def described_hash_property_names
|
188
|
+
memoize(:described_hash_property_names) do
|
189
|
+
Set.new.tap do |property_names|
|
190
|
+
if schema_node['properties'].respond_to?(:to_hash)
|
191
|
+
property_names.merge(schema_node['properties'].keys)
|
192
|
+
end
|
193
|
+
if schema_node['required'].respond_to?(:to_ary)
|
194
|
+
property_names.merge(schema_node['required'].to_ary)
|
195
|
+
end
|
196
|
+
# we _could_ look at the properties of 'default' and each 'enum' but ... nah.
|
197
|
+
# we should look at dependencies (TODO).
|
198
|
+
%w(oneOf allOf anyOf).select { |k| schema_node[k].respond_to?(:to_ary) }.each do |schemas_key|
|
199
|
+
schema_node[schemas_key].map(&:deref).map do |someof_node|
|
200
|
+
property_names.merge(self.class.new(someof_node).described_hash_property_names)
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
def fully_validate(instance)
|
208
|
+
::JSON::Validator.fully_validate(schema_node.document, object_to_content(instance), fragment: schema_node.fragment)
|
209
|
+
end
|
210
|
+
def validate(instance)
|
211
|
+
::JSON::Validator.validate(schema_node.document, object_to_content(instance), fragment: schema_node.fragment)
|
212
|
+
end
|
213
|
+
def validate!(instance)
|
214
|
+
::JSON::Validator.validate!(schema_node.document, object_to_content(instance), fragment: schema_node.fragment)
|
215
|
+
end
|
216
|
+
|
217
|
+
def object_group_text
|
218
|
+
"schema_id=#{schema_id}"
|
219
|
+
end
|
220
|
+
def inspect
|
221
|
+
"\#<#{self.class.inspect} #{object_group_text} #{schema_object.inspect}>"
|
222
|
+
end
|
223
|
+
alias_method :to_s, :inspect
|
224
|
+
def pretty_print(q)
|
225
|
+
q.instance_exec(self) do |obj|
|
226
|
+
text "\#<#{obj.class.inspect} #{obj.object_group_text}"
|
227
|
+
group_sub {
|
228
|
+
nest(2) {
|
229
|
+
breakable ' '
|
230
|
+
pp obj.schema_object
|
231
|
+
}
|
232
|
+
}
|
233
|
+
breakable ''
|
234
|
+
text '>'
|
235
|
+
end
|
236
|
+
end
|
237
|
+
def fingerprint
|
238
|
+
{class: self.class, schema_node: schema_node}
|
239
|
+
end
|
240
|
+
include FingerprintHash
|
241
|
+
|
242
|
+
private
|
243
|
+
def object_to_content(object)
|
244
|
+
object = object.instance if object.is_a?(JSI::Base)
|
245
|
+
object = object.content if object.is_a?(JSI::JSON::Node)
|
246
|
+
object
|
247
|
+
end
|
248
|
+
end
|
249
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
module JSI
|
2
|
+
# this is a ActiveRecord serialization class intended to store JSON in the
|
3
|
+
# database column and expose a ruby class once loaded on a model instance.
|
4
|
+
# this allows for better ruby idioms to access to properties, and definition
|
5
|
+
# of related methods on the loaded class.
|
6
|
+
#
|
7
|
+
# the first argument, `loaded_class`, is the class which will be used to
|
8
|
+
# instantiate the column data. properties of the loaded class will correspond
|
9
|
+
# to keys of the json object in the database.
|
10
|
+
#
|
11
|
+
# the column data may be either a single instance of the loaded class
|
12
|
+
# (represented as one json object) or an array of them (represented as a json
|
13
|
+
# array of json objects), indicated by the keyword argument `array`.
|
14
|
+
#
|
15
|
+
# the column behind the attribute may be an actual JSON column (postgres json
|
16
|
+
# or jsonb - hstore should work too if you only have string attributes) or a
|
17
|
+
# serialized string, indicated by the keyword argument `string`.
|
18
|
+
class ObjectJSONCoder
|
19
|
+
class Error < StandardError
|
20
|
+
end
|
21
|
+
class LoadError < Error
|
22
|
+
end
|
23
|
+
class DumpError < Error
|
24
|
+
end
|
25
|
+
|
26
|
+
def initialize(loaded_class, string: false, array: false, next_coder: nil)
|
27
|
+
@loaded_class = loaded_class
|
28
|
+
# this notes the order of the keys as they were in the json, used by dump_object to generate
|
29
|
+
# json that is equivalent to the json/jsonifiable that came in, so that AR's #changed_attributes
|
30
|
+
# can tell whether the attribute has been changed.
|
31
|
+
@loaded_class.send(:attr_accessor, :object_json_coder_keys_order)
|
32
|
+
@string = string
|
33
|
+
@array = array
|
34
|
+
@next_coder = next_coder
|
35
|
+
end
|
36
|
+
|
37
|
+
def load(column_data)
|
38
|
+
return nil if column_data.nil?
|
39
|
+
data = @string ? ::JSON.parse(column_data) : column_data
|
40
|
+
object = if @array
|
41
|
+
unless data.respond_to?(:to_ary)
|
42
|
+
raise TypeError, "expected array-like column data; got: #{data.class}: #{data.inspect}"
|
43
|
+
end
|
44
|
+
data.map { |el| load_object(el) }
|
45
|
+
else
|
46
|
+
load_object(data)
|
47
|
+
end
|
48
|
+
object = @next_coder.load(object) if @next_coder
|
49
|
+
object
|
50
|
+
end
|
51
|
+
|
52
|
+
def dump(object)
|
53
|
+
object = @next_coder.dump(object) if @next_coder
|
54
|
+
return nil if object.nil?
|
55
|
+
jsonifiable = begin
|
56
|
+
if @array
|
57
|
+
unless object.respond_to?(:to_ary)
|
58
|
+
raise DumpError, "expected array-like attribute; got: #{object.class}: #{object.inspect}"
|
59
|
+
end
|
60
|
+
object.map do |el|
|
61
|
+
dump_object(el)
|
62
|
+
end
|
63
|
+
else
|
64
|
+
dump_object(object)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
@string ? ::JSON.generate(jsonifiable) : jsonifiable
|
68
|
+
end
|
69
|
+
end
|
70
|
+
# this is a ActiveRecord serialization class intended to store JSON in the
|
71
|
+
# database column and expose a given JSI::Base subclass once loaded
|
72
|
+
# on a model instance.
|
73
|
+
class SchemaInstanceJSONCoder < ObjectJSONCoder
|
74
|
+
private
|
75
|
+
def load_object(data)
|
76
|
+
@loaded_class.new(data)
|
77
|
+
end
|
78
|
+
|
79
|
+
def dump_object(object)
|
80
|
+
JSI::Typelike.as_json(object)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module JSI
|
2
|
+
# this is a ActiveRecord serialization class intended to store JSON in the
|
3
|
+
# database column and expose a Struct subclass once loaded on a model instance.
|
4
|
+
class StructJSONCoder < ObjectJSONCoder
|
5
|
+
private
|
6
|
+
def load_object(data)
|
7
|
+
if data.is_a?(Hash)
|
8
|
+
good_keys = @loaded_class.members.map(&:to_s)
|
9
|
+
bad_keys = data.keys - good_keys
|
10
|
+
unless bad_keys.empty?
|
11
|
+
raise LoadError, "expected keys #{good_keys}; got unrecognized keys: #{bad_keys}"
|
12
|
+
end
|
13
|
+
instance = @loaded_class.new(*@loaded_class.members.map { |m| data[m.to_s] })
|
14
|
+
instance.object_json_coder_keys_order = data.keys
|
15
|
+
instance
|
16
|
+
else
|
17
|
+
raise LoadError, "expected instance(s) of #{Hash}; got: #{data.class}: #{data.inspect}"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def dump_object(object)
|
22
|
+
if object.is_a?(@loaded_class)
|
23
|
+
keys = (object.object_json_coder_keys_order || []) | @loaded_class.members.map(&:to_s)
|
24
|
+
keys.map { |member| {member => object[member]} }.inject({}, &:update)
|
25
|
+
else
|
26
|
+
raise TypeError, "expected instance(s) of #{@loaded_class}; got: #{object.class}: #{object.inspect}"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,164 @@
|
|
1
|
+
module JSI
|
2
|
+
module Typelike
|
3
|
+
def self.modified_copy(other, &block)
|
4
|
+
if other.respond_to?(:modified_copy)
|
5
|
+
other.modified_copy(&block)
|
6
|
+
else
|
7
|
+
return yield(other)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
# I could require 'json/add/core' and use #as_json but I like this better.
|
12
|
+
def self.as_json(object, *opt)
|
13
|
+
if object.respond_to?(:to_hash)
|
14
|
+
object.map do |k, v|
|
15
|
+
unless k.is_a?(Symbol) || k.respond_to?(:to_str)
|
16
|
+
raise(TypeError, "json object (hash) cannot be keyed with: #{k.pretty_inspect.chomp}")
|
17
|
+
end
|
18
|
+
{k.to_s => as_json(v, *opt)}
|
19
|
+
end.inject({}, &:update)
|
20
|
+
elsif object.respond_to?(:to_ary)
|
21
|
+
object.map { |e| as_json(e, *opt) }
|
22
|
+
elsif [String, TrueClass, FalseClass, NilClass, Numeric].any? { |c| object.is_a?(c) }
|
23
|
+
object
|
24
|
+
elsif object.is_a?(Symbol)
|
25
|
+
object.to_s
|
26
|
+
elsif object.is_a?(Set)
|
27
|
+
as_json(object.to_a, *opt)
|
28
|
+
elsif object.respond_to?(:as_json)
|
29
|
+
as_json(object.as_json(*opt), *opt)
|
30
|
+
else
|
31
|
+
raise(TypeError, "cannot express object as json: #{object.pretty_inspect.chomp}")
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
module Hashlike
|
36
|
+
# safe methods which can be delegated to #to_hash (which the includer is assumed to have defined).
|
37
|
+
# 'safe' means, in this context, nondestructive - methods which do not modify the receiver.
|
38
|
+
|
39
|
+
# methods which do not need to access the value.
|
40
|
+
SAFE_KEY_ONLY_METHODS = %w(each_key empty? has_key? include? key? keys length member? size)
|
41
|
+
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)
|
42
|
+
DESTRUCTIVE_METHODS = %w(clear delete delete_if keep_if reject! replace select! shift)
|
43
|
+
# these return a modified copy
|
44
|
+
safe_modified_copy_methods = %w(compact merge)
|
45
|
+
# select and reject will return a modified copy but need the yielded block variable value from #[]
|
46
|
+
safe_kv_block_modified_copy_methods = %w(select reject)
|
47
|
+
SAFE_METHODS = SAFE_KEY_ONLY_METHODS | SAFE_KEY_VALUE_METHODS
|
48
|
+
safe_to_hash_methods = SAFE_METHODS - safe_modified_copy_methods - safe_kv_block_modified_copy_methods
|
49
|
+
safe_to_hash_methods.each do |method_name|
|
50
|
+
define_method(method_name) { |*a, &b| to_hash.public_send(method_name, *a, &b) }
|
51
|
+
end
|
52
|
+
safe_modified_copy_methods.each do |method_name|
|
53
|
+
define_method(method_name) do |*a, &b|
|
54
|
+
JSI::Typelike.modified_copy(self) do |object_to_modify|
|
55
|
+
object_to_modify.public_send(method_name, *a, &b)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
safe_kv_block_modified_copy_methods.each do |method_name|
|
60
|
+
define_method(method_name) do |*a, &b|
|
61
|
+
JSI::Typelike.modified_copy(self) do |object_to_modify|
|
62
|
+
object_to_modify.public_send(method_name, *a) do |k, _v|
|
63
|
+
b.call(k, self[k])
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def inspect
|
70
|
+
object_group_text = respond_to?(:object_group_text) ? ' ' + self.object_group_text : ''
|
71
|
+
"\#{<#{self.class.to_s}#{object_group_text}>#{empty? ? '' : ' '}#{self.map { |k, v| "#{k.inspect} => #{v.inspect}" }.join(', ')}}"
|
72
|
+
end
|
73
|
+
|
74
|
+
def to_s
|
75
|
+
inspect
|
76
|
+
end
|
77
|
+
|
78
|
+
def pretty_print(q)
|
79
|
+
q.instance_exec(self) do |obj|
|
80
|
+
object_group_text = obj.respond_to?(:object_group_text) ? ' ' + obj.object_group_text : ''
|
81
|
+
text "\#{<#{obj.class.to_s}#{object_group_text}>"
|
82
|
+
group_sub {
|
83
|
+
nest(2) {
|
84
|
+
breakable(obj.any? { true } ? ' ' : '')
|
85
|
+
seplist(obj, nil, :each_pair) { |k, v|
|
86
|
+
group {
|
87
|
+
pp k
|
88
|
+
text ' => '
|
89
|
+
pp v
|
90
|
+
}
|
91
|
+
}
|
92
|
+
}
|
93
|
+
}
|
94
|
+
breakable ''
|
95
|
+
text '}'
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
module Arraylike
|
100
|
+
# safe methods which can be delegated to #to_ary (which the includer is assumed to have defined).
|
101
|
+
# 'safe' means, in this context, nondestructive - methods which do not modify the receiver.
|
102
|
+
|
103
|
+
# methods which do not need to access the element.
|
104
|
+
SAFE_INDEX_ONLY_METHODS = %w(each_index empty? length size)
|
105
|
+
# there are some ambiguous ones that are omitted, like #sort, #map / #collect.
|
106
|
+
SAFE_INDEX_ELEMENT_METHODS = %w(| & * + - <=> abbrev assoc 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)
|
107
|
+
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)
|
108
|
+
|
109
|
+
# methods (well, method) that returns a modified copy and doesn't need any handling of block variable(s)
|
110
|
+
safe_modified_copy_methods = %w(compact)
|
111
|
+
|
112
|
+
# methods that return a modified copy and do need handling of block variables
|
113
|
+
safe_el_block_methods = %w(reject select)
|
114
|
+
|
115
|
+
SAFE_METHODS = SAFE_INDEX_ONLY_METHODS | SAFE_INDEX_ELEMENT_METHODS
|
116
|
+
safe_to_ary_methods = SAFE_METHODS - safe_modified_copy_methods - safe_el_block_methods
|
117
|
+
safe_to_ary_methods.each do |method_name|
|
118
|
+
define_method(method_name) { |*a, &b| to_ary.public_send(method_name, *a, &b) }
|
119
|
+
end
|
120
|
+
safe_modified_copy_methods.each do |method_name|
|
121
|
+
define_method(method_name) do |*a, &b|
|
122
|
+
JSI::Typelike.modified_copy(self) do |object_to_modify|
|
123
|
+
object_to_modify.public_send(method_name, *a, &b)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
safe_el_block_methods.each do |method_name|
|
128
|
+
define_method(method_name) do |*a, &b|
|
129
|
+
JSI::Typelike.modified_copy(self) do |object_to_modify|
|
130
|
+
i = 0
|
131
|
+
object_to_modify.public_send(method_name, *a) do |_e|
|
132
|
+
b.call(self[i]).tap { i += 1 }
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def inspect
|
139
|
+
object_group_text = respond_to?(:object_group_text) ? ' ' + self.object_group_text : ''
|
140
|
+
"\#[<#{self.class.to_s}#{object_group_text}>#{empty? ? '' : ' '}#{self.map { |e| e.inspect }.join(', ')}]"
|
141
|
+
end
|
142
|
+
|
143
|
+
def to_s
|
144
|
+
inspect
|
145
|
+
end
|
146
|
+
|
147
|
+
def pretty_print(q)
|
148
|
+
q.instance_exec(self) do |obj|
|
149
|
+
object_group_text = obj.respond_to?(:object_group_text) ? ' ' + obj.object_group_text : ''
|
150
|
+
text "\#[<#{obj.class.to_s}#{object_group_text}>"
|
151
|
+
group_sub {
|
152
|
+
nest(2) {
|
153
|
+
breakable(obj.any? { true } ? ' ' : '')
|
154
|
+
seplist(obj, nil, :each) { |e|
|
155
|
+
pp e
|
156
|
+
}
|
157
|
+
}
|
158
|
+
}
|
159
|
+
breakable ''
|
160
|
+
text ']'
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|