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

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 (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,75 @@
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 Library
12
+ module Collection
13
+ # Convert an array of arrays to an array of objects. Pass in an array of
14
+ # Burner::Modeling::KeyIndexMapping instances or hashable configurations which specifies
15
+ # the index-to-key mappings to use.
16
+ #
17
+ # Expected Payload#value input: array of arrays.
18
+ # Payload#value output: An array of hashes.
19
+ #
20
+ # An example using a configuration-first pipeline:
21
+ #
22
+ # config = {
23
+ # jobs: [
24
+ # {
25
+ # name: 'set',
26
+ # type: 'b/set_value',
27
+ # value: [
28
+ # [1, 'funky']
29
+ # ]
30
+ # },
31
+ # {
32
+ # name: 'map',
33
+ # type: 'b/collection/arrays_to_objects',
34
+ # mappings: [
35
+ # { index: 0, key: 'id' },
36
+ # { index: 1, key: 'name' }
37
+ # ]
38
+ # },
39
+ # {
40
+ # name: 'output',
41
+ # type: 'b/echo',
42
+ # message: 'value is currently: {__value}'
43
+ # },
44
+ #
45
+ # ],
46
+ # steps: %w[set map output]
47
+ # }
48
+ #
49
+ # Burner::Pipeline.make(config).execute
50
+ class ArraysToObjects < JobWithRegister
51
+ attr_reader :mappings
52
+
53
+ def initialize(name:, mappings: [], register: '')
54
+ super(name: name, register: register)
55
+
56
+ @mappings = Modeling::KeyIndexMapping.array(mappings)
57
+
58
+ freeze
59
+ end
60
+
61
+ def perform(_output, payload)
62
+ payload[register] = array(payload[register]).map { |array| index_to_key_map(array) }
63
+ end
64
+
65
+ private
66
+
67
+ def index_to_key_map(array)
68
+ mappings.each_with_object({}) do |mapping, memo|
69
+ memo[mapping.key] = array[mapping.index]
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -8,17 +8,18 @@
8
8
  #
9
9
 
10
10
  module Burner
11
- class Jobs
11
+ module Library
12
12
  module Collection
13
13
  # Take an array of (denormalized) objects and create an object hierarchy from them.
14
14
  # Under the hood it uses Hashematics: https://github.com/bluemarblepayroll/hashematics.
15
+ #
15
16
  # Expected Payload#value input: array of objects.
16
17
  # Payload#value output: An array of objects.
17
- class Graph < Job
18
+ class Graph < JobWithRegister
18
19
  attr_reader :key, :groups
19
20
 
20
- def initialize(name:, key:, config: Hashematics::Configuration.new)
21
- super(name: name)
21
+ def initialize(name:, key:, config: Hashematics::Configuration.new, register: '')
22
+ super(name: name, register: register)
22
23
 
23
24
  raise ArgumentError, 'key is required' if key.to_s.empty?
24
25
 
@@ -29,13 +30,11 @@ module Burner
29
30
  end
30
31
 
31
32
  def perform(output, payload)
32
- graph = Hashematics::Graph.new(groups).add(payload.value || [])
33
+ graph = Hashematics::Graph.new(groups).add(array(payload[register]))
33
34
 
34
35
  output.detail("Graphing: #{key}")
35
36
 
36
- payload.value = graph.data(key)
37
-
38
- nil
37
+ payload[register] = graph.data(key)
39
38
  end
40
39
  end
41
40
  end
@@ -0,0 +1,88 @@
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 Library
12
+ module Collection
13
+ # Convert an array of objects to an array of arrays. You can leverage the separator
14
+ # option to support key paths and nested objects. Pass in an array of
15
+ # Burner::Modeling::KeyIndexMapping instances or hashable configurations which specifies
16
+ # the key-to-index mappings to use.
17
+ #
18
+ # Expected Payload#value input: array of hashes.
19
+ # Payload#value output: An array of arrays.
20
+ #
21
+ # An example using a configuration-first pipeline:
22
+ #
23
+ # config = {
24
+ # jobs: [
25
+ # {
26
+ # name: 'set',
27
+ # type: 'b/set_value',
28
+ # value: [
29
+ # [1, 'funky']
30
+ # ]
31
+ # },
32
+ # {
33
+ # name: 'map',
34
+ # type: 'b/collection/objects_to_arrays',
35
+ # mappings: [
36
+ # { index: 0, key: 'id' },
37
+ # { index: 1, key: 'name' }
38
+ # ]
39
+ # },
40
+ # {
41
+ # name: 'output',
42
+ # type: 'b/echo',
43
+ # message: 'value is currently: {__value}'
44
+ # },
45
+ #
46
+ # ],
47
+ # steps: %w[set map output]
48
+ # }
49
+ #
50
+ # Burner::Pipeline.make(config).execute
51
+ class ObjectsToArrays < JobWithRegister
52
+ attr_reader :mappings
53
+
54
+ # If you wish to support nested objects you can pass in a string to use as a
55
+ # key path separator. For example: if you would like to recognize dot-notation for
56
+ # nested hashes then set separator to '.'. For more information, see the underlying
57
+ # library that supports this dot-notation concept:
58
+ # https://github.com/bluemarblepayroll/objectable
59
+ def initialize(name:, mappings: [], register: '', separator: '')
60
+ super(name: name, register: register)
61
+
62
+ @mappings = Modeling::KeyIndexMapping.array(mappings)
63
+ @resolver = Objectable.resolver(separator: separator.to_s)
64
+
65
+ freeze
66
+ end
67
+
68
+ def perform(_output, payload)
69
+ payload[register] = array(payload[register]).map { |object| key_to_index_map(object) }
70
+ end
71
+
72
+ private
73
+
74
+ attr_reader :resolver
75
+
76
+ def key_to_index_map(object)
77
+ mappings.each_with_object(prototype_array) do |mapping, memo|
78
+ memo[mapping.index] = resolver.get(object, mapping.key)
79
+ end
80
+ end
81
+
82
+ def prototype_array
83
+ Array.new(mappings.length)
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -8,21 +8,23 @@
8
8
  #
9
9
 
10
10
  module Burner
11
- class Jobs
11
+ module Library
12
12
  module Collection
13
13
  # Take an array and remove the first N elements, where N is specified by the amount
14
- # attribute.
14
+ # attribute. The initial use case for this was to remove "header" rows from arrays,
15
+ # like you would expect when parsing CSV files.
16
+ #
15
17
  # Expected Payload#value input: nothing.
16
18
  # Payload#value output: An array with N beginning elements removed.
17
- class Shift < Job
19
+ class Shift < JobWithRegister
18
20
  DEFAULT_AMOUNT = 0
19
21
 
20
22
  private_constant :DEFAULT_AMOUNT
21
23
 
22
24
  attr_reader :amount
23
25
 
24
- def initialize(name:, amount: DEFAULT_AMOUNT)
25
- super(name: name)
26
+ def initialize(name:, amount: DEFAULT_AMOUNT, register: '')
27
+ super(name: name, register: register)
26
28
 
27
29
  @amount = amount.to_i
28
30
 
@@ -32,10 +34,7 @@ module Burner
32
34
  def perform(output, payload)
33
35
  output.detail("Shifting #{amount} entries.")
34
36
 
35
- payload.value ||= []
36
- payload.value.shift(amount)
37
-
38
- nil
37
+ payload[register] = array(payload[register]).slice(amount..-1)
39
38
  end
40
39
  end
41
40
  end
@@ -7,44 +7,46 @@
7
7
  # LICENSE file in the root directory of this source tree.
8
8
  #
9
9
 
10
- require_relative 'transform/attribute'
11
- require_relative 'transform/attribute_renderer'
12
-
13
10
  module Burner
14
- class Jobs
11
+ module Library
15
12
  module Collection
16
- # Iterate over all objects and return a new set of transformed objects. Under the hood
17
- # this uses the Realize library: https://github.com/bluemarblepayroll/realize
13
+ # Iterate over all objects and return a new set of transformed objects. The object is
14
+ # transformed per the "transformers" attribute for its attributes. An attribute defines
15
+ # the ultimate key to place the value in and then the transformer pipeline to use to
16
+ # derive the value. Under the hood this uses the Realize library:
17
+ # https://github.com/bluemarblepayroll/realize
18
+ # For more information on the specific contract for attributes, see the
19
+ # Burner::Modeling::Attribute class.
20
+ #
18
21
  # Expected Payload#value input: array of objects.
19
22
  # Payload#value output: An array of objects.
20
- class Transform < Job
23
+ class Transform < JobWithRegister
21
24
  BLANK = ''
22
25
 
23
26
  attr_reader :attribute_renderers,
24
27
  :exclusive,
25
28
  :resolver
26
29
 
27
- def initialize(name:, attributes: [], exclusive: false, separator: BLANK)
28
- super(name: name)
30
+ def initialize(name:, attributes: [], exclusive: false, register: '', separator: BLANK)
31
+ super(name: name, register: register)
29
32
 
30
33
  @resolver = Objectable.resolver(separator: separator)
31
34
  @exclusive = exclusive || false
32
35
 
33
- @attribute_renderers = Attribute.array(attributes)
34
- .map { |a| AttributeRenderer.new(a, resolver) }
36
+ @attribute_renderers =
37
+ Modeling::Attribute.array(attributes)
38
+ .map { |a| Modeling::AttributeRenderer.new(a, resolver) }
35
39
 
36
40
  freeze
37
41
  end
38
42
 
39
43
  def perform(output, payload)
40
- payload.value = (payload.value || []).map { |row| transform(row, payload.time) }
44
+ payload[register] = array(payload[register]).map { |row| transform(row, payload.time) }
41
45
 
42
46
  attr_count = attribute_renderers.length
43
- row_count = payload.value.length
47
+ row_count = payload[register].length
44
48
 
45
49
  output.detail("Transformed #{attr_count} attributes(s) for #{row_count} row(s)")
46
-
47
- nil
48
50
  end
49
51
 
50
52
  private
@@ -8,18 +8,19 @@
8
8
  #
9
9
 
10
10
  module Burner
11
- class Jobs
11
+ module Library
12
12
  module Collection
13
13
  # Take an array of objects and un-pivot groups of keys into rows.
14
14
  # Under the hood it uses HashMath's Unpivot class:
15
15
  # https://github.com/bluemarblepayroll/hash_math
16
+ #
16
17
  # Expected Payload#value input: array of objects.
17
18
  # Payload#value output: An array of objects.
18
- class Unpivot < Job
19
+ class Unpivot < JobWithRegister
19
20
  attr_reader :unpivot
20
21
 
21
- def initialize(name:, pivot_set: HashMath::Unpivot::PivotSet.new)
22
- super(name: name)
22
+ def initialize(name:, pivot_set: HashMath::Unpivot::PivotSet.new, register: '')
23
+ super(name: name, register: register)
23
24
 
24
25
  @unpivot = HashMath::Unpivot.new(pivot_set)
25
26
 
@@ -27,17 +28,24 @@ module Burner
27
28
  end
28
29
 
29
30
  def perform(output, payload)
30
- pivot_count = unpivot.pivot_set.pivots.length
31
- key_count = unpivot.pivot_set.pivots.map { |p| p.keys.length }.sum
32
- object_count = payload.value&.length || 0
31
+ payload[register] = array(payload[register])
32
+ object_count = payload[register].length || 0
33
33
 
34
34
  message = "#{pivot_count} Pivots, Key(s): #{key_count} key(s), #{object_count} objects(s)"
35
35
 
36
36
  output.detail(message)
37
37
 
38
- payload.value = (payload.value || []).flat_map { |object| unpivot.expand(object) }
38
+ payload[register] = payload[register].flat_map { |object| unpivot.expand(object) }
39
+ end
40
+
41
+ private
42
+
43
+ def pivot_count
44
+ unpivot.pivot_set.pivots.length
45
+ end
39
46
 
40
- nil
47
+ def key_count
48
+ unpivot.pivot_set.pivots.map { |p| p.keys.length }.sum
41
49
  end
42
50
  end
43
51
  end
@@ -0,0 +1,89 @@
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 Library
12
+ module Collection
13
+ # Process each object in an array and see if its attribute values match a given set
14
+ # of validations. The main register will include the valid objects and the invalid_register
15
+ # will contain the invalid objects.
16
+ #
17
+ # Expected Payload#value input: array of objects.
18
+ # Payload#value output: An array of objects.
19
+ class Validate < JobWithRegister
20
+ DEFAULT_INVALID_REGISTER = 'invalid'
21
+ DEFAULT_JOIN_CHAR = ', '
22
+ DEFAULT_MESSAGE_KEY = 'errors'
23
+
24
+ attr_reader :invalid_register,
25
+ :join_char,
26
+ :message_key,
27
+ :resolver,
28
+ :validations
29
+
30
+ def initialize(
31
+ name:,
32
+ invalid_register: DEFAULT_INVALID_REGISTER,
33
+ join_char: DEFAULT_JOIN_CHAR,
34
+ message_key: DEFAULT_MESSAGE_KEY,
35
+ register: '',
36
+ separator: '',
37
+ validations: []
38
+ )
39
+ super(name: name, register: register)
40
+
41
+ @invalid_register = invalid_register.to_s
42
+ @join_char = join_char.to_s
43
+ @message_key = message_key.to_s
44
+ @resolver = Objectable.resolver(separator: separator)
45
+ @validations = Modeling::Validations.array(validations)
46
+
47
+ freeze
48
+ end
49
+
50
+ def perform(output, payload)
51
+ valid = []
52
+ invalid = []
53
+
54
+ (payload[register] || []).each do |object|
55
+ errors = validate(object)
56
+
57
+ if errors.empty?
58
+ valid << object
59
+ else
60
+ invalid << make_in_error(object, errors)
61
+ end
62
+ end
63
+
64
+ output.detail("Valid count: #{valid.length}")
65
+ output.detail("Invalid count: #{invalid.length}")
66
+
67
+ payload[register] = valid
68
+ payload[invalid_register] = invalid
69
+
70
+ nil
71
+ end
72
+
73
+ private
74
+
75
+ def validate(object)
76
+ validations.each_with_object([]) do |validation, memo|
77
+ next if validation.valid?(object, resolver)
78
+
79
+ memo << validation.message
80
+ end
81
+ end
82
+
83
+ def make_in_error(object, errors)
84
+ resolver.set(object, message_key, errors.join(join_char))
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end