Class: Tap::Support::Audit

Inherits:
Object show all
Defined in:
lib/tap/support/audit.rb

Overview

Audit provides a way to track the values (inputs and results) passed among tasks or, more generally, any Executable. Audits allow you to track inputs as they make their way through a workflow, and have great utility in debugging and record keeping.

During execution, the inputs to a task are used to initialize an Audit. These inputs are the original value of the audit and mark the begining of an audit trail; every task adds to the trail by recording it’s result and itself as the ‘source’ of the result.

Audits can take any object as a source, so for illustration lets use some symbols:

# initialize a new audit
a = Audit.new(1, nil)

# record some values
a._record(:A, 2)
a._record(:B, 3)

Now you can pull up the source and value trails, as well as the current and original values:

a._source_trail      # => [nil, :A, :B]
a._value_trail       # => [1, 2, 3]

a._original          # => 1
a._original_source   # => nil

a._current           # => 3
a._current_source    # => :B

Merges are supported by using an array of the merged trails (actually an AuditMerge) as the source, and an array of the merged values as the original value.

b = Audit.new(10, nil)
b._record(:C, 11)
b._record(:D, 12)  

c = Audit.merge(a, b)
c._source_trail      # => [ [[nil, :A, :B], [nil, :C, :D]] ]
c._value_trail       # => [ [[1,2,3], [10, 11, 12]] ]
c._current           # => [3, 12]

c._record(:E, "a string value")
c._record(:F, {'a' => 'hash value'})
c._record(:G, ['an', 'array', 'value'])

c._source_trail      # => [ [[nil, :A, :B], [nil, :C, :D]], :E, :F, :G]
c._value_trail       # => [ [[1,2,3], [10, 11, 12]], "a string value", {'a' => 'hash value'}, ['an', 'array', 'value']]

Audit supports forks by duplicating the source and value trails. Forks can be developed independently. Audits are also forked during a merge; notice the additional record in ‘a’ doesn’t change the source trail for ‘c’:

a1 = a._fork

a._record(:X, -1)
a1._record(:Y, -2)

a._source_trail      # => [nil, :A, :B, :X]
a1._source_trail     # => [nil, :A, :B, :Y]
c._source_trail      # => [ [[nil, :A, :B], [nil, :C, :D]], :E, :F, :G]

The data structure for an audit gets nasty after a few merges because the lead array gets more and more nested. Audit provides iterators to help gain access, as well as a printing method to visualize the audit trail:

c._to_s
# =>
# o-[] 1
# o-[A] 2
# o-[B] 3
# | 
# | o-[] 10
# | o-[C] 11
# | o-[D] 12
# | | 
# `-`-o-[E] "a string value"
#     o-[F] {"a"=>"hash value"}
#     o-[G] ["an", "array", "value"]

In practice, tasks are recored as sources. Thus source trails can be used

to access task configurations and other information that may be useful when creating reports or making workflow decisions.

– TODO: Track nesting level of ams; see if you can hook this into the _to_s process to make extraction/presentation of audits more managable.

Create a FirstLastArray to minimize the audit data collected. Allow different audit modes:

  • full ([] both)

  • source_only (fl value)

  • minimal (fl source and value)

Try to work a _to_s that doesn’t repeat the same audit twice. Think about a format like:

      | 
------|-----+
      |     | 
------|-----|-----+ 
      |     |     | 
      `-----`-----`-o-[j] j5

Constant Summary collapse

AUDIT_NIL =

An arbitrary object used to identify when no inputs have been provided to Audit.new. (nil cannot be used since nil is a valid initial value)

Object.new

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(value = AUDIT_NIL, source = nil) ⇒ Audit

A new audit takes a value and/or source. A nil source is typically given for the original value.



187
188
189
190
191
192
# File 'lib/tap/support/audit.rb', line 187

def initialize(value=AUDIT_NIL, source=nil)
  @_sources = []
  @_values = []
  
  _record(source, value) unless value == AUDIT_NIL
end

Instance Attribute Details

#_sourcesObject

An array of the sources in self



175
176
177
# File 'lib/tap/support/audit.rb', line 175

def _sources
  @_sources
end

#_valuesObject

An array of the values in self



178
179
180
# File 'lib/tap/support/audit.rb', line 178

def _values
  @_values
end

Class Method Details

.merge(*audits) ⇒ Object

Creates a new Audit by merging the input audits. The value of the new Audit will be an array of the _current values of the inputs. The source will be an AuditMerge whose values are forks of the inputs. Non-Audit sources may be provided; they are initialized to Audits before merging.

a = Audit.new
a._record(:a, 'a')

b = Audit.new
b._record(:b, 'b')

c = Audit.merge(a, b, 1)
c._record(:c, 'c')

c._values        # => [['a','b', 1], 'c']
c._sources       # => [AuditMerge[a, b, Audit.new(1)], :c]

If no audits are provided, merge returns a new Audit. If only one audit is provided, merge returns a fork of that audit.



160
161
162
163
164
165
166
167
168
169
170
171
# File 'lib/tap/support/audit.rb', line 160

def merge(*audits)
  case audits.length
  when 0 then Audit.new
  when 1 then audits[0]._fork
  else
    sources = AuditMerge.new
    audits.each {|a| sources << (a.kind_of?(Audit) ? a._fork : Audit.new(a)) }
    values = audits.collect {|a| a.kind_of?(Audit) ? a._current : a}
  
    Audit.new(values, sources)
  end
end

Instance Method Details

#==(another) ⇒ Object

Returns true if the _sources and _values for self are equal to those of another.



293
294
295
# File 'lib/tap/support/audit.rb', line 293

def ==(another)
  another.kind_of?(Audit) && self._sources == another._sources && self._values == another._values
end

#_collect_records(&block) ⇒ Object

:yields: source, value



249
250
251
252
253
254
255
# File 'lib/tap/support/audit.rb', line 249

def _collect_records(&block) # :yields: source, value
  collection = []
  0.upto(_sources.length-1) do |i|
    collection << collect_records(_sources[i], _values[i], &block)
  end
  collection
end

#_currentObject

The current (ie last) value recorded in the Audit



223
224
225
# File 'lib/tap/support/audit.rb', line 223

def _current
  _values.last
end

#_current_sourceObject

The current (ie last) source recorded in the Audit



233
234
235
# File 'lib/tap/support/audit.rb', line 233

def _current_source
  _sources.last
end

#_each_record(merge_level = 0, merge_index = 0, &block) ⇒ Object

:yields: source, value, merge_level, merge_index, index



257
258
259
260
261
# File 'lib/tap/support/audit.rb', line 257

def _each_record(merge_level=0, merge_index=0, &block) # :yields: source, value, merge_level, merge_index, index
  0.upto(_sources.length-1) do |i|
    each_record(_sources[i], _values[i], merge_level, merge_index, i, &block)
  end
end

#_forkObject

Produces a new Audit with duplicate sources and values, suitable for independent development.



270
271
272
273
274
275
# File 'lib/tap/support/audit.rb', line 270

def _fork
  a = Audit.new
  a._sources = _sources.dup
  a._values = _values.dup
  a
end

#_iterateObject

Produces a fork of self for each item in the current value (_current). Iterate is useful for developing each item of (say) an array along different paths.

Records the next value of each fork as [item, AuditIterate.new(<index of item>)].

Raises an error if _current does not respond to each.



283
284
285
286
287
288
289
# File 'lib/tap/support/audit.rb', line 283

def _iterate
  expanded = []
  _current.each do |value|
    expanded << _fork._record(AuditIterate.new(expanded.length), value)
  end
  expanded
end

#_merge(*audits) ⇒ Object

Creates a new Audit by merging self and the input audits, using Audit#merge.



264
265
266
# File 'lib/tap/support/audit.rb', line 264

def _merge(*audits)
  Audit.merge(self, *audits)
end

#_originalObject

The original value used to initialize the Audit



218
219
220
# File 'lib/tap/support/audit.rb', line 218

def _original
  _values.first
end

#_original_sourceObject

The original source used to initialize the Audit



228
229
230
# File 'lib/tap/support/audit.rb', line 228

def _original_source
  _sources.first
end

#_record(source, value) ⇒ Object

Records the next value produced by the source. When an audit is passed as a value, record will record the current value of the audit. Record will similarly resolve every audit in an array containing audits.

Example:

a = Audit.new(1)
b = Audit.new(2)
c = Audit.new(3)

c.record(:a, a) 
c.sources           # => [:a]
c.values            # => [1]

c.record(:ab, [a,b])
c.sources           # => [:a, :ab]
c.values            # => [1, [1, 2]]


211
212
213
214
215
# File 'lib/tap/support/audit.rb', line 211

def _record(source, value)
  _sources << source
  _values << value
  self
end

#_source_trailObject

Searches back and recursively (if the source is an audit) collects all sources for the current value.



239
240
241
# File 'lib/tap/support/audit.rb', line 239

def _source_trail
  _collect_records {|source, value| source}
end

#_to_sObject

A kind of pretty-print for Audits. See the example in the overview.



298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
# File 'lib/tap/support/audit.rb', line 298

def _to_s
  # TODO -- find a way to avoid repeating groups
  
  group = []
  groups = [group]
  extended_groups = [groups]
  group_merges = []
  extended_group_merges = []
  current_level = nil
  current_index = nil
  
  _each_record do |source, value, merge_level, merge_index, index|  
    source_str, value_str = if block_given?
      yield(source, value)
    else
      [source, value == nil ? '' : PP.singleline_pp(value, '')]
    end
    
    if !group.empty? && (merge_level != current_level || index == 0)
      unless merge_level <= current_level
        groups = [] 
        extended_groups << groups
      end

      group = []
      groups << group 
      
      if merge_level < current_level
        if merge_index == 0
          extended_group_merges << group.object_id
        end
        
        unless index == 0   
          group_merges << group.object_id
        end
      end
    end

    group << "o-[#{source_str}] #{value_str}"
    current_level = merge_level
    current_index = merge_index
  end

  lines = []
  group_prefix = ""
  extended_groups.each do |ext_groups|
    indentation = 0

    ext_groups.each_with_index do |ext_group, group_num|
      ext_group.each_with_index do |line, line_num|
        if line_num == 0
          unless lines.empty?
            lines << group_prefix + "  " * indentation + "| " * (group_num-indentation) 
          end
          
          if group_merges.include?(ext_group.object_id)
            lines << group_prefix + "  " * indentation + "`-" * (group_num-indentation) + line
            indentation = group_num
            
            if extended_group_merges.include?(ext_group.object_id) 
              lines.last.gsub!(/\| \s*/) {|match| "`-" + "-" * (match.length - 2)}
              group_prefix.gsub!(/\| /, " ")
            end
            next
          end
        end
        
        lines << group_prefix + "  " * indentation + "| " * (group_num-indentation) + line
      end
    end
    
    group_prefix += "  " * (ext_groups.length-1) + "| "
  end
  
  lines.join("\n") + "\n"
end

#_value_trailObject

Searches back and recursively (if the source is an audit) collects all values leading to the current value.



245
246
247
# File 'lib/tap/support/audit.rb', line 245

def _value_trail
  _collect_records {|source, value| value}
end