burner 1.0.0.pre.alpha → 1.0.0.pre.alpha.5

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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +320 -2
  3. data/burner.gemspec +3 -0
  4. data/lib/burner.rb +10 -0
  5. data/lib/burner/cli.rb +7 -7
  6. data/lib/burner/job.rb +4 -2
  7. data/lib/burner/jobs.rb +30 -10
  8. data/lib/burner/jobs/collection/arrays_to_objects.rb +43 -0
  9. data/lib/burner/jobs/collection/graph.rb +43 -0
  10. data/lib/burner/jobs/collection/objects_to_arrays.rb +54 -0
  11. data/lib/burner/jobs/collection/shift.rb +43 -0
  12. data/lib/burner/jobs/collection/transform.rb +64 -0
  13. data/lib/burner/jobs/collection/transform/attribute.rb +33 -0
  14. data/lib/burner/jobs/collection/transform/attribute_renderer.rb +36 -0
  15. data/lib/burner/jobs/collection/unpivot.rb +45 -0
  16. data/lib/burner/jobs/collection/values.rb +50 -0
  17. data/lib/burner/jobs/deserialize/csv.rb +28 -0
  18. data/lib/burner/jobs/deserialize/json.rb +1 -1
  19. data/lib/burner/jobs/deserialize/yaml.rb +1 -1
  20. data/lib/burner/jobs/dummy.rb +1 -1
  21. data/lib/burner/jobs/echo.rb +2 -2
  22. data/lib/burner/jobs/io/base.rb +3 -16
  23. data/lib/burner/jobs/io/exist.rb +43 -0
  24. data/lib/burner/jobs/io/read.rb +12 -2
  25. data/lib/burner/jobs/io/write.rb +25 -3
  26. data/lib/burner/jobs/serialize/csv.rb +38 -0
  27. data/lib/burner/jobs/serialize/json.rb +1 -1
  28. data/lib/burner/jobs/serialize/yaml.rb +1 -1
  29. data/lib/burner/jobs/set.rb +1 -1
  30. data/lib/burner/jobs/sleep.rb +1 -1
  31. data/lib/burner/modeling.rb +10 -0
  32. data/lib/burner/modeling/key_index_mapping.rb +29 -0
  33. data/lib/burner/payload.rb +19 -4
  34. data/lib/burner/pipeline.rb +10 -3
  35. data/lib/burner/step.rb +5 -3
  36. data/lib/burner/string_template.rb +6 -5
  37. data/lib/burner/version.rb +1 -1
  38. data/lib/burner/written_file.rb +28 -0
  39. metadata +59 -2
@@ -12,7 +12,7 @@ module Burner
12
12
  module Deserialize
13
13
  # Take a JSON string and deserialize into object(s).
14
14
  class Json < Job
15
- def perform(_output, payload, _params)
15
+ def perform(_output, payload)
16
16
  payload.value = JSON.parse(payload.value)
17
17
 
18
18
  nil
@@ -27,7 +27,7 @@ module Burner
27
27
  # in a sandbox. By default, though, we will try and drive them towards using it
28
28
  # in the safer alternative.
29
29
  # rubocop:disable Security/YAMLLoad
30
- def perform(output, payload, _params)
30
+ def perform(output, payload)
31
31
  output.detail('Warning: loading YAML not using safe_load.') unless safe
32
32
 
33
33
  payload.value = safe ? YAML.safe_load(payload.value) : YAML.load(payload.value)
@@ -11,7 +11,7 @@ module Burner
11
11
  class Jobs
12
12
  # Do nothing.
13
13
  class Dummy < Job
14
- def perform(_output, _payload, _params)
14
+ def perform(_output, _payload)
15
15
  nil
16
16
  end
17
17
  end
@@ -21,8 +21,8 @@ module Burner
21
21
  freeze
22
22
  end
23
23
 
24
- def perform(output, _payload, params)
25
- compiled_message = eval_string_template(message, params)
24
+ def perform(output, payload)
25
+ compiled_message = job_string_template(message, output, payload)
26
26
 
27
27
  output.detail(compiled_message)
28
28
 
@@ -12,27 +12,14 @@ module Burner
12
12
  module IO
13
13
  # Common configuration/code for all IO Job subclasses.
14
14
  class Base < Job
15
- attr_reader :binary, :path
15
+ attr_reader :path
16
16
 
17
- def initialize(name:, path:, binary: false)
17
+ def initialize(name:, path:)
18
18
  super(name: name)
19
19
 
20
20
  raise ArgumentError, 'path is required' if path.to_s.empty?
21
21
 
22
- @path = path.to_s
23
- @binary = binary || false
24
-
25
- freeze
26
- end
27
-
28
- private
29
-
30
- def compile_path(params)
31
- eval_string_template(path, params)
32
- end
33
-
34
- def mode
35
- binary ? 'wb' : 'w'
22
+ @path = path.to_s
36
23
  end
37
24
  end
38
25
  end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright (c) 2020-present, Blue Marble Payroll, LLC
5
+ #
6
+ # This source code is licensed under the MIT license found in the
7
+ # LICENSE file in the root directory of this source tree.
8
+ #
9
+
10
+ require_relative 'base'
11
+
12
+ module Burner
13
+ class Jobs
14
+ module IO
15
+ # Check to see if a file exists. If short_circuit is set to true and the file
16
+ # does not exist then the job will return false and short circuit the pipeline.
17
+ class Exist < Base
18
+ attr_reader :short_circuit
19
+
20
+ def initialize(name:, path:, short_circuit: false)
21
+ super(name: name, path: path)
22
+
23
+ @short_circuit = short_circuit || false
24
+
25
+ freeze
26
+ end
27
+
28
+ def perform(output, payload)
29
+ compiled_path = job_string_template(path, output, payload)
30
+
31
+ exists = File.exist?(compiled_path)
32
+ verb = exists ? 'does' : 'does not'
33
+
34
+ output.detail("The path: #{compiled_path} #{verb} exist")
35
+
36
+ # if anything but false is returned then the pipeline will not short circuit. So
37
+ # we need to make sure we explicitly return false.
38
+ short_circuit && !exists ? false : nil
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -14,8 +14,18 @@ module Burner
14
14
  module IO
15
15
  # Read value from disk.
16
16
  class Read < Base
17
- def perform(output, payload, params)
18
- compiled_path = compile_path(params)
17
+ attr_reader :binary
18
+
19
+ def initialize(name:, path:, binary: false)
20
+ super(name: name, path: path)
21
+
22
+ @binary = binary || false
23
+
24
+ freeze
25
+ end
26
+
27
+ def perform(output, payload)
28
+ compiled_path = job_string_template(path, output, payload)
19
29
 
20
30
  output.detail("Reading: #{compiled_path}")
21
31
 
@@ -14,14 +14,32 @@ module Burner
14
14
  module IO
15
15
  # Write value to disk.
16
16
  class Write < Base
17
- def perform(output, payload, params)
18
- compiled_path = compile_path(params)
17
+ attr_reader :binary
18
+
19
+ def initialize(name:, path:, binary: false)
20
+ super(name: name, path: path)
21
+
22
+ @binary = binary || false
23
+
24
+ freeze
25
+ end
26
+
27
+ def perform(output, payload)
28
+ compiled_path = job_string_template(path, output, payload)
19
29
 
20
30
  ensure_directory_exists(output, compiled_path)
21
31
 
22
32
  output.detail("Writing: #{compiled_path}")
23
33
 
24
- File.open(compiled_path, mode) { |io| io.write(payload.value) }
34
+ time_in_seconds = Benchmark.measure do
35
+ File.open(compiled_path, mode) { |io| io.write(payload.value) }
36
+ end.real
37
+
38
+ payload.add_written_file(
39
+ logical_filename: compiled_path,
40
+ physical_filename: compiled_path,
41
+ time_in_seconds: time_in_seconds
42
+ )
25
43
 
26
44
  nil
27
45
  end
@@ -39,6 +57,10 @@ module Burner
39
57
 
40
58
  nil
41
59
  end
60
+
61
+ def mode
62
+ binary ? 'wb' : 'w'
63
+ end
42
64
  end
43
65
  end
44
66
  end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright (c) 2020-present, Blue Marble Payroll, LLC
5
+ #
6
+ # This source code is licensed under the MIT license found in the
7
+ # LICENSE file in the root directory of this source tree.
8
+ #
9
+
10
+ module Burner
11
+ class Jobs
12
+ module Serialize
13
+ # Take an array of arrays and create a CSV.
14
+ # Expected Payload#value input: array of arrays.
15
+ # Payload#value output: a serialized CSV string.
16
+ class Csv < Job
17
+ def perform(_output, payload)
18
+ payload.value = CSV.generate(options) do |csv|
19
+ (payload.value || []).each do |row|
20
+ csv << row
21
+ end
22
+ end
23
+
24
+ nil
25
+ end
26
+
27
+ private
28
+
29
+ def options
30
+ {
31
+ headers: false,
32
+ write_headers: false
33
+ }
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -12,7 +12,7 @@ module Burner
12
12
  module Serialize
13
13
  # Treat value like a Ruby object and serialize it using JSON.
14
14
  class Json < Job
15
- def perform(_output, payload, _params)
15
+ def perform(_output, payload)
16
16
  payload.value = payload.value.to_json
17
17
 
18
18
  nil
@@ -12,7 +12,7 @@ module Burner
12
12
  module Serialize
13
13
  # Treat value like a Ruby object and serialize it using YAML.
14
14
  class Yaml < Job
15
- def perform(_output, payload, _params)
15
+ def perform(_output, payload)
16
16
  payload.value = payload.value.to_yaml
17
17
 
18
18
  nil
@@ -21,7 +21,7 @@ module Burner
21
21
  freeze
22
22
  end
23
23
 
24
- def perform(_output, payload, _params)
24
+ def perform(_output, payload)
25
25
  payload.value = value
26
26
 
27
27
  nil
@@ -21,7 +21,7 @@ module Burner
21
21
  freeze
22
22
  end
23
23
 
24
- def perform(output, _payload, _params)
24
+ def perform(output, _payload)
25
25
  output.detail("Going to sleep for #{seconds} second(s)")
26
26
 
27
27
  Kernel.sleep(seconds)
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright (c) 2020-present, Blue Marble Payroll, LLC
5
+ #
6
+ # This source code is licensed under the MIT license found in the
7
+ # LICENSE file in the root directory of this source tree.
8
+ #
9
+
10
+ require_relative 'modeling/key_index_mapping'
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright (c) 2020-present, Blue Marble Payroll, LLC
5
+ #
6
+ # This source code is licensed under the MIT license found in the
7
+ # LICENSE file in the root directory of this source tree.
8
+ #
9
+
10
+ module Burner
11
+ module Modeling
12
+ # Generic relationship between a numeric index and a key.
13
+ class KeyIndexMapping
14
+ acts_as_hashable
15
+
16
+ attr_reader :index, :key
17
+
18
+ def initialize(index:, key:)
19
+ raise ArgumentError, 'index is required' if index.to_s.empty?
20
+ raise ArgumentError, 'key is required' if key.to_s.empty?
21
+
22
+ @index = index.to_i
23
+ @key = key.to_s
24
+
25
+ freeze
26
+ end
27
+ end
28
+ end
29
+ end
@@ -7,6 +7,8 @@
7
7
  # LICENSE file in the root directory of this source tree.
8
8
  #
9
9
 
10
+ require_relative 'written_file'
11
+
10
12
  module Burner
11
13
  # The input for all Job#perform methods. The main notion of this object is its "value"
12
14
  # attribute. This is dynamic and weak on purpose and is subject to whatever the Job#perform
@@ -16,11 +18,24 @@ module Burner
16
18
  class Payload
17
19
  attr_accessor :value
18
20
 
19
- attr_reader :context
21
+ attr_reader :params,
22
+ :time,
23
+ :written_files
24
+
25
+ def initialize(
26
+ params: {},
27
+ time: Time.now.utc,
28
+ value: nil,
29
+ written_files: []
30
+ )
31
+ @params = params || {}
32
+ @time = time || Time.now.utc
33
+ @value = value
34
+ @written_files = written_files || []
35
+ end
20
36
 
21
- def initialize(context: {}, value: nil)
22
- @context = context || {}
23
- @value = value
37
+ def add_written_file(written_file)
38
+ tap { written_files << WrittenFile.make(written_file) }
24
39
  end
25
40
  end
26
41
  end
@@ -35,14 +35,21 @@ module Burner
35
35
  end
36
36
 
37
37
  # The main entry-point for kicking off a pipeline.
38
- def execute(params: {}, output: Output.new, payload: Payload.new)
38
+ def execute(output: Output.new, payload: Payload.new)
39
39
  output.write("Pipeline started with #{steps.length} step(s)")
40
40
 
41
- output_params(params, output)
41
+ output_params(payload.params, output)
42
42
  output.ruler
43
43
 
44
44
  time_in_seconds = Benchmark.measure do
45
- steps.each { |step| step.perform(output, payload, params) }
45
+ steps.each do |step|
46
+ return_value = step.perform(output, payload)
47
+
48
+ if return_value.is_a?(FalseClass)
49
+ output.detail('Job returned false, ending pipeline.')
50
+ break
51
+ end
52
+ end
46
53
  end.real.round(3)
47
54
 
48
55
  output.ruler
@@ -28,16 +28,18 @@ module Burner
28
28
  freeze
29
29
  end
30
30
 
31
- def perform(output, payload, params)
31
+ def perform(output, payload)
32
+ return_value = nil
33
+
32
34
  output.title("#{job.class.name}#{SEPARATOR}#{job.name}")
33
35
 
34
36
  time_in_seconds = Benchmark.measure do
35
- job.perform(output, payload, params)
37
+ return_value = job.perform(output, payload)
36
38
  end.real.round(3)
37
39
 
38
40
  output.complete(time_in_seconds)
39
41
 
40
- nil
42
+ return_value
41
43
  end
42
44
  end
43
45
  end
@@ -9,10 +9,11 @@
9
9
 
10
10
  module Burner
11
11
  # Can take in a string and an object and use the object for formatting string interpolations
12
- # using tokens of form: {attribute_name}. It can also understand dot-notation for nested
13
- # objects using the Objectable library. Another benefit of using Objectable for resolution
14
- # is that it can understand almost any type of object: Hash, Struct, OpenStruct, custom
15
- # objects, etc. For more information see underlying libraries:
12
+ # using tokens of form: {attribute_name}. This templating class does not understand nested
13
+ # structures, so input should be a flat object/hash in the form of key-value pairs. A benefit of
14
+ # using Objectable for resolution is that it can understand almost any type of
15
+ # object: Hash, Struct, OpenStruct, custom objects, etc.
16
+ # For more information see underlying libraries:
16
17
  # * Stringento: https://github.com/bluemarblepayroll/stringento
17
18
  # * Objectable: https://github.com/bluemarblepayroll/objectable
18
19
  class StringTemplate
@@ -21,7 +22,7 @@ module Burner
21
22
  attr_reader :resolver
22
23
 
23
24
  def initialize
24
- @resolver = Objectable.resolver
25
+ @resolver = Objectable.resolver(separator: '')
25
26
 
26
27
  freeze
27
28
  end
@@ -8,5 +8,5 @@
8
8
  #
9
9
 
10
10
  module Burner
11
- VERSION = '1.0.0-alpha'
11
+ VERSION = '1.0.0-alpha.5'
12
12
  end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright (c) 2020-present, Blue Marble Payroll, LLC
5
+ #
6
+ # This source code is licensed under the MIT license found in the
7
+ # LICENSE file in the root directory of this source tree.
8
+ #
9
+
10
+ module Burner
11
+ # Describes a file that was generated by a Job. If a Job emits a file, it should also add the
12
+ # file details to the Payload#written_files array using the Payload#add_written_file method.
13
+ class WrittenFile
14
+ acts_as_hashable
15
+
16
+ attr_reader :logical_filename,
17
+ :physical_filename,
18
+ :time_in_seconds
19
+
20
+ def initialize(logical_filename:, physical_filename:, time_in_seconds:)
21
+ @logical_filename = logical_filename.to_s
22
+ @physical_filename = physical_filename.to_s
23
+ @time_in_seconds = time_in_seconds.to_f
24
+
25
+ freeze
26
+ end
27
+ end
28
+ end