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,562 @@
1
+ #
2
+ # colspecs.rb
3
+ #
4
+ # Specification of the column names that have predefined meaning
5
+ # when encountered in a input file.
6
+ #
7
+
8
+ require 'set'
9
+ require 'yaml'
10
+ require 'nodepile/keyed_array.rb'
11
+ require 'nodepile/base_structs.rb'
12
+
13
+ module Nodepile
14
+
15
+ # This class provides information about the valid columns for potential use
16
+ # in documentation and also provides facilities for doing per-line verification
17
+ # of column values within a single line.
18
+ # that can appear on a non-header line of an input file.
19
+ # Note that the best way to think of this class is as a scanner which is in some sense
20
+ # stateless
21
+ #
22
+ # Records generated by the #parse method and related methods will by default
23
+ # set metadata fields, particularly including:
24
+ # '@type' = :node, :edge, :rule, :pragma
25
+ # '@key' = String or [String,String] for node or edge respectively
26
+ class InputColumnSpecs
27
+
28
+ class InvalidRecordError < StandardError
29
+ attr_accessor :rec_num,:file_path # use to add error detail
30
+ def initialize(msg) = @msg = msg
31
+ def message
32
+ prefix = "Nodepile parsing error at record [#{self.rec_num||'?'}] from source [#{self.file_path||'?'}]: "
33
+ return (!self.rec_num.nil? || !self.file_path.nil?) ? (prefix + @msg) : @msg
34
+ end
35
+ end # parsing errors throw this
36
+
37
+ DEFAULT_ID_DELIMITER = ',' # may be used in _link_from and _link_to for multiple edges
38
+ DEFAULT_PRAGMA_MARKER = "#pragma "
39
+
40
+
41
+ public
42
+ # Provide a simple hash of field names and their meaning/use.
43
+ def self.coldefs
44
+ @@class_mcache.cache(__method__){||
45
+ h = YAML.load(defined?(DATA) ? DATA.read :
46
+ /__END__\s+(.*)/m.match(File.read(__FILE__))[1]
47
+ )['data']['fields']
48
+ h # this value is cached
49
+ }
50
+ end
51
+
52
+ def self.val_is_pattern?(s)
53
+ s[0] == '/' ? :pattern : nil
54
+ end
55
+
56
+ # List the most crucial columns that indicate the existence of
57
+ # nodes, edges, and styling instructions.
58
+ def self.id_cols; %w(_id _links_from _links_to); end #do not reorder
59
+ def self.all_cols; coldefs().keys; end
60
+
61
+ # Defines the characters that will be interpreted as delimiting entity "id"
62
+ # values.
63
+ attr_accessor :id_delimiter
64
+
65
+ # Creates a customized InputColumnSpecs object based on the column names and order
66
+ # that are included in one specific file. That object can then be used
67
+ # ONLY to validate that specific file. See the #coldefs
68
+ # @param col_names[Array<String>] Order of column data expected from calls to
69
+ # #validate
70
+ # @param id_delimiter[String] Indicates a character that will be considered
71
+ # a delimiter between ids so that multiple may occupy the
72
+ # field
73
+ # @param pragmas[String,nil] If nil, "pragmas" are not identified.
74
+ # If true, then when the _id field is started with the
75
+ # "#pragma", it is identified as a pragma and made available
76
+ # through the #each_pragma method. If a string, then
77
+ # any record whose _id column starts with that string is
78
+ # considered a pragma. Note that ONLY the _id column of
79
+ # a pragma record is captured.
80
+ # @param metadata_key_prefix [String,nil] During #parse and related methods
81
+ # records are yielded in the form of KeyedArrayAccessor objects
82
+ # that have both the loaded data and also metadata about the
83
+ # records such as the type of the entity and whether its
84
+ # existence was triggered explicitly or implicitly. This
85
+ # value is is passed to the KeyedArrayAccessor.
86
+ # @raise InvalidRecordError
87
+ def initialize(col_names,id_delimiter: DEFAULT_ID_DELIMITER,pragmas: DEFAULT_PRAGMA_MARKER,
88
+ metadata_key_prefix: '@')
89
+ @col_names = col_names.dup.freeze
90
+ @id_cols_indices = self.class.id_cols.map{|cnm| @col_names.find_index(cnm)}.freeze
91
+ @id_delimiter = id_delimiter
92
+ @pragma_marker = pragmas
93
+ @empty_kv_array = KeyedArrayAccessor.new(@col_names,Array.new(@col_names.length).freeze)
94
+ raise InvalidRecordError.new(<<~ERRMSG) if @id_cols_indices[0].nil?
95
+ A valid record set must contain an '_id' column
96
+ ERRMSG
97
+ @metadata_key_prefix = metadata_key_prefix
98
+ @md_pfxs = [(@metadata_key_prefix||'')+'type',
99
+ (@metadata_key_prefix||'')+'key',
100
+ (@metadata_key_prefix||'')+'is_implied',
101
+ ]
102
+
103
+ @mc = CrudeCalculationCache.new
104
+ end
105
+
106
+
107
+
108
+ # Given a string representing the contents of the "_id", "_links_to", or "_links_from" field,
109
+ # this method will split it into zero or more tokens representing either ids or
110
+ # or else patterns. Patterns start with the question mark character.
111
+ # Leading and trailing spaces are stripped before return.
112
+ # @param id_containing_field [String] Any of the possible id containing fields
113
+ # @return [Array<String>] zero or more
114
+ def split_ids(id_containing_field, &block)
115
+ # very simple implementation (make smarter later???)
116
+ return [] if id_containing_field.nil?
117
+ return enum_for(:split_ids,id_containing_field) unless block_given?
118
+ raise "A field containing a rule calculation may not contain other ids" if /,\s*\?/ =~ id_containing_field
119
+ id_containing_field.split(@id_delimiter).tap{|a2|
120
+ a2.each{|s|
121
+ s.strip!
122
+ yield s unless s == ''
123
+ }
124
+ }
125
+ end
126
+
127
+ # Given a single "record" (which may define zero or more entities or contain errors)
128
+ # this method will yield once for each "entity" or "rule" that may be inferred
129
+ # by that record. The "entities" defined by a given record are determined by
130
+ # three fields: _id, _links_from, and _links_to.
131
+ #
132
+ # The entries in these fields can indicate several things:
133
+ # 1) The explicit existence and attribute values for a node
134
+ # 2) Override values for a node or pattern of nodes
135
+ # 3) The implicit existence of a node (because an explicit node links explicitly to/from it)
136
+ # 4) The explicit existence of an edge (because an edge is explicitly in the to/from fields)
137
+ # and attribute values for the edge
138
+ # 5) The implicit existence of an edge (because an edge is implied by a rule in the to/from)
139
+ # and attribute values for the edge
140
+ #
141
+ # Note that when metadata is attached to the KeyedArrayAccessors, it the metada will
142
+ # be updated to include the following key-values.
143
+ # * 'type' = :node, :edge, :rule, :pragma
144
+ # * 'key' = either a single String of nodes/node-rules or an array of two strings for edges
145
+ # and edge-rules
146
+ # * 'is_implied' = true,nil to indicate whether the entity is implied
147
+ #
148
+ # @param col_value_array [Array] Column values in exact order of column names
149
+ # provided when this object was constructed.
150
+ # @return [Integer] Number of entities encountered. Note that zero is valid.
151
+ # @param metadata [Hash,nil] If provided, the given metadata will be attached to each of the
152
+ # KeyedArrayAccessors that are yielded along with metadata about
153
+ # this particular entity. Note that the hash passed in will be altered
154
+ # in two ways. Firstly, if a @metadata_key_prefix is specified, all keys
155
+ # will be changed to include this prefix (if they aren't already).
156
+ # Secondly, the three additional metadata key-values will be added
157
+ # (type, key, is_implied).
158
+ # @param metadata_key_prefix [String,nil] See KeyedArrayAccessor#initialize for detail.
159
+ # If provided, this string will be foreced to appear at the beginning
160
+ # of every metadata key.
161
+ # @param source [String,nil,Object] see KeyedArrayAccessor#initialize for detail
162
+ # @param ref_num [Integer,nil] see KeyedArrayAccessor#iniialize for detail
163
+ # @raise [InvalidRecordError] If errors or omissions in data make it uninterpretable
164
+ # @yieldparam [Nodepile::KeyedArrayAccessor] A single node, edge, or rule taken extracted
165
+ # from the record. Note that the id, links_to, and links_from
166
+ # fields may be altered in the return value.
167
+ def parse(col_value_array,source: nil, ref_num: nil,metadata: nil,&entity_receiver)
168
+ #see below in this file for the various preprocessing defined
169
+ _preprocs.each{|(ix,preproc_block)|
170
+ col_value_array[ix] = preproc_block.call(col_value_array[ix])
171
+ }
172
+ _validators.each{|(vl_col_nums,val_block)|
173
+ errmsg = val_block.call(*vl_col_nums.map{|i| i && col_value_array[i]}) # test the specified column values
174
+ raise InvalidRecordError.new(errmsg) if errmsg
175
+ }
176
+ if metadata && (@metadata_key_prefix||'') != ''
177
+ # if necessary, facilitate quick attachment of metadata to KeyedArrayAccessor
178
+ metadata.transform_keys{|k| k.start_with?(@metadata_key_prefix) ? k : @metadata_key_prefix + k}
179
+ end
180
+ metadata ||= Hash.new
181
+ # following proc is used to package up the return value at multiplel places below
182
+ yieldval_bldr = Proc.new{|kaa,*three_md_fields|
183
+ (0..(@md_pfxs.length-1)).each{|i| metadata[@md_pfxs[i]] = three_md_fields[i]}
184
+ kaa.reset_metadata(metadata,metadata_key_prefix: @metadata_key_prefix)
185
+ kaa
186
+ }
187
+ ids, links_from, links_to = @id_cols_indices.map{|i| i && col_value_array[i]}
188
+ return 0 if ids&.start_with?('#') # ignore these records
189
+ base_kva = KeyedArrayAccessor.new(@col_names, col_value_array, source: source, ref_num: ref_num)
190
+ if @pragma_marker && ids&.start_with?(@pragma_marker)
191
+ # pragmas get shortcut treatment, not keyed, ignore all other columns
192
+ yield yieldval_bldr(base_kva,:pragma,nil,false) if block_given?
193
+ return 1 # pragmas do not have links, or multiple ids
194
+ end
195
+ entity_count = 0
196
+ lf_list = split_ids(links_from).to_a
197
+ lt_list = split_ids(links_to).to_a
198
+ if !ids.nil?
199
+ edge_list = Array.new
200
+ else
201
+ # for pure edges, add them to list for later yielding
202
+ edge_list = lf_list.to_a.product(lt_list.to_a)
203
+ .map{|(lf,lt)|
204
+ kva = base_kva.dup
205
+ kva['_links_from'] = lf
206
+ kva['_links_to'] = lt
207
+ [lf,lt,kva ]
208
+ }
209
+ end #detecting pure edges
210
+
211
+ split_ids(ids).each{|id|
212
+ kva = base_kva.dup.tap{|kva|
213
+ kva['_id'] = id
214
+ kva['_links_from'] = nil
215
+ kva['_links_to'] = nil
216
+ }
217
+ entity_count += 1
218
+ yield yieldval_bldr.call(kva,id[0] == '?' ? :rule : :node,id.freeze,false) if block_given?
219
+ # emit any implicitly existing nodes
220
+ (lf_list + lt_list).each{|link|
221
+ if !link.start_with?('?')
222
+ entity_count += 1
223
+ # implied nodes have cleared value except their key
224
+ kva = base_kva.dup.tap{|x|
225
+ x['_id'] = link
226
+ x['_links_from'] = nil
227
+ x['_links_to'] = nil
228
+ }
229
+ yield yieldval_bldr.call(kva,:node,link.freeze,true) if block_given?
230
+ end
231
+ }
232
+ # Flag edges the go from/to _id. Note, you can't define rules this way.
233
+ (lf_list.product([id]) + [id].product(lt_list)).each{|a|
234
+ next if a.any?{|v| v.start_with?('?')} # rules can't imply an edge
235
+ kva = @empty_kv_array.dup
236
+ kva['_links_from'] = a[0]
237
+ kva['_links_to'] = a[1]
238
+ kva.source = base_kva.source
239
+ kva.ref_num = base_kva.ref_num
240
+ edge_list << [a[0],a[1],kva]
241
+ }
242
+ }
243
+ edge_list.each{|(n1,n2,kva)|
244
+ entity_count += 1
245
+ et = (n1.start_with?('?') || n2.start_with?('?')) ? :rule : :edge
246
+ yield yieldval_bldr.call(kva,et,[n1,n2].freeze,false) if block_given?
247
+ }
248
+ return entity_count
249
+ end
250
+
251
+
252
+ # Bulk parse is a convenience method for parsing a source of records. It is essentially
253
+ # the same as instantiating an object using the first record and then calling parse multiple times
254
+ #
255
+ # For information on most of the parameters, see the #parse method
256
+ #
257
+ # @param rec_source [Enumerable<Array<String>>] first record is presumed to be
258
+ # the header and all other lines will be forced into the #parse
259
+ # method.
260
+ # @return [Integer, Enumerator] If a block is passed in, returns the total of all
261
+ # entities that were yielded from the source. Otherwise
262
+ # returns an enumerator.
263
+ #
264
+ def self.bulk_parse(rec_source,source: nil,metadata: nil, metadata_key_prefix: nil, &entity_receiver)
265
+ return enum_for(:bulk_parse,rec_source, source:, metadata:, metadata_key_prefix:) unless block_given?
266
+ hdr_vals = rec_source.next
267
+ specs = InputColumnSpecs.new(hdr_vals)
268
+ rec_count = 0
269
+ begin
270
+ loop do
271
+ next_rec = rec_source.next
272
+ rec_count += specs.parse(next_rec,source:, ref_num: rec_count+2,metadata:,&entity_receiver)
273
+ end
274
+ rescue StopIteration
275
+ #no-op
276
+ end
277
+ return rec_count
278
+ end
279
+
280
+ # Utility class returned by the #make_pattern_match_verifier() method
281
+ #
282
+ # It holds tests that can be used to confirm whether a pattern matches
283
+ # aspects of a given node.
284
+ #
285
+ # Example Pattern Strings:
286
+ # 1) "?/^alpha/" matches type == :node where key starts with "alpha"
287
+ # 2) "beta" mates type == :node where key is exactly "beta"
288
+ #
289
+ class PatternMatchVerifier
290
+ ALWAYS_TRUE_PROC = Proc.new{true}
291
+ def initialize(pattern_string)
292
+ @non_id_test = ALWAYS_TRUE_PROC
293
+ @id_test = nil
294
+ @pattern_string = pattern_string
295
+ case pattern_string
296
+ when /^\s*\?\s*\/(.*)\/\s*$/
297
+ rx = Regexp.new($1)
298
+ @id_test = Proc.new{|id| rx.match?(id)}
299
+ else
300
+ exact_id = pattern_string.strip # match with the exact (trimmed) string
301
+ @id_test = Proc.new{|id| id == exact_id }
302
+ end
303
+ end #initialize
304
+
305
+
306
+ def inspect = "#<#{self.class} 0x#{object_id} pattern_string=#{@pattern_string.inspect}> "
307
+
308
+
309
+ # Exclusively test whether the given node id would be acceptable for this
310
+ # verifier.
311
+ #
312
+ # @param test_id_string [String]
313
+ def id_match?(test_id_string) = @id_test.call(test_id_string)
314
+
315
+ # Exclusively test whether any of the non-id aspects of the node would be
316
+ # acceptable for this verifier.
317
+ # @param node_entity_packet [Nodepile::EntityPacket]
318
+ def non_id_match?(node_entity_packet) = @non_id_test.call(node_entity_packet)
319
+
320
+ # Perform both the id_match?() and return their logical AND
321
+ def match?(nep) = id_match?(nep.key) && non_id_match?(nep)
322
+ end #class PatternMatchVerifier
323
+
324
+ # "Rule" type entities are characterized by having one or more "patterns"
325
+ # that are used to determine which of the nodes a given rule should apply to.
326
+ # Most often, the patterns specify sets of node IDs would satisfy them
327
+ # such as through regular expression matching. However, future instances
328
+ # may use field values to determine matching.
329
+ #
330
+ # For explanation of pattern logic see the PatternMatchVerifier class
331
+ #
332
+
333
+ def self.make_pattern_match_verifier(pattern_string)
334
+ return PatternMatchVerifier.new(pattern_string)
335
+ end
336
+
337
+
338
+
339
+
340
+ private
341
+
342
+
343
+ def _preprocs
344
+ @mc.cache(__method__){||
345
+ # collect preproc relevant for the columns present
346
+ my_preprocs = Array.new
347
+ @col_names.each_with_index{|nm,ix|
348
+ self.class._all_preprocs[nm]&.tap{|(skip_nil,block)|
349
+ my_preprocs << [ix,block].freeze
350
+ }
351
+ }
352
+ my_preprocs.freeze # will get cached
353
+ } # end cache calculator
354
+
355
+ end # _preprocs()
356
+
357
+ def _validators
358
+ @mc.cache(__method__){||
359
+ my_validators = Array.new
360
+ # collect validators relevant for the columns present
361
+ self.class._all_validators.each{|(always,vl_col_names,block)|
362
+ vl_col_nums = vl_col_names.map{|nm| @col_names.find_index(nm) }
363
+ if always || vl_col_nums.none?{|v| v.nil?}
364
+ my_validators << [vl_col_nums.freeze,block].freeze
365
+ end
366
+ }
367
+ my_validators.freeze # this should get cached
368
+ } # end cache calculator
369
+ end
370
+
371
+
372
+
373
+ # A validator is a block used to verify the values in a specific
374
+ # set of fields. The blocks registered here are compiled into calls
375
+ # to InputColumnSpecs#new. A validator block should evaluate to nil
376
+ # if everything is okay. If it evaluates to a string, that string may be
377
+ # communicated to users as a validation failure.
378
+ #
379
+ # @param always [true,false,nil] Indicates that the validator should
380
+ # be run regardless of whether all fields
381
+ # are present. Nils will be passed
382
+ # to the validator for missing fields.
383
+ # @param col_name_array [Array<String>] These fields must be passed
384
+ # in this order to the block to
385
+ # perform the validation.
386
+ def self._make_validator(col_name_array,always: nil, &validator_block)
387
+ [always,col_name_array.dup.freeze,validator_block].freeze
388
+ end
389
+
390
+ # Package up field preprocessing into a record for later use
391
+ # @param skip_nil [Boolean] if true, does no preprocessing if the field value
392
+ # is nil
393
+ def self._make_field_preproc(col_name, skip_nil: true, &preproc_block)
394
+ return [col_name,skip_nil ? Proc.new{|s| s && preproc_block.(s)} : preproc_block]
395
+ end
396
+
397
+ # Package up preprocs for a field using some standard rules. Multiple
398
+ # rules may apply to the same field.
399
+ #
400
+ # * :strip will cause leading and trailing spaces to be removed and
401
+ # blank fields will be set to nil
402
+ # * :downcase will cause field contents to be downcased
403
+ # @param col_name [String] name of the column the preproc applies to
404
+ # @param std_syms [Array<Symbol>,nil] one or more symbols representing
405
+ # the preprocs that should be combined. They area applied
406
+ # in the specified order although is :strip is present it
407
+ # must appear first. Method is a no-op of the std_syms is
408
+ # nil
409
+ def self._make_standard_preproc(col_name,std_syms)
410
+ return nil if std_syms.nil?
411
+ nproc = Proc.new{|s|
412
+ std_syms.each{|instr|
413
+ case instr
414
+ when :downcase then s&.downcase!
415
+ when :strip then s = nil if (s.strip!||s) == ''
416
+ else
417
+ raise "Unrecognized preproc found [#{proc_sym.inspect}]"
418
+ end
419
+ } #each instruction
420
+ next s # "return value"
421
+ } #nproc
422
+ _make_field_preproc(col_name,skip_nil: true,&nproc)
423
+ end
424
+
425
+ @@class_mcache = CrudeCalculationCache.new
426
+
427
+ def self._all_preprocs
428
+ @@class_mcache.cache(__method__){||
429
+ h = Hash.new # append to this array
430
+ # generate preprocs using the flags in the YAML at bottom of this file
431
+ coldefs.each_pair{|fieldname,fielddata|
432
+ h[fieldname] = _make_standard_preproc(fieldname,fielddata['preproc']&.map(&:to_sym))
433
+ }
434
+ h.freeze # this Hash will get cached
435
+ } # end cache calculator
436
+ end # _all_preprocs()
437
+
438
+ def self._all_validators
439
+ @@class_mcache.cache(__method__){||
440
+ a = Array.new
441
+ a << _make_validator(['_id','_links_from','_links_to'],always: true){|id,lf,lt|
442
+ if id.nil? && (lf.nil? ^ lt.nil?)
443
+ next 'If the _id field is blank, both _links_from and links_to fields must be blank or both populated'
444
+ end
445
+ if id&.start_with?('?') && (lf || lt)
446
+ next 'If the _id field indicates a :rule, _links_from and _links_to must be blank'
447
+ end
448
+ if id && (lf&.start_with?('?') || lt&.start_with?('?'))
449
+ next "If the _id field is populated, you may not put a rule formula in _links_from or _links_to"
450
+ end
451
+ next nil
452
+ }
453
+ a.freeze # this Array will get cached
454
+ } # end cache calculator
455
+
456
+ end # _all_validators()
457
+
458
+
459
+
460
+
461
+ end # class InputColumnSpecs
462
+
463
+ end # module Nodepile
464
+
465
+ # Below are the column spec to be used for documentation and to some degree
466
+ __END__
467
+ ---
468
+ data:
469
+ fields:
470
+ _id:
471
+ description: >
472
+ Required column in any input file. Can be one of three value types.
473
+ If it starts with a literal asterisk character or with a literal
474
+ forward slash character, it indicates the line is a
475
+ style instruction. If it is blank or whitespace, it indicates the
476
+ line defines an edge or edge style instruction. Any other value
477
+ indicates that this line defines a node and the value in this column
478
+ is interpreted as a unique identifier (node_id) that can be used on
479
+ other lines to reference this node. Unless otherwise overridden, the
480
+ _id is used to label the node. Note that your life may be happier
481
+ if you forbid using commas as part of _id values although it is not
482
+ forbidden.
483
+ preproc:
484
+ - strip
485
+
486
+ _links_from:
487
+ description: >
488
+ Required if the _id field has been left blank. Specifies one or
489
+ more node_id values separated by valid delimiter characters. If the
490
+ first character is an asterisk or forward slash, it indicates that this
491
+ is a edge styling instruction. Otherwise, this is used to indicate
492
+ the existence of one or more edges originating from the specified node.
493
+ preproc:
494
+ - strip
495
+
496
+ _links_to:
497
+ description: Follows same protocol as _link_from.
498
+ preproc:
499
+ - strip
500
+
501
+ _label:
502
+ description: >
503
+ Indicates a (typically short) label that should appear rather than
504
+ value of the _id for nodes and edges. For nodes, see also _labelNN
505
+ which allows specifying node labels in a line-by-line format.
506
+ #preproc: # no preproc for this one... deliberate blanks may be meaningful
507
+
508
+ _labelNN:
509
+ description: >
510
+ If present and non-blank, this value supercedes any text in _label
511
+ column.
512
+ When a column with this pattern is specified it should replace
513
+ _labelNN with a integer such as _label3 or _label22 to indicate
514
+ that the provided text appears on line 3 or line 22 respectively.
515
+
516
+ _color:
517
+ decription: >
518
+ For nodes, color is the border color of the shape. For edges, this is
519
+ the actual edge color. There are a very wide variety of ways that
520
+ color can be specified. Any format supported by the DOT language is
521
+ permitted. The rock bottom simplest is to use the supported set of
522
+ simple color words like red, blue, etc.
523
+ dot_ref: https://graphviz.org/docs/attrs/color/
524
+ preproc:
525
+ - strip
526
+ - downcase
527
+
528
+ _fillcolor:
529
+ description: >
530
+ For nodes, fillcolor is the background color of the shape.
531
+ dot_ref: https://graphviz.org/docs/attrs/fillcolor/
532
+ preproc:
533
+ - strip
534
+ - downcase
535
+
536
+ _fontcolor:
537
+ description: >
538
+ For many entities, defines the text color.
539
+ dot_ref: https://graphviz.org/docs/attrs/fontcolor/
540
+ preproc:
541
+ - strip
542
+ - downcase
543
+
544
+
545
+ _shape:
546
+ description: >
547
+ For nodes, determines the shape of the node. Shape names tend to be
548
+ either simple things like (box, plain, plaintext, circle, ellipse,
549
+ etc.) or else it is a record type that is meant to render data
550
+ in a structured layout.
551
+ dot_ref: https://graphviz.org/docs/attr-types/shape/
552
+ preproc:
553
+ - strip
554
+ - downcase
555
+
556
+
557
+
558
+
559
+
560
+
561
+
562
+
@@ -0,0 +1,38 @@
1
+ require 'nodepile/gviz.rb'
2
+ require 'nodepile/pile_organizer.rb'
3
+
4
+ module Nodepile
5
+
6
+ # A set of large scale, batch-like operations
7
+ module GrossActions
8
+
9
+ # Given an output filepath and one or more tabular datafile inputs, render
10
+ # the visualization of the input datafiles.
11
+ # @param output_filepath [String] location where the output file should be
12
+ # written. Note, if the file already exists it
13
+ # will be overwritten. Note that the file
14
+ # suffix indicates the output format that will
15
+ # be used. Valid extensions are: jpg, dot, gif,
16
+ # svg, png, json, pdf
17
+ #
18
+ # @param input_filepaths [Array<String>] One or more input files. Note that the
19
+ # program will take a best guess at the text file
20
+ # format with file suffix (csv, tsv) being the
21
+ # a strong signal. The order of the filenames
22
+ # is used as their load order.
23
+ def self.render_to_file(output_filepath, *input_filepaths)
24
+ pile = Nodepile::PileOrganizer.new
25
+ gviz = Nodepile::GraphVisualizer.new
26
+ input_filepaths.each{|fpath|
27
+ raise "File not found: #{fpath}" unless File.exist?(fpath)
28
+ pile.load_from_file(fpath)
29
+ gviz.load(pile.node_records,pile.edge_records,configs: pile.pragmas)
30
+ gviz.emit_file(output_filepath,configs: pile.pragmas)
31
+ }
32
+ return nil # for now, return is meaningless
33
+ end #render_to_file()
34
+
35
+
36
+ end #module GrossActions
37
+
38
+ end #module Nodepile