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

Sign up to get free protection for your applications and to get access to all the features.
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