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.
@@ -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