burner 1.0.0.pre.alpha.4 → 1.0.0.pre.alpha.9

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +2 -0
  3. data/README.md +52 -47
  4. data/burner.gemspec +1 -1
  5. data/exe/burner +2 -3
  6. data/lib/burner.rb +2 -0
  7. data/lib/burner/cli.rb +2 -0
  8. data/lib/burner/job.rb +27 -9
  9. data/lib/burner/job_with_register.rb +24 -0
  10. data/lib/burner/jobs.rb +28 -39
  11. data/lib/burner/library.rb +32 -0
  12. data/lib/burner/library/collection/arrays_to_objects.rb +75 -0
  13. data/lib/burner/{jobs → library}/collection/graph.rb +7 -8
  14. data/lib/burner/library/collection/objects_to_arrays.rb +88 -0
  15. data/lib/burner/{jobs → library}/collection/shift.rb +8 -9
  16. data/lib/burner/{jobs → library}/collection/transform.rb +17 -15
  17. data/lib/burner/{jobs → library}/collection/unpivot.rb +17 -9
  18. data/lib/burner/library/collection/validate.rb +89 -0
  19. data/lib/burner/library/collection/values.rb +49 -0
  20. data/lib/burner/{jobs → library}/deserialize/csv.rb +4 -5
  21. data/lib/burner/{jobs → library}/deserialize/json.rb +6 -5
  22. data/lib/burner/{jobs → library}/deserialize/yaml.rb +13 -7
  23. data/lib/burner/{jobs → library}/dummy.rb +4 -4
  24. data/lib/burner/{jobs → library}/echo.rb +3 -3
  25. data/lib/burner/{jobs → library}/io/base.rb +4 -4
  26. data/lib/burner/{jobs → library}/io/exist.rb +11 -9
  27. data/lib/burner/{jobs → library}/io/read.rb +7 -6
  28. data/lib/burner/{jobs → library}/io/write.rb +9 -6
  29. data/lib/burner/{jobs → library}/serialize/csv.rb +5 -6
  30. data/lib/burner/{jobs → library}/serialize/json.rb +6 -5
  31. data/lib/burner/{jobs → library}/serialize/yaml.rb +6 -5
  32. data/lib/burner/{jobs/set.rb → library/set_value.rb} +8 -7
  33. data/lib/burner/{jobs → library}/sleep.rb +3 -3
  34. data/lib/burner/modeling.rb +3 -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/validations.rb +23 -0
  38. data/lib/burner/modeling/validations/base.rb +35 -0
  39. data/lib/burner/modeling/validations/blank.rb +31 -0
  40. data/lib/burner/modeling/validations/present.rb +31 -0
  41. data/lib/burner/payload.rb +52 -15
  42. data/lib/burner/pipeline.rb +23 -4
  43. data/lib/burner/side_effects.rb +10 -0
  44. data/lib/burner/side_effects/written_file.rb +28 -0
  45. data/lib/burner/step.rb +1 -5
  46. data/lib/burner/util.rb +11 -0
  47. data/lib/burner/util/arrayable.rb +30 -0
  48. data/lib/burner/util/string_template.rb +42 -0
  49. data/lib/burner/version.rb +1 -1
  50. metadata +40 -28
  51. data/lib/burner/jobs/collection/arrays_to_objects.rb +0 -43
  52. data/lib/burner/jobs/collection/objects_to_arrays.rb +0 -54
  53. data/lib/burner/jobs/collection/transform/attribute.rb +0 -33
  54. data/lib/burner/jobs/collection/transform/attribute_renderer.rb +0 -36
  55. data/lib/burner/string_template.rb +0 -40
  56. data/lib/burner/written_file.rb +0 -28
@@ -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,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,35 +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
20
-
21
22
  attr_reader :params,
22
- :time,
23
- :written_files
23
+ :registers,
24
+ :side_effects,
25
+ :time
24
26
 
25
27
  def initialize(
26
28
  params: {},
27
- time: Time.now.utc,
28
- value: nil,
29
- written_files: []
29
+ registers: {},
30
+ side_effects: [],
31
+ time: Time.now.utc
30
32
  )
31
- @params = params || {}
32
- @time = time || Time.now.utc
33
- @value = value
34
- @written_files = written_files || []
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
67
+
68
+ private
69
+
70
+ def set(key, value)
71
+ registers[key.to_s] = value
35
72
  end
36
73
 
37
- def add_written_file(written_file)
38
- tap { written_files << WrittenFile.make(written_file) }
74
+ def add_registers(registers)
75
+ (registers || {}).each { |k, v| set(k, v) }
39
76
  end
40
77
  end
41
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]
@@ -43,10 +48,10 @@ module Burner
43
48
 
44
49
  time_in_seconds = Benchmark.measure do
45
50
  steps.each do |step|
46
- return_value = step.perform(output, payload)
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:')
@@ -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 'side_effects/written_file'
@@ -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
+ module SideEffects
12
+ # Describes a file that was generated by a Job. If a Job emits a file, it should also add the
13
+ # file details to the Payload#side_effects array using the Payload#add_side_effect method.
14
+ class WrittenFile
15
+ attr_reader :logical_filename,
16
+ :physical_filename,
17
+ :time_in_seconds
18
+
19
+ def initialize(logical_filename:, physical_filename:, time_in_seconds:)
20
+ @logical_filename = logical_filename.to_s
21
+ @physical_filename = physical_filename.to_s
22
+ @time_in_seconds = time_in_seconds.to_f
23
+
24
+ freeze
25
+ end
26
+ end
27
+ end
28
+ end
@@ -29,17 +29,13 @@ module Burner
29
29
  end
30
30
 
31
31
  def perform(output, payload)
32
- return_value = nil
33
-
34
32
  output.title("#{job.class.name}#{SEPARATOR}#{job.name}")
35
33
 
36
34
  time_in_seconds = Benchmark.measure do
37
- return_value = job.perform(output, payload)
35
+ job.perform(output, payload)
38
36
  end.real.round(3)
39
37
 
40
38
  output.complete(time_in_seconds)
41
-
42
- return_value
43
39
  end
44
40
  end
45
41
  end
@@ -0,0 +1,11 @@
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 'util/arrayable'
11
+ require_relative 'util/string_template'
@@ -0,0 +1,30 @@
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 Util
12
+ # Provide helper methods for dealing with Arrays.
13
+ module Arrayable
14
+ # Since Ruby's Kernel#Array will properly call #to_a for scalar Hash objects, this could
15
+ # return something funky in the context of this library. In this library, Hash instances
16
+ # are typically viewed as an atomic key-value-based "object". This library likes to deal
17
+ # with object-like things, treating Hash, OpenStruct, Struct, or Object subclasses as
18
+ # basically the same thing. In this vein, this library leverages Objectable to help
19
+ # unify access data from objects. See the Objectable library for more information:
20
+ # https://github.com/bluemarblepayroll/objectable
21
+ def array(value)
22
+ if value.is_a?(Hash)
23
+ [value]
24
+ else
25
+ Array(value)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,42 @@
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 Util
12
+ # Can take in a string and an object and use the object for formatting string interpolations
13
+ # using tokens of form: {attribute_name}. This templating class does not understand nested
14
+ # structures, so input should be a flat object/hash in the form of key-value pairs.
15
+ # A benefit of using Objectable for resolution is that it can understand almost any type of
16
+ # object: Hash, Struct, OpenStruct, custom objects, etc.
17
+ # For more information see underlying libraries:
18
+ # * Stringento: https://github.com/bluemarblepayroll/stringento
19
+ # * Objectable: https://github.com/bluemarblepayroll/objectable
20
+ class StringTemplate
21
+ include Singleton
22
+
23
+ attr_reader :resolver
24
+
25
+ def initialize
26
+ @resolver = Objectable.resolver(separator: '')
27
+
28
+ freeze
29
+ end
30
+
31
+ # For general consumption
32
+ def evaluate(expression, input)
33
+ Stringento.evaluate(expression, input, resolver: self)
34
+ end
35
+
36
+ # For Stringento consumption
37
+ def resolve(value, input)
38
+ resolver.get(input, value)
39
+ end
40
+ end
41
+ end
42
+ end