burner 1.0.0.pre.alpha.3 → 1.0.0.pre.alpha.8

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +2 -0
  3. data/README.md +76 -44
  4. data/burner.gemspec +4 -1
  5. data/exe/burner +2 -3
  6. data/lib/burner.rb +11 -0
  7. data/lib/burner/cli.rb +10 -10
  8. data/lib/burner/job.rb +29 -9
  9. data/lib/burner/job_with_register.rb +24 -0
  10. data/lib/burner/jobs.rb +27 -23
  11. data/lib/burner/library.rb +32 -0
  12. data/lib/burner/library/collection/arrays_to_objects.rb +75 -0
  13. data/lib/burner/library/collection/graph.rb +42 -0
  14. data/lib/burner/library/collection/objects_to_arrays.rb +88 -0
  15. data/lib/burner/library/collection/shift.rb +42 -0
  16. data/lib/burner/library/collection/transform.rb +66 -0
  17. data/lib/burner/library/collection/unpivot.rb +53 -0
  18. data/lib/burner/library/collection/validate.rb +89 -0
  19. data/lib/burner/library/collection/values.rb +49 -0
  20. data/lib/burner/library/deserialize/csv.rb +27 -0
  21. data/lib/burner/{jobs → library}/deserialize/json.rb +7 -6
  22. data/lib/burner/{jobs → library}/deserialize/yaml.rb +14 -8
  23. data/lib/burner/{jobs → library}/dummy.rb +4 -4
  24. data/lib/burner/{jobs → library}/echo.rb +5 -5
  25. data/lib/burner/{jobs → library}/io/base.rb +4 -10
  26. data/lib/burner/{jobs → library}/io/exist.rb +13 -11
  27. data/lib/burner/{jobs → library}/io/read.rb +9 -8
  28. data/lib/burner/{jobs → library}/io/write.rb +11 -8
  29. data/lib/burner/library/serialize/csv.rb +37 -0
  30. data/lib/burner/{jobs → library}/serialize/json.rb +7 -6
  31. data/lib/burner/{jobs → library}/serialize/yaml.rb +7 -6
  32. data/lib/burner/{jobs/set.rb → library/set_value.rb} +9 -8
  33. data/lib/burner/{jobs → library}/sleep.rb +4 -4
  34. data/lib/burner/modeling.rb +13 -0
  35. data/lib/burner/modeling/attribute.rb +29 -0
  36. data/lib/burner/modeling/attribute_renderer.rb +32 -0
  37. data/lib/burner/modeling/key_index_mapping.rb +29 -0
  38. data/lib/burner/modeling/validations.rb +23 -0
  39. data/lib/burner/modeling/validations/base.rb +35 -0
  40. data/lib/burner/modeling/validations/blank.rb +31 -0
  41. data/lib/burner/modeling/validations/present.rb +31 -0
  42. data/lib/burner/payload.rb +55 -10
  43. data/lib/burner/pipeline.rb +25 -6
  44. data/lib/burner/side_effects.rb +10 -0
  45. data/lib/burner/side_effects/written_file.rb +28 -0
  46. data/lib/burner/step.rb +2 -8
  47. data/lib/burner/util.rb +11 -0
  48. data/lib/burner/util/arrayable.rb +30 -0
  49. data/lib/burner/util/string_template.rb +42 -0
  50. data/lib/burner/version.rb +1 -1
  51. metadata +84 -18
  52. data/lib/burner/string_template.rb +0 -40
  53. data/lib/burner/written_file.rb +0 -28
@@ -8,14 +8,15 @@
8
8
  #
9
9
 
10
10
  module Burner
11
- class Jobs
11
+ module Library
12
12
  module Serialize
13
13
  # Treat value like a Ruby object and serialize it using JSON.
14
- class Json < Job
15
- def perform(_output, payload, _params)
16
- payload.value = payload.value.to_json
17
-
18
- nil
14
+ #
15
+ # Expected Payload#value input: anything.
16
+ # Payload#value output: string representing the output of the JSON serializer.
17
+ class Json < JobWithRegister
18
+ def perform(_output, payload)
19
+ payload[register] = payload[register].to_json
19
20
  end
20
21
  end
21
22
  end
@@ -8,14 +8,15 @@
8
8
  #
9
9
 
10
10
  module Burner
11
- class Jobs
11
+ module Library
12
12
  module Serialize
13
13
  # Treat value like a Ruby object and serialize it using YAML.
14
- class Yaml < Job
15
- def perform(_output, payload, _params)
16
- payload.value = payload.value.to_yaml
17
-
18
- nil
14
+ #
15
+ # Expected Payload#value input: anything.
16
+ # Payload#value output: string representing the output of the YAML serializer.
17
+ class Yaml < JobWithRegister
18
+ def perform(_output, payload)
19
+ payload[register] = payload[register].to_yaml
19
20
  end
20
21
  end
21
22
  end
@@ -8,23 +8,24 @@
8
8
  #
9
9
 
10
10
  module Burner
11
- class Jobs
11
+ module Library
12
12
  # Arbitrarily set value
13
- class Set < Job
13
+ #
14
+ # Expected Payload#value input: anything.
15
+ # Payload#value output: whatever value was specified in this job.
16
+ class SetValue < JobWithRegister
14
17
  attr_reader :value
15
18
 
16
- def initialize(name:, value: nil)
17
- super(name: name)
19
+ def initialize(name:, register: '', value: nil)
20
+ super(name: name, register: register)
18
21
 
19
22
  @value = value
20
23
 
21
24
  freeze
22
25
  end
23
26
 
24
- def perform(_output, payload, _params)
25
- payload.value = value
26
-
27
- nil
27
+ def perform(_output, payload)
28
+ payload[register] = value
28
29
  end
29
30
  end
30
31
  end
@@ -8,8 +8,10 @@
8
8
  #
9
9
 
10
10
  module Burner
11
- class Jobs
11
+ module Library
12
12
  # Arbitrarily put thread to sleep for X number of seconds
13
+ #
14
+ # Payload#value output: whatever value was specified in this job.
13
15
  class Sleep < Job
14
16
  attr_reader :seconds
15
17
 
@@ -21,12 +23,10 @@ module Burner
21
23
  freeze
22
24
  end
23
25
 
24
- def perform(output, _payload, _params)
26
+ def perform(output, _payload)
25
27
  output.detail("Going to sleep for #{seconds} second(s)")
26
28
 
27
29
  Kernel.sleep(seconds)
28
-
29
- nil
30
30
  end
31
31
  end
32
32
  end
@@ -0,0 +1,13 @@
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/attribute'
11
+ require_relative 'modeling/attribute_renderer'
12
+ require_relative 'modeling/key_index_mapping'
13
+ require_relative 'modeling/validations'
@@ -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
+ # Defines a top-level key and the associated transformers for deriving the final value
13
+ # to set the key to.
14
+ class Attribute
15
+ acts_as_hashable
16
+
17
+ attr_reader :key, :transformers
18
+
19
+ def initialize(key:, transformers: [])
20
+ raise ArgumentError, 'key is required' if key.to_s.empty?
21
+
22
+ @key = key.to_s
23
+ @transformers = Realize::Transformers.array(transformers)
24
+
25
+ freeze
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,32 @@
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
+ # Composed of an Attribute instance and a Pipeline instance. It knows how to
13
+ # render/transform an Attribute. Since this library is data-first, these intermediary
14
+ # objects are necessary for non-data-centric modeling.
15
+ class AttributeRenderer
16
+ extend Forwardable
17
+
18
+ attr_reader :attribute, :pipeline
19
+
20
+ def_delegators :attribute, :key
21
+
22
+ def_delegators :pipeline, :transform
23
+
24
+ def initialize(attribute, resolver)
25
+ @attribute = attribute
26
+ @pipeline = Realize::Pipeline.new(attribute.transformers, resolver: resolver)
27
+
28
+ freeze
29
+ end
30
+ end
31
+ end
32
+ end
@@ -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
@@ -0,0 +1,23 @@
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 'validations/blank'
11
+ require_relative 'validations/present'
12
+
13
+ module Burner
14
+ module Modeling
15
+ # Factory for building sub-classes that can validate an individual object and field value.
16
+ class Validations
17
+ acts_as_hashable_factory
18
+
19
+ register 'blank', Validations::Blank
20
+ register 'present', Validations::Present
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,35 @@
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
+ class Validations
13
+ # Common logic shared among all Validation subclasses.
14
+ # This class is an abstract class, make sure to implement:
15
+ # - #valid?(object, resolver)
16
+ # - #default_message
17
+ class Base
18
+ acts_as_hashable
19
+
20
+ attr_reader :key
21
+
22
+ def initialize(key:, message: '')
23
+ raise ArgumentError, 'key is required' if key.to_s.empty?
24
+
25
+ @key = key.to_s
26
+ @message = message.to_s
27
+ end
28
+
29
+ def message
30
+ @message.to_s.empty? ? "#{key}#{default_message}" : @message.to_s
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,31 @@
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
+ module Modeling
14
+ class Validations
15
+ # Check if a value is blank, if it is not blank then it is not valid.
16
+ class Blank < Base
17
+ acts_as_hashable
18
+
19
+ def valid?(object, resolver)
20
+ resolver.get(object, key).to_s.empty?
21
+ end
22
+
23
+ private
24
+
25
+ def default_message
26
+ ' must be blank'
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,31 @@
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
+ module Modeling
14
+ class Validations
15
+ # Check if a value is present. If it is blank (null or empty) then it is invalid.
16
+ class Present < Base
17
+ acts_as_hashable
18
+
19
+ def valid?(object_value, resolver)
20
+ !resolver.get(object_value, key).to_s.empty?
21
+ end
22
+
23
+ private
24
+
25
+ def default_message
26
+ ' is required'
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -7,27 +7,72 @@
7
7
  # LICENSE file in the root directory of this source tree.
8
8
  #
9
9
 
10
- require_relative 'written_file'
11
-
12
10
  module Burner
13
11
  # The input for all Job#perform methods. The main notion of this object is its "value"
14
12
  # attribute. This is dynamic and weak on purpose and is subject to whatever the Job#perform
15
13
  # methods decides it is. This definitely adds an order-of-magnitude complexity to this whole
16
14
  # library and lifecycle, but I am not sure there is any other way around it: trying to build
17
15
  # a generic, open-ended object pipeline to serve almost any use case.
16
+ #
17
+ # The side_effects attribute can also be utilized as a way for jobs to emit any data in a more
18
+ # structured/additive manner. The initial use case for this was for Burner's core IO jobs to
19
+ # report back the files it has written in a more structured data way (as opposed to simply
20
+ # writing some information to the output.)
18
21
  class Payload
19
- attr_accessor :value
22
+ attr_reader :params,
23
+ :registers,
24
+ :side_effects,
25
+ :time
26
+
27
+ def initialize(
28
+ params: {},
29
+ registers: {},
30
+ side_effects: [],
31
+ time: Time.now.utc
32
+ )
33
+ @params = params || {}
34
+ @registers = {}
35
+ @side_effects = side_effects || []
36
+ @time = time || Time.now.utc
37
+
38
+ add_registers(registers)
39
+ end
40
+
41
+ # Add a side effect of a job. This helps to keep track of things jobs do outside of its
42
+ # register mutations.
43
+ def add_side_effect(side_effect)
44
+ tap { side_effects << side_effect }
45
+ end
46
+
47
+ # Set a register's value.
48
+ def []=(key, value)
49
+ set(key, value)
50
+ end
51
+
52
+ # Retrieve a register's value.
53
+ def [](key)
54
+ registers[key.to_s]
55
+ end
56
+
57
+ # Set halt_pipeline to true. This will indicate to the pipeline to stop all
58
+ # subsequent processing.
59
+ def halt_pipeline
60
+ @halt_pipeline = true
61
+ end
62
+
63
+ # Check and see if halt_pipeline was called.
64
+ def halt_pipeline?
65
+ @halt_pipeline || false
66
+ end
20
67
 
21
- attr_reader :context, :written_files
68
+ private
22
69
 
23
- def initialize(context: {}, value: nil, written_files: [])
24
- @context = context || {}
25
- @value = value
26
- @written_files = written_files || []
70
+ def set(key, value)
71
+ registers[key.to_s] = value
27
72
  end
28
73
 
29
- def add_written_file(written_file)
30
- tap { written_files << WrittenFile.make(written_file) }
74
+ def add_registers(registers)
75
+ (registers || {}).each { |k, v| set(k, v) }
31
76
  end
32
77
  end
33
78
  end
@@ -19,11 +19,16 @@ module Burner
19
19
  acts_as_hashable
20
20
 
21
21
  class JobNotFoundError < StandardError; end
22
+ class DuplicateJobNameError < StandardError; end
22
23
 
23
24
  attr_reader :steps
24
25
 
25
26
  def initialize(jobs: [], steps: [])
26
- jobs_by_name = Jobs.array(jobs).map { |job| [job.name, job] }.to_h
27
+ jobs = Jobs.array(jobs)
28
+
29
+ assert_unique_job_names(jobs)
30
+
31
+ jobs_by_name = jobs.map { |job| [job.name, job] }.to_h
27
32
 
28
33
  @steps = Array(steps).map do |step_name|
29
34
  job = jobs_by_name[step_name.to_s]
@@ -35,18 +40,18 @@ module Burner
35
40
  end
36
41
 
37
42
  # The main entry-point for kicking off a pipeline.
38
- def execute(output: Output.new, params: {}, payload: Payload.new)
43
+ def execute(output: Output.new, payload: Payload.new)
39
44
  output.write("Pipeline started with #{steps.length} step(s)")
40
45
 
41
- output_params(params, output)
46
+ output_params(payload.params, output)
42
47
  output.ruler
43
48
 
44
49
  time_in_seconds = Benchmark.measure do
45
50
  steps.each do |step|
46
- return_value = step.perform(output, payload, params)
51
+ step.perform(output, payload)
47
52
 
48
- if return_value.is_a?(FalseClass)
49
- output.detail('Job returned false, ending pipeline.')
53
+ if payload.halt_pipeline?
54
+ output.detail('Payload was halted, ending pipeline.')
50
55
  break
51
56
  end
52
57
  end
@@ -60,6 +65,20 @@ module Burner
60
65
 
61
66
  private
62
67
 
68
+ def assert_unique_job_names(jobs)
69
+ unique_job_names = Set.new
70
+
71
+ jobs.each do |job|
72
+ if unique_job_names.include?(job.name)
73
+ raise DuplicateJobNameError, "job with name: #{job.name} already declared"
74
+ end
75
+
76
+ unique_job_names << job.name
77
+ end
78
+
79
+ nil
80
+ end
81
+
63
82
  def output_params(params, output)
64
83
  if params.keys.any?
65
84
  output.write('Parameters:')