nodepile 0.1.1 → 0.1.2
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/.rspec +2 -0
- data/.rubocop.yml +1 -1
- data/BACKLOG.md +34 -0
- data/Rakefile +92 -2
- data/lib/nodepile/base_structs.rb +62 -0
- data/lib/nodepile/colspecs.rb +562 -0
- data/lib/nodepile/gross_actions.rb +38 -0
- data/lib/nodepile/gviz.rb +108 -0
- data/lib/nodepile/keyed_array.rb +386 -0
- data/lib/nodepile/pile_organizer.rb +258 -0
- data/lib/nodepile/pragmas.rb +97 -0
- data/lib/nodepile/rec_source.rb +329 -0
- data/lib/nodepile/rule_eval.rb +155 -0
- data/lib/nodepile/version.rb +1 -1
- data/nodepile.gemspec +53 -0
- data/tmp/.gitignore +1 -0
- metadata +136 -19
@@ -0,0 +1,108 @@
|
|
1
|
+
require 'nodepile/base_structs.rb'
|
2
|
+
require 'ruby-graphviz'
|
3
|
+
|
4
|
+
|
5
|
+
|
6
|
+
|
7
|
+
module Nodepile
|
8
|
+
|
9
|
+
# This class converts a set of nodes and edges into a visualization rendered
|
10
|
+
# by the graphviz tool.
|
11
|
+
#
|
12
|
+
# Note: At present this is a wrapper around the ruby-graphviz gem as interface
|
13
|
+
# to the graphviz software tool. In order for this class to work
|
14
|
+
# the graphviz software package must be installed on the system. At some
|
15
|
+
# point I'm probably going to want to pull out that dependency because
|
16
|
+
# I'm a classic NIH bigot. But I always think about Luke and his lightsaber.
|
17
|
+
class GraphVisualizer
|
18
|
+
def initialize
|
19
|
+
@graph = nil
|
20
|
+
end
|
21
|
+
|
22
|
+
def inspect = "#<#{self.class}:0x#{object_id}"
|
23
|
+
|
24
|
+
NODE_ATTR_MAPS = {
|
25
|
+
'_shape' => :shape,
|
26
|
+
'_color' => :color,
|
27
|
+
'_fillcolor' => Proc.new{|obj,s| obj[:fillcolor] = s; obj[:style] ||= 'filled'},
|
28
|
+
'_fontcolor' => :fontcolor,
|
29
|
+
'_label' => :label,
|
30
|
+
}
|
31
|
+
|
32
|
+
EDGE_ATTR_MAPS = {
|
33
|
+
'_color' => :color,
|
34
|
+
'_fontcolor' => :fontcolor,
|
35
|
+
'_label' => :label,
|
36
|
+
}
|
37
|
+
|
38
|
+
# Given edge and node packets,
|
39
|
+
#
|
40
|
+
# @param node_packet_enum [Enumerable<EntityPacket>] all nodes
|
41
|
+
# @param edge_packet_enum [Enumerable<EntityPacket>] all edges
|
42
|
+
# @param configs [#[]] may be interrogated for config info
|
43
|
+
# such as the preferred rendering engine
|
44
|
+
# @return [void] no meaningful return value
|
45
|
+
def load(node_record_enum, edge_record_enum,configs: Hash.new)
|
46
|
+
@graph = GraphViz.new(:G)
|
47
|
+
@graph.type = configs[:directionality] if configs[:directionality]
|
48
|
+
nodes = Hash.new # temporary cache
|
49
|
+
node_record_enum.each{|nr|
|
50
|
+
n = nodes[nr['@key']] = @graph.add_nodes(nr['@key'])
|
51
|
+
self.class._apply_attrs(n,nr,NODE_ATTR_MAPS)
|
52
|
+
}
|
53
|
+
edge_record_enum.each{|er|
|
54
|
+
node_pair = er['@key'].map{|k| nodes[k] }
|
55
|
+
e = @graph.add_edges(*node_pair)
|
56
|
+
self.class._apply_attrs(e,er,EDGE_ATTR_MAPS)
|
57
|
+
}
|
58
|
+
end
|
59
|
+
|
60
|
+
# Generate a file based on the load specified
|
61
|
+
#
|
62
|
+
# @param fpath [String] filepath location at which to create the file.
|
63
|
+
# Filename to create/overwrite with the generated visualization.
|
64
|
+
# @param file_format [nil,:png,:gif,:svg,:dot] Note that the desired output will be
|
65
|
+
# deduced from the fpath extension if it exactly matches one
|
66
|
+
# of the valid arguments here (e.g. "xyz.png").
|
67
|
+
# @param configs [#[]] may be interrogated for config info
|
68
|
+
# such as the preferred rendering engine
|
69
|
+
#
|
70
|
+
# @return [String] returns the path of the file created
|
71
|
+
def emit_file(fpath,configs: nil ,file_format: nil)
|
72
|
+
configs ||= Hash.new
|
73
|
+
extension = /.*\.(png|svg|dot)$/.match(fpath)&.[](1)
|
74
|
+
fmt = file_format || extension&.to_sym
|
75
|
+
raise "Output file format is unspecified and can't be deduced from file extension" unless fmt
|
76
|
+
oargs = Hash.new
|
77
|
+
oargs[fmt] = fpath
|
78
|
+
oargs[:use] = configs[:layout_engine] if configs[:layout_engine]
|
79
|
+
@graph.output(oargs)
|
80
|
+
return fpath
|
81
|
+
end #emit file
|
82
|
+
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
# Convert field values into the rendering attributes and set them on the
|
87
|
+
# target objet.
|
88
|
+
# @param target [GraphViz::Node, GraphViz::Edge]
|
89
|
+
# @return [void]
|
90
|
+
def self._apply_attrs(target,fieldset, attr_map)
|
91
|
+
fieldset.each_filled_pair{|k,v|
|
92
|
+
next if !k.start_with?('_') || k == '~' # treat tilde like blank
|
93
|
+
gvkey = attr_map[k]
|
94
|
+
case gvkey
|
95
|
+
when nil
|
96
|
+
#no-op
|
97
|
+
when Proc
|
98
|
+
gvkey.call(target,v)
|
99
|
+
else
|
100
|
+
target[gvkey] = v if gvkey # naive assignment (hope the value is legit)
|
101
|
+
end
|
102
|
+
}
|
103
|
+
return nil
|
104
|
+
end
|
105
|
+
|
106
|
+
end #class GraphVisualizer
|
107
|
+
|
108
|
+
end #module Nodepile
|
@@ -0,0 +1,386 @@
|
|
1
|
+
module Nodepile
|
2
|
+
|
3
|
+
# Class makes an array of values behave like a hash. Intended to be used
|
4
|
+
# for rendering records from a tabular data source.
|
5
|
+
class KeyedArrayAccessor
|
6
|
+
include Enumerable
|
7
|
+
|
8
|
+
attr_accessor :extensible
|
9
|
+
|
10
|
+
# Note that this method will always freeze and retain a reference to the
|
11
|
+
# keys_array that is passed in. Note that this method may use a reference
|
12
|
+
# to the values_array passed in (allowing later mutation). See the copy
|
13
|
+
# parameter for override.
|
14
|
+
# @param keys_array [Array] The keys in order (think column header
|
15
|
+
# names). Note that these need not be unique (although
|
16
|
+
# this non-unique keys will have effect on many methods).
|
17
|
+
# @param values_array [Array,nil] Array of values. Should be the same size
|
18
|
+
# as the keys array and provides the value of each
|
19
|
+
# corresponding key. Note that the newly created object
|
20
|
+
# maintains a reference to the array passed in, so if
|
21
|
+
# you want to protect from side effects, you should use
|
22
|
+
# a copy of your values array. If passed nil, creates
|
23
|
+
# an array composed entirely of nil values.
|
24
|
+
# @param extensible [Boolean] indicates whether adding keys is permitted
|
25
|
+
# @param source [String,nil,Object] If provided on creation, will be returned by
|
26
|
+
# the #source method. Typically indicates the file
|
27
|
+
# or other source of the record.
|
28
|
+
# @param ref_num [Integer,nil] If provided on creation, will be returned by the
|
29
|
+
# #ref_num method. Typically indicates the relative position of the
|
30
|
+
# record within a given source.
|
31
|
+
# @param metadata [#each] Read-only data that will be retained with this
|
32
|
+
# record. It will be extracted from the object passed in
|
33
|
+
# using #each (which works great if the object is a hash or
|
34
|
+
# array of two element arrays). NOTE: A metadata key can
|
35
|
+
# hide the value of standard keys unless metadata_key_prefix
|
36
|
+
# is set to nil. Note that if a key prefix has been
|
37
|
+
# provided but does not appear in the keys resulting
|
38
|
+
# from the metadata object, then the prefix will be prepended
|
39
|
+
# to the key. Except where explicitly noted below, methods
|
40
|
+
# do not alter, access, or consider metadata values.
|
41
|
+
# @param metadata_key_prefix [String,nil] If nil, metadata values cannot be
|
42
|
+
# retrieved via the square bracket operators #[]. If non-nil
|
43
|
+
# Then keys passed to the square bracket operator will be
|
44
|
+
# first tested to see if they have the metadata_key_prefix.
|
45
|
+
# See the #[] operator for details about how the prefix is
|
46
|
+
# treated.
|
47
|
+
def initialize(keys_array, values_array, extensible: true,source: nil, ref_num: nil,
|
48
|
+
metadata: nil, metadata_key_prefix: ''
|
49
|
+
)
|
50
|
+
raise "keys must all be of type String or nil" unless keys_array.all?{|k| k.nil? || k.is_a?(String)}
|
51
|
+
@keys = keys_array.freeze
|
52
|
+
@vals = values_array || Array.new(@keys.length){nil}
|
53
|
+
@extensible = extensible
|
54
|
+
@source = source
|
55
|
+
@ref_num = ref_num
|
56
|
+
reset_metadata(metadata,metadata_key_prefix: metadata_key_prefix) if metadata
|
57
|
+
end
|
58
|
+
|
59
|
+
# Copy self, including metadata, source, and ref_num
|
60
|
+
def dup
|
61
|
+
self.class.new(@keys,@vals.dup,extensible: @extensible, metadata: @meta,
|
62
|
+
source: @source, ref_num: @ref_num,metadata_key_prefix: @meta_key_prefix
|
63
|
+
)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Dump any existing metadata and replace it with the provided metadata
|
67
|
+
# @param pair_enumerable [#each] Enumerable that should return key,value pairs
|
68
|
+
# @return [void]
|
69
|
+
def reset_metadata(pair_enumerable, metadata_key_prefix: :leave_prefix_unchanged)
|
70
|
+
@meta_key_prefix = metadata_key_prefix unless metadata_key_prefix == :leave_prefix_unchanged
|
71
|
+
pfx = @metadata_key_prefix || ''
|
72
|
+
if pair_enumerable.is_a?(Hash) && pair_enumerable.each_key.all?{|k| k.start_with?(pfx)}
|
73
|
+
@meta = pair_enumerable.dup
|
74
|
+
else
|
75
|
+
@meta = Hash.new
|
76
|
+
pair_enumerable&.each{|(k,v)|
|
77
|
+
key = (@meta_key_prefix + k) unless @meta_key_prefix.nil? || k.start_with?(@meta_key_prefix)
|
78
|
+
@meta[key] = v
|
79
|
+
}
|
80
|
+
end
|
81
|
+
nil
|
82
|
+
end
|
83
|
+
|
84
|
+
# @param key [String] If the value does not start with the metadata_prefix, it will be
|
85
|
+
# appended
|
86
|
+
# @param value [Object,nil]
|
87
|
+
def update_metadata(key,value)
|
88
|
+
@meta = Hash.new if @meta.nil?
|
89
|
+
k = key.start_with?(@meta_key_prefix) ? k : (@meta_key_prefix + key)
|
90
|
+
@meta&.[]=(k,value)
|
91
|
+
end
|
92
|
+
|
93
|
+
|
94
|
+
# retrieve metadata value
|
95
|
+
# @param key [String] Note that the key passed in should start with the metadata_key_prefix
|
96
|
+
# if one has been specified.
|
97
|
+
def metadata(key) = @meta[key]
|
98
|
+
def metadata_include?(key) = @meta.include?(key)
|
99
|
+
def metadata_key_prefix = @metadata_key_prefix
|
100
|
+
|
101
|
+
attr_accessor :source,:ref_num
|
102
|
+
|
103
|
+
# Return a copy of self where all values have been cleared.
|
104
|
+
# Metadata is
|
105
|
+
def cleared() = self.class.new(@keys,Array.new(@keys.length){nil})
|
106
|
+
|
107
|
+
# clear blanks will replace all fields where the value is pure whitespace
|
108
|
+
# with a nil instead. Note that nils get special treatment in operations
|
109
|
+
# like #overlay()
|
110
|
+
# @return [self]
|
111
|
+
def clear_blanks()
|
112
|
+
@vals.transform_values{|v| (v.is_a?(String) && /^\s*$/.match?(v)) ? nil : v }
|
113
|
+
return self
|
114
|
+
end
|
115
|
+
|
116
|
+
# @return [Boolean] Returns true if the value for each provided key is nil
|
117
|
+
def clear?(*key_names)
|
118
|
+
key_names.flatten!
|
119
|
+
return key_names.all?{|k| self[k].nil?}
|
120
|
+
end
|
121
|
+
|
122
|
+
# Note that if the same key name is duplicated multiple times, the leftmost
|
123
|
+
# value is used
|
124
|
+
def to_h
|
125
|
+
h = Hash.new
|
126
|
+
# reverse order so that in the case of duplicates the leftmost dominates
|
127
|
+
(-1..-@keys.length).step(-1).each{|i| h[@keys[i]] = @vals[i] }
|
128
|
+
return h
|
129
|
+
end
|
130
|
+
|
131
|
+
def keys = @keys
|
132
|
+
|
133
|
+
|
134
|
+
def each_key
|
135
|
+
return enum_for(:each_key) unless block_given?
|
136
|
+
@keys.each{|k| yield k }
|
137
|
+
end
|
138
|
+
|
139
|
+
# Similar to a Hash's #map! function
|
140
|
+
# @return [void]
|
141
|
+
def kv_map!(&kv_receiver)
|
142
|
+
raise "Block required" unless block_given?
|
143
|
+
@keys.each_with_index{|k,i| @vals[i] = yield(k,@vals[i])}
|
144
|
+
return nil
|
145
|
+
end
|
146
|
+
|
147
|
+
# @yield [key,value]
|
148
|
+
def each
|
149
|
+
return enum_for(:each) unless block_given?
|
150
|
+
@keys.each_with_index{|k,i| yield(k,@vals[i]) }
|
151
|
+
end
|
152
|
+
|
153
|
+
def each_value(&block)
|
154
|
+
return enum_for(:each_value) unless block_given?
|
155
|
+
@vals.each{|v| yield(v)}
|
156
|
+
end
|
157
|
+
|
158
|
+
# An empty key is a key whose value is nil.
|
159
|
+
# @param yield_index [Boolean] If true, yields the internal storage index number
|
160
|
+
# rather than the key name. Note that internal index number is
|
161
|
+
# the same for objects that #conforms?()
|
162
|
+
# @return [Void,Enumerator]
|
163
|
+
def each_empty_key(yield_index = false)
|
164
|
+
return enum_for(:each_key_blank, yield_index) unless block_given?
|
165
|
+
@keys.each_with_index{|k,i| yield(yield_index ? i : k) if @vals[i].nil?}
|
166
|
+
return nil
|
167
|
+
end
|
168
|
+
|
169
|
+
# A filled key is a key whose value is not nil. The block is yielded with
|
170
|
+
# the key and value (or index and value depending on yield_index parameter).
|
171
|
+
# @param yield_index_instead_of_val [Boolean] If true, yields the internal storage index number
|
172
|
+
# rather than the key name. Note that internal index number is
|
173
|
+
# the same for objects that #conforms?()
|
174
|
+
# @return [Void,Enumerator]
|
175
|
+
def each_filled_pair(yield_index_instead_of_val = false)
|
176
|
+
return enum_for(:each_key_nonblank, yield_index_instead_of_val) unless block_given?
|
177
|
+
@keys.each_with_index{|k,i| yield((yield_index_instead_of_val ? i : k),@vals[i]) unless @vals[i].nil?}
|
178
|
+
return nil
|
179
|
+
end
|
180
|
+
|
181
|
+
|
182
|
+
def values = return @vals.dup
|
183
|
+
|
184
|
+
# alias for #values
|
185
|
+
def to_a = values()
|
186
|
+
|
187
|
+
# Note that duplications of the same key are counted toward this number
|
188
|
+
def size = keys.length
|
189
|
+
def length = self.size
|
190
|
+
|
191
|
+
# Equality comparison is very tolerant and may not be what you expect.
|
192
|
+
# * Hashes are deemed equal if they have the same unique keys
|
193
|
+
# and the key-value pairs retrieved via #[] are equal.
|
194
|
+
# * Arrays are deemed equal if the to_a() representation of self matches the
|
195
|
+
# other array.
|
196
|
+
# * Another KeyedArrayAccessor is deemed equal using one of two rules.
|
197
|
+
# For #conforms? true objects, the exact value of keys and values is compared.
|
198
|
+
# For #conforms? false objects, the set of distinct keys is the same in both arrays
|
199
|
+
# and the value associated with each key using #[] is the same.
|
200
|
+
# Another KeyedAccessArray is deemed equal if the key-value
|
201
|
+
# pairs have the same number of keys and are equal (which means that
|
202
|
+
# only the first of duplicate columns is compared)
|
203
|
+
#
|
204
|
+
# Note: Metadata is not considered for purposes of this comparison.
|
205
|
+
def ==(otr)
|
206
|
+
return true if self.equal?(otr)
|
207
|
+
case otr
|
208
|
+
in Hash
|
209
|
+
return ((@keys + otr.keys)-(@keys&otr.keys)).empty? &&
|
210
|
+
otr.all?{|k,v| self[k] == v}
|
211
|
+
in Array
|
212
|
+
return @vals == otr
|
213
|
+
in KeyedArrayAccessor
|
214
|
+
if self.conforms?(otr)
|
215
|
+
return @vals == otr._internal_vals
|
216
|
+
else
|
217
|
+
return ((@keys + otr._internal_keys) - (@keys & otr._internal_keys)).empty? &&
|
218
|
+
@keys.all?{|k| self[k] == otr[k]}
|
219
|
+
end
|
220
|
+
else
|
221
|
+
return false # currently no other types are supported
|
222
|
+
end #pattern match
|
223
|
+
|
224
|
+
|
225
|
+
end
|
226
|
+
|
227
|
+
def value_at(index) = @vals[index]
|
228
|
+
def include?(key) = return @keys.include?(key)
|
229
|
+
|
230
|
+
# Other object must support
|
231
|
+
# Note: metadata is left unchanged by this method.
|
232
|
+
def merge!(otr_hashlike)
|
233
|
+
raise "Block handling not yet supported by this method" if block_given?
|
234
|
+
otr_hashlike.each_key{|k| self[k] = otr_hashlike[k]}
|
235
|
+
return self
|
236
|
+
end
|
237
|
+
def merge(otr_hashlike) = self.dup.merge!(otr_hashlike)
|
238
|
+
|
239
|
+
# Provides hash-style access to a value by it's key (rather than its position).
|
240
|
+
# Note that if duplicate keys exist, the leftmost key is returned.
|
241
|
+
# Returns the value of the key or quietly returns nil if the key isn't found
|
242
|
+
#
|
243
|
+
# Note, if the object has metadata, and the metadata_key_prefix is not nil,
|
244
|
+
# this method will attempt to retrieve metadata matching the key before
|
245
|
+
# retrieving the normal key data. If the metadata does not contain the
|
246
|
+
# requested key, this will check for a match of the normal data.
|
247
|
+
def [](key)
|
248
|
+
return @meta[key] if @meta_key_prefix && key.start_with?(@meta_key_prefix) && @meta&.include?(key)
|
249
|
+
@keys.index(key)&.tap{|ix| return @vals[ix]}
|
250
|
+
end
|
251
|
+
|
252
|
+
|
253
|
+
# Uses a hash-style access to update values. Note that becuase this data
|
254
|
+
# structure does not enforce uniqueness of keys, this method will only update
|
255
|
+
# the leftmost value corresponding to the given key.
|
256
|
+
#
|
257
|
+
# Important note. Adding a new key to the object will make it non-conforming
|
258
|
+
# with other objects.
|
259
|
+
def []=(key,new_val)
|
260
|
+
ix = @keys.index(key)
|
261
|
+
if ix
|
262
|
+
(@vals[ix] = new_val) if ix # simple case... update existing value
|
263
|
+
else # ix.nil?
|
264
|
+
raise <<~ERRMSG if !extensible
|
265
|
+
Because the #extensible() attribute is set to false, a new key may not be added [#{key}]
|
266
|
+
ERRMSG
|
267
|
+
# new keys are appended to the right side
|
268
|
+
@keys = (@keys.dup << key)
|
269
|
+
@vals << new_val
|
270
|
+
end
|
271
|
+
return new_val
|
272
|
+
end
|
273
|
+
|
274
|
+
|
275
|
+
# indicated that the object has exactly the same keys in exactly the same order
|
276
|
+
def conforms?(otr)
|
277
|
+
return otr.is_a?(self.class) && @keys == otr._internal_keys
|
278
|
+
end
|
279
|
+
|
280
|
+
# Given a KeyedArrayAccessor objects, update self to form a "merged"
|
281
|
+
# KeyedArrayAccessor where self "underlays" an "upper" array to generate
|
282
|
+
# a merged data structure.
|
283
|
+
# An overlay/underlay follows these rules:
|
284
|
+
# If "upper" and self have non-blank for the same element, then the upper element would
|
285
|
+
# "overlay" the corresponding entry in self. If the upper is blank, then
|
286
|
+
# it does not "overlay".
|
287
|
+
# Note that the object in array position zero is at the bottom of the overlay.
|
288
|
+
# Random Observation: If the upper_kaa is completely populated, the
|
289
|
+
# lower_kaa is essentially ignored.
|
290
|
+
#
|
291
|
+
# NOTE: When the upper_kaa does not #conforms?(), the result
|
292
|
+
# will have a key set containing the union of the upper and lower keysets.
|
293
|
+
# Also, overlaying non-conforming objects will have worse performance.
|
294
|
+
# Note that if it is possible, for the overlay to be generated without
|
295
|
+
# adding keys, this strategy will be used.
|
296
|
+
#
|
297
|
+
#
|
298
|
+
# @param upper_kaa [KeyedArrayAccessor] Non-blank entries here will "overlay"
|
299
|
+
# entries of the lower kaa. This method is a no-op
|
300
|
+
# if it is passed itself
|
301
|
+
# @return [self]
|
302
|
+
#
|
303
|
+
# Note: Metadata for self is unchanged by this method.
|
304
|
+
def underlay!(upper_kaa)
|
305
|
+
return self if self.equal?(upper_kaa) # return self (no-op)
|
306
|
+
if conforms?(upper_kaa)
|
307
|
+
upper_kaa._each_value_with_index(true){|upper_val,ix| @vals[ix] = upper_val}
|
308
|
+
else
|
309
|
+
upper_kaa.each_filled_pair(false){|key,upper_val| self[key] = upper_val}
|
310
|
+
end
|
311
|
+
return self
|
312
|
+
end
|
313
|
+
|
314
|
+
# See #overlay!() except this creates a copy rather than altering self.
|
315
|
+
def overlay(lower_kaa) = lower_kaa.underlay(self)
|
316
|
+
def underlay(upper_kaa) = self.dup.underlay!(upper_kaa)
|
317
|
+
|
318
|
+
# See #underlay()
|
319
|
+
# An important difference between overlay and underlay is the ordering
|
320
|
+
# of columns in the result. Column order is in the order of the lower
|
321
|
+
# object plus any (non-nil) additions appearing to the right. This operation
|
322
|
+
# is not particularly efficient except when working with conforming arrays.
|
323
|
+
#
|
324
|
+
# @return [self]
|
325
|
+
#
|
326
|
+
# Note: metadata is unchanged by this method.
|
327
|
+
def overlay!(lower_kaa)
|
328
|
+
return self if self.equal?(lower_kaa) #no-op
|
329
|
+
if conforms?(lower_kaa)
|
330
|
+
lower_kaa._each_value_with_index(true){|lower_val,ix| @vals[ix] ||= lower_val}
|
331
|
+
else
|
332
|
+
new_arr = lower_kaa.dup.underlay!(self)
|
333
|
+
@keys = new_arr._internal_keys
|
334
|
+
@vals = new_arr._internal_vals
|
335
|
+
@key_count = nil
|
336
|
+
end
|
337
|
+
return self
|
338
|
+
end
|
339
|
+
|
340
|
+
# Repeatedly overlays successive KeyedArrayAccessor with the first
|
341
|
+
# one being at the bottom and the last one being at the top.
|
342
|
+
# @return [KeyedArrayAccessor,nil] returns nil if the inbound enumerable was empty
|
343
|
+
def self.bulk_overlay(kaa_enumerable)
|
344
|
+
kaa_enumerable.inject(nil){|accum,kaa| (accum||kaa.dup).underlay!(kaa) }
|
345
|
+
end
|
346
|
+
|
347
|
+
# Internal implementation to optimize pattern matching
|
348
|
+
# Does two main things:
|
349
|
+
# 1) Converts keys passed in to string type (allowing symbols to be used for pattern matching)
|
350
|
+
# 2) Makes it looks like metadata and data occupy the same keyspace regardless of
|
351
|
+
# the setting of the @metadata_key_prefix
|
352
|
+
class PseudoROHashForDeconstruct
|
353
|
+
def initialize(keyed_array) = @kaa = keyed_array
|
354
|
+
def include?(k) = @kaa.include?(k.to_s) || @kaa.metadata_include?(k.to_s)
|
355
|
+
def [](k) = (@kaa.metadata_key_prefix.nil? ? (@kaa[k.to_s] || @kaa.metadata(k.to_s)) : @kaa[k.to_s] )
|
356
|
+
end
|
357
|
+
|
358
|
+
# Note that deconstruct_keys will expose metadata values regardless of the choice
|
359
|
+
# of @metadata_key_prefix. Although, an actual key with the same name as a
|
360
|
+
# metadata value will hide the metadata value.
|
361
|
+
def deconstruct_keys(keys)
|
362
|
+
# Developer note: I think the below line should work as desired because this object is so
|
363
|
+
# much like a hash, but possibly it'll be necessary to support one or more methods.
|
364
|
+
return PseudoROHashForDeconstruct.new(self)
|
365
|
+
end
|
366
|
+
|
367
|
+
protected
|
368
|
+
def _same_keys?(keys2) = return @keys.equal?(keys2) || @keys == keys2
|
369
|
+
def _set_val_at(index,newval) = (@vals[index] = newval)
|
370
|
+
def _internal_keys = @keys
|
371
|
+
def _internal_vals = @vals
|
372
|
+
|
373
|
+
# Beware, the indices returned are only meaningful for conforming arrays
|
374
|
+
# Only iterates through visible values
|
375
|
+
def _each_value_with_index(suppress_nils = false)
|
376
|
+
return enum_for(:each_value_with_index) unless block_given?
|
377
|
+
@keys.each_with_index{|k,i|
|
378
|
+
yield(@vals[i],i) unless suppress_nils && @vals[i].nil?
|
379
|
+
}
|
380
|
+
end
|
381
|
+
|
382
|
+
end #class KeyedArrayAccessor
|
383
|
+
|
384
|
+
|
385
|
+
|
386
|
+
end #module Nodepile
|