burner 1.2.0 → 1.5.0.pre.alpha

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a2c3e9af2bbfbf80cd5c5884e05b1282f52871566103baf4ca1b599fc76af8ca
4
- data.tar.gz: e7c2eb8a00086e1937a4d9e39383d38a60c5ebaa5efa1d67da4e6af9741261cd
3
+ metadata.gz: 5cbb761b5c317ea13a380ac7c96fe48846a5194d628c425fa0305c59738ce74e
4
+ data.tar.gz: d8d178a3c4ae9ea142610a4f341608c29e28cc8e9ad76edd10031e0983558178
5
5
  SHA512:
6
- metadata.gz: e885c7bf710f613323bbc3fc49162815632d7785aa6e753f3cd53bb04d2b53e74f92be549f75b6997d4dedce5c2dabe01fd032796a6d6e2fadfd6e84c633b168
7
- data.tar.gz: e09d491b3fb7ef1b79932ac6094adedd74f806170a2a11aa1ff1eda838b6bf76cec1684dbfaba12d07a6e071062d432bcd5f3939f275830fc13fb71cabb39f8c
6
+ metadata.gz: bc546faf0d6feb8828287b5c9f8893d3f21e29edb2ec638a2839e67bc05f9b5de8bbfb3ad9a3459bb4557fe052ee874f917aefbf3ad5b1829fcc499f1a4f9979
7
+ data.tar.gz: 778df0850ef5ef87a843409694577f8a75de9a541d6d32830780eec9add582bf95af932c01ec8fe17f3d29ba87d410fce8d0ad498049ca217cff7d5cb278a623
@@ -1,3 +1,24 @@
1
+
2
+ # 1.5.0 (TBD)
3
+
4
+ Added Jobs:
5
+
6
+ * b/collection/zip
7
+ # 1.4.0 (December 17th, 2020)
8
+
9
+ Additions:
10
+
11
+ * byte_order_mark option for b/serialize/csv job
12
+
13
+ Added Jobs:
14
+
15
+ * b/compress/row_reader
16
+ * b/io/row_reader
17
+ # 1.3.0 (December 11th, 2020)
18
+
19
+ Additions:
20
+
21
+ * Decoupled storage: `Burner::Disks` factory, `Burner::Disks::Local` reference implementation, and `b/io/*` `disk` option for configuring IO jobs to use custom disks.
1
22
  # 1.2.0 (November 25th, 2020)
2
23
 
3
24
  #### Enhancements:
data/README.md CHANGED
@@ -42,7 +42,7 @@ pipeline = {
42
42
  {
43
43
  name: :output_value,
44
44
  type: 'b/echo',
45
- message: 'The current value is: {__value}'
45
+ message: 'The current value is: {__default_register}'
46
46
  },
47
47
  {
48
48
  name: :parse,
@@ -89,7 +89,7 @@ Some notes:
89
89
 
90
90
  * Some values are able to be string-interpolated using the provided Payload#params. This allows for the passing runtime configuration/data into pipelines/jobs.
91
91
  * The job's ID can be accessed using the `__id` key.
92
- * The current job's payload value can be accessed using the `__value` key.
92
+ * The current payload registers' values can be accessed using the `__<register_name>_register` key.
93
93
  * Jobs can be re-used (just like the output_id and output_value jobs).
94
94
  * If steps is nil then all jobs will execute in their declared order.
95
95
 
@@ -163,7 +163,7 @@ jobs:
163
163
 
164
164
  - name: output_value
165
165
  type: b/echo
166
- message: 'The current value is: {__value}'
166
+ message: 'The current value is: {__default_register}'
167
167
 
168
168
  - name: parse
169
169
  type: b/deserialize/json
@@ -227,6 +227,11 @@ This library only ships with very basic, rudimentary jobs that are meant to just
227
227
  * **b/collection/unpivot** [pivot_set, register]: Take an array of objects and unpivot specific sets of keys into rows. Under the hood it uses [HashMath's Unpivot class](https://github.com/bluemarblepayroll/hash_math#unpivot-hash-key-coalescence-and-row-extrapolation).
228
228
  * **b/collection/validate** [invalid_register, join_char, message_key, register, separator, validations]: Take an array of objects, run it through each declared validator, and split the objects into two registers. The valid objects will be split into the current register while the invalid ones will go into the invalid_register as declared. Optional arguments, join_char and message_key, help determine the compiled error messages. The separator option can be utilized to use dot-notation for validating keys. See each validation's options by viewing their classes within the `lib/modeling/validations` directory.
229
229
  * **b/collection/values** [include_keys, register]: Take an array of objects and call `#values` on each object. If include_keys is true (it is false by default), then call `#keys` on the first object and inject that as a "header" object.
230
+ * **b/collection/zip** [base_register, register, with_register]: Combines `base_register` and `with_register`s' data to form one single array in `register`. It will combine each element, positionally in each array to form the final array. For example: ['hello', 'bugs'] + ['world', 'bunny'] => [['hello', 'world'], ['bugs', 'bunny']]
231
+
232
+ #### Compression
233
+
234
+ * **b/compress/row_reader** [data_key, ignore_blank_path, ignore_blank_data, path_key, register, separator]: Iterates over an array of objects, extracts a path and data in each object, and creates a zip file.
230
235
 
231
236
  #### De-serialization
232
237
 
@@ -236,13 +241,16 @@ This library only ships with very basic, rudimentary jobs that are meant to just
236
241
 
237
242
  #### IO
238
243
 
239
- * **b/io/exist** [path, short_circuit]: Check to see if a file exists. The path parameter can be interpolated using `Payload#params`. If short_circuit was set to true (defaults to false) and the file does not exist then the pipeline will be short-circuited.
240
- * **b/io/read** [binary, path, register]: Read in a local file. The path parameter can be interpolated using `Payload#params`. If the contents are binary, pass in `binary: true` to open it up in binary+read mode.
241
- * **b/io/write** [binary, path, register]: Write to a local file. The path parameter can be interpolated using `Payload#params`. If the contents are binary, pass in `binary: true` to open it up in binary+write mode.
244
+ By default all jobs will use the `Burner::Disks::Local` disk for its persistence. But this is configurable by implementing and registering custom disk-based classes in the `Burner::Disks` factory. For example: a consumer application may also want to interact with cloud-based storage providers and could leverage this as its job library instead of implementing custom jobs.
245
+
246
+ * **b/io/exist** [disk, path, short_circuit]: Check to see if a file exists. The path parameter can be interpolated using `Payload#params`. If short_circuit was set to true (defaults to false) and the file does not exist then the pipeline will be short-circuited.
247
+ * **b/io/read** [binary, disk, path, register]: Read in a local file. The path parameter can be interpolated using `Payload#params`. If the contents are binary, pass in `binary: true` to open it up in binary+read mode.
248
+ * **b/io/row_reader** [data_key, disk, ignore_blank_path, ignore_file_not_found, path_key, register, separator]: Iterates over an array of objects, extracts a filepath from a key in each object, and attempts to load the file's content for each record. The file's content will be stored at the specified data_key. By default missing paths or files will be treated as hard errors. If you wish to ignore these then pass in true for ignore_blank_path and/or ignore_file_not_found.
249
+ * **b/io/write** [binary, disk, path, register]: Write to a local file. The path parameter can be interpolated using `Payload#params`. If the contents are binary, pass in `binary: true` to open it up in binary+write mode.
242
250
 
243
251
  #### Serialization
244
252
 
245
- * **b/serialize/csv** [register]: Take an array of arrays and create a CSV.
253
+ * **b/serialize/csv** [byte_order_mark, register]: Take an array of arrays and create a CSV. You can optionally pre-pend a byte order mark, see Burner::Modeling::ByteOrderMark for acceptable options.
246
254
  * **b/serialize/json** [register]: Convert value to JSON.
247
255
  * **b/serialize/yaml** [register]: Convert value to YAML.
248
256
 
@@ -299,7 +307,7 @@ pipeline = {
299
307
  {
300
308
  name: :output_value,
301
309
  type: 'b/echo',
302
- message: 'The current value is: {__value}'
310
+ message: 'The current value is: {__default_register}'
303
311
  },
304
312
  {
305
313
  name: :parse,
@@ -33,6 +33,7 @@ Gem::Specification.new do |s|
33
33
  s.add_dependency('hash_math', '~>1.2')
34
34
  s.add_dependency('objectable', '~>1.0')
35
35
  s.add_dependency('realize', '~>1.3')
36
+ s.add_dependency('rubyzip', '~>1.2')
36
37
  s.add_dependency('stringento', '~>2.1')
37
38
 
38
39
  s.add_development_dependency('guard-rspec', '~>4.7')
@@ -10,6 +10,7 @@
10
10
  require 'acts_as_hashable'
11
11
  require 'benchmark'
12
12
  require 'csv'
13
+ require 'fileutils'
13
14
  require 'forwardable'
14
15
  require 'hash_math'
15
16
  require 'hashematics'
@@ -21,8 +22,10 @@ require 'singleton'
21
22
  require 'stringento'
22
23
  require 'time'
23
24
  require 'yaml'
25
+ require 'zip'
24
26
 
25
27
  # Common/Shared
28
+ require_relative 'burner/disks'
26
29
  require_relative 'burner/modeling'
27
30
  require_relative 'burner/side_effects'
28
31
  require_relative 'burner/util'
@@ -0,0 +1,26 @@
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 'disks/local'
11
+
12
+ module Burner
13
+ # A factory to register and emit instances that conform to the Disk interface with requests
14
+ # the instance responds to: #exist?, #read, and #write. See an example implementation within
15
+ # the lib/burner/disks directory.
16
+ #
17
+ # The benefit to this pluggable disk model is a consumer application can decide which file
18
+ # backend to use and how to store files. For example: an application may choose to use
19
+ # some cloud provider with their own file store implementation. This can be wrapped up
20
+ # in a Disk class and registered here and then referenced in the Pipeline's IO jobs.
21
+ class Disks
22
+ acts_as_hashable_factory
23
+
24
+ register 'local', '', Disks::Local
25
+ end
26
+ end
@@ -0,0 +1,61 @@
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 Disks
12
+ # Operations against the local file system.
13
+ class Local
14
+ acts_as_hashable
15
+
16
+ # Check to see if the passed in path exists within the local file system.
17
+ # It will not make assumptions on what the 'file' is, only that it is recognized
18
+ # by Ruby's File class.
19
+ def exist?(path)
20
+ File.exist?(path)
21
+ end
22
+
23
+ # Open and read the contents of a local file. If binary is passed in as true then the file
24
+ # will be opened in binary mode.
25
+ def read(path, binary: false)
26
+ File.open(path, read_mode(binary), &:read)
27
+ end
28
+
29
+ # Open and write the specified data to a local file. If binary is passed in as true then
30
+ # the file will be opened in binary mode. It is important to note that if the file's
31
+ # directory structure will be automatically created if it does not exist.
32
+ def write(path, data, binary: false)
33
+ ensure_directory_exists(path)
34
+
35
+ File.open(path, write_mode(binary)) { |io| io.write(data) }
36
+
37
+ path
38
+ end
39
+
40
+ private
41
+
42
+ def ensure_directory_exists(path)
43
+ dirname = File.dirname(path)
44
+
45
+ return if File.exist?(dirname)
46
+
47
+ FileUtils.mkdir_p(dirname)
48
+
49
+ nil
50
+ end
51
+
52
+ def write_mode(binary)
53
+ binary ? 'wb' : 'w'
54
+ end
55
+
56
+ def read_mode(binary)
57
+ binary ? 'rb' : 'r'
58
+ end
59
+ end
60
+ end
61
+ end
@@ -34,6 +34,9 @@ module Burner
34
34
  register 'b/collection/unpivot', Library::Collection::Unpivot
35
35
  register 'b/collection/values', Library::Collection::Values
36
36
  register 'b/collection/validate', Library::Collection::Validate
37
+ register 'b/collection/zip', Library::Collection::Zip
38
+
39
+ register 'b/compress/row_reader', Library::Compress::RowReader
37
40
 
38
41
  register 'b/deserialize/csv', Library::Deserialize::Csv
39
42
  register 'b/deserialize/json', Library::Deserialize::Json
@@ -41,6 +44,7 @@ module Burner
41
44
 
42
45
  register 'b/io/exist', Library::IO::Exist
43
46
  register 'b/io/read', Library::IO::Read
47
+ register 'b/io/row_reader', Library::IO::RowReader
44
48
  register 'b/io/write', Library::IO::Write
45
49
 
46
50
  register 'b/serialize/csv', Library::Serialize::Csv
@@ -25,6 +25,9 @@ require_relative 'library/collection/transform'
25
25
  require_relative 'library/collection/unpivot'
26
26
  require_relative 'library/collection/validate'
27
27
  require_relative 'library/collection/values'
28
+ require_relative 'library/collection/zip'
29
+
30
+ require_relative 'library/compress/row_reader'
28
31
 
29
32
  require_relative 'library/deserialize/csv'
30
33
  require_relative 'library/deserialize/json'
@@ -32,6 +35,7 @@ require_relative 'library/deserialize/yaml'
32
35
 
33
36
  require_relative 'library/io/exist'
34
37
  require_relative 'library/io/read'
38
+ require_relative 'library/io/row_reader'
35
39
  require_relative 'library/io/write'
36
40
 
37
41
  require_relative 'library/serialize/csv'
@@ -0,0 +1,53 @@
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
+ # This job can take two arrays and coalesces them by index. For example:
14
+ #
15
+ # input:
16
+ # base_register: [ 'hello', 'bugs' ]
17
+ # with_register: [ 'world', 'bunny' ]
18
+ # output:
19
+ # register: [ ['hello', 'world'], ['bugs', 'bunny'] ]
20
+ #
21
+ # Expected Payload[base_register] input: array of objects.
22
+ # Expected Payload[with_register] input: array of objects.
23
+ # Payload[register] output: An array of two-dimensional arrays.
24
+ class Zip < JobWithRegister
25
+ attr_reader :base_register, :with_register
26
+
27
+ def initialize(
28
+ name:,
29
+ with_register:,
30
+ base_register: DEFAULT_REGISTER,
31
+ register: DEFAULT_REGISTER
32
+ )
33
+ super(name: name, register: register)
34
+
35
+ @base_register = base_register.to_s
36
+ @with_register = with_register.to_s
37
+
38
+ freeze
39
+ end
40
+
41
+ def perform(output, payload)
42
+ base_data = array(payload[base_register])
43
+ with_data = array(payload[with_register])
44
+
45
+ output.detail("Combining register: #{base_register} (#{base_data.length} record(s))")
46
+ output.detail("With register: #{with_register} (#{with_data.length} record(s))")
47
+
48
+ payload[register] = base_data.zip(with_data)
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,102 @@
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 Compress
13
+ # Iterates over an array of objects, extracts a path and data in each object, and
14
+ # creates a zip file. By default, if a path is blank then an ArgumentError will be raised.
15
+ # If this is undesirable then you can set ignore_blank_path to true and the record will be
16
+ # skipped. You also have the option to supress blank files being added by configuring
17
+ # ignore_blank_data as true.
18
+ #
19
+ # Expected Payload[register] input: array of objects.
20
+ # Payload[register] output: compressed binary zip file contents.
21
+ class RowReader < JobWithRegister
22
+ Content = Struct.new(:path, :data)
23
+
24
+ private_constant :Content
25
+
26
+ DEFAULT_DATA_KEY = 'data'
27
+ DEFAULT_PATH_KEY = 'path'
28
+
29
+ attr_reader :data_key,
30
+ :ignore_blank_data,
31
+ :ignore_blank_path,
32
+ :path_key,
33
+ :resolver
34
+
35
+ def initialize(
36
+ name:,
37
+ data_key: DEFAULT_DATA_KEY,
38
+ ignore_blank_data: false,
39
+ ignore_blank_path: false,
40
+ path_key: DEFAULT_PATH_KEY,
41
+ register: DEFAULT_REGISTER,
42
+ separator: ''
43
+ )
44
+ super(name: name, register: register)
45
+
46
+ @data_key = data_key.to_s
47
+ @ignore_blank_data = ignore_blank_data || false
48
+ @ignore_blank_path = ignore_blank_path || false
49
+ @path_key = path_key.to_s
50
+ @resolver = Objectable.resolver(separator: separator)
51
+
52
+ freeze
53
+ end
54
+
55
+ def perform(output, payload)
56
+ payload[register] = Zip::OutputStream.write_buffer do |zip|
57
+ array(payload[register]).each.with_index(1) do |record, index|
58
+ content = extract_path_and_data(record, index, output)
59
+
60
+ next unless content
61
+
62
+ zip.put_next_entry(content.path)
63
+ zip.write(content.data)
64
+ end
65
+ end.string
66
+ end
67
+
68
+ private
69
+
70
+ def extract_path_and_data(record, index, output)
71
+ path = strip_leading_separator(resolver.get(record, path_key))
72
+ data = resolver.get(record, data_key)
73
+
74
+ return if assert_and_skip_missing_path?(path, index, output)
75
+ return if skip_missing_data?(data, index, output)
76
+
77
+ Content.new(path, data)
78
+ end
79
+
80
+ def strip_leading_separator(path)
81
+ path.to_s.start_with?(File::SEPARATOR) ? path.to_s[1..-1] : path.to_s
82
+ end
83
+
84
+ def assert_and_skip_missing_path?(path, index, output)
85
+ if ignore_blank_path && path.to_s.empty?
86
+ output.detail("Skipping record #{index} because of blank path")
87
+ true
88
+ elsif path.to_s.empty?
89
+ raise ArgumentError, "Record #{index} is missing a path at key: #{path_key}"
90
+ end
91
+ end
92
+
93
+ def skip_missing_data?(data, index, output)
94
+ return false unless ignore_blank_data && data.to_s.empty?
95
+
96
+ output.detail("Skipping record #{index} because of blank data")
97
+ true
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -7,8 +7,6 @@
7
7
  # LICENSE file in the root directory of this source tree.
8
8
  #
9
9
 
10
- require_relative 'base'
11
-
12
10
  module Burner
13
11
  module Library
14
12
  module IO
@@ -17,13 +15,14 @@ module Burner
17
15
  #
18
16
  # Note: this does not use Payload#registers.
19
17
  class Exist < Job
20
- attr_reader :path, :short_circuit
18
+ attr_reader :disk, :path, :short_circuit
21
19
 
22
- def initialize(name:, path:, short_circuit: false)
20
+ def initialize(name:, path:, disk: {}, short_circuit: false)
23
21
  super(name: name)
24
22
 
25
23
  raise ArgumentError, 'path is required' if path.to_s.empty?
26
24
 
25
+ @disk = Disks.make(disk)
27
26
  @path = path.to_s
28
27
  @short_circuit = short_circuit || false
29
28
  end
@@ -31,7 +30,7 @@ module Burner
31
30
  def perform(output, payload)
32
31
  compiled_path = job_string_template(path, output, payload)
33
32
 
34
- exists = File.exist?(compiled_path)
33
+ exists = disk.exist?(compiled_path)
35
34
  verb = exists ? 'does' : 'does not'
36
35
 
37
36
  output.detail("The path: #{compiled_path} #{verb} exist")
@@ -10,16 +10,20 @@
10
10
  module Burner
11
11
  module Library
12
12
  module IO
13
- # Common configuration/code for all IO Job subclasses.
14
- class Base < JobWithRegister
15
- attr_reader :path
13
+ # Common configuration/code for all IO Job subclasses that open a file.
14
+ class OpenFileBase < JobWithRegister
15
+ attr_reader :binary, :disk, :path
16
16
 
17
- def initialize(name:, path:, register: DEFAULT_REGISTER)
17
+ def initialize(name:, path:, binary: false, disk: {}, register: DEFAULT_REGISTER)
18
18
  super(name: name, register: register)
19
19
 
20
20
  raise ArgumentError, 'path is required' if path.to_s.empty?
21
21
 
22
- @path = path.to_s
22
+ @binary = binary || false
23
+ @disk = Disks.make(disk)
24
+ @path = path.to_s
25
+
26
+ freeze
23
27
  end
24
28
  end
25
29
  end
@@ -7,7 +7,7 @@
7
7
  # LICENSE file in the root directory of this source tree.
8
8
  #
9
9
 
10
- require_relative 'base'
10
+ require_relative 'open_file_base'
11
11
 
12
12
  module Burner
13
13
  module Library
@@ -16,29 +16,13 @@ module Burner
16
16
  #
17
17
  # Expected Payload[register] input: nothing.
18
18
  # Payload[register] output: contents of the specified file.
19
- class Read < Base
20
- attr_reader :binary
21
-
22
- def initialize(name:, path:, binary: false, register: DEFAULT_REGISTER)
23
- super(name: name, path: path, register: register)
24
-
25
- @binary = binary || false
26
-
27
- freeze
28
- end
29
-
19
+ class Read < OpenFileBase
30
20
  def perform(output, payload)
31
21
  compiled_path = job_string_template(path, output, payload)
32
22
 
33
23
  output.detail("Reading: #{compiled_path}")
34
24
 
35
- payload[register] = File.open(compiled_path, mode, &:read)
36
- end
37
-
38
- private
39
-
40
- def mode
41
- binary ? 'rb' : 'r'
25
+ payload[register] = disk.read(compiled_path, binary: binary)
42
26
  end
43
27
  end
44
28
  end
@@ -0,0 +1,119 @@
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 'open_file_base'
11
+
12
+ module Burner
13
+ module Library
14
+ module IO
15
+ # Iterates over an array of objects, extracts a filepath from a key in each object,
16
+ # and attempts to load the file's content for each record. The file's content will be
17
+ # stored at the specified data_key. By default missing paths or files will be
18
+ # treated as hard errors. If you wish to ignore these then pass in true for
19
+ # ignore_blank_path and/or ignore_file_not_found.
20
+ #
21
+ # Expected Payload[register] input: array of objects.
22
+ # Payload[register] output: array of objects.
23
+ class RowReader < JobWithRegister
24
+ class FileNotFoundError < StandardError; end
25
+
26
+ DEFAULT_DATA_KEY = 'data'
27
+ DEFAULT_PATH_KEY = 'path'
28
+
29
+ attr_reader :binary,
30
+ :data_key,
31
+ :disk,
32
+ :ignore_blank_path,
33
+ :ignore_file_not_found,
34
+ :path_key,
35
+ :resolver
36
+
37
+ def initialize(
38
+ name:,
39
+ binary: false,
40
+ data_key: DEFAULT_DATA_KEY,
41
+ disk: {},
42
+ ignore_blank_path: false,
43
+ ignore_file_not_found: false,
44
+ path_key: DEFAULT_PATH_KEY,
45
+ register: DEFAULT_REGISTER,
46
+ separator: ''
47
+ )
48
+ super(name: name, register: register)
49
+
50
+ @binary = binary || false
51
+ @data_key = data_key.to_s
52
+ @disk = Disks.make(disk)
53
+ @ignore_blank_path = ignore_blank_path || false
54
+ @ignore_file_not_found = ignore_file_not_found || false
55
+ @path_key = path_key.to_s
56
+ @resolver = Objectable.resolver(separator: separator)
57
+
58
+ freeze
59
+ end
60
+
61
+ def perform(output, payload)
62
+ records = array(payload[register])
63
+
64
+ output.detail("Reading path_key: #{path_key} for #{payload[register].length} records(s)")
65
+ output.detail("Storing read data in: #{path_key}")
66
+
67
+ payload[register] = records.map.with_index(1) do |object, index|
68
+ load_data(object, index, output)
69
+ end
70
+ end
71
+
72
+ private
73
+
74
+ def assert_and_skip_missing_path?(path, index, output)
75
+ missing_path = path.to_s.empty?
76
+ blank_path_raises_error = !ignore_blank_path
77
+
78
+ if missing_path && blank_path_raises_error
79
+ output.detail("Record #{index} is missing a path, raising error")
80
+
81
+ raise ArgumentError, "Record #{index} is missing a path"
82
+ elsif missing_path
83
+ output.detail("Record #{index} is missing a path")
84
+
85
+ true
86
+ end
87
+ end
88
+
89
+ def assert_and_skip_file_not_found?(path, index, output)
90
+ does_not_exist = !disk.exist?(path)
91
+ file_not_found_raises_error = !ignore_file_not_found
92
+
93
+ if file_not_found_raises_error && does_not_exist
94
+ output.detail("Record #{index} path: '#{path}' does not exist, raising error")
95
+
96
+ raise FileNotFoundError, "#{path} does not exist"
97
+ elsif does_not_exist
98
+ output.detail("Record #{index} path: '#{path}' does not exist, skipping")
99
+
100
+ true
101
+ end
102
+ end
103
+
104
+ def load_data(object, index, output)
105
+ path = resolver.get(object, path_key)
106
+
107
+ return object if assert_and_skip_missing_path?(path, index, output)
108
+ return object if assert_and_skip_file_not_found?(path, index, output)
109
+
110
+ data = disk.read(path, binary: binary)
111
+
112
+ resolver.set(object, data_key, data)
113
+
114
+ object
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
@@ -7,7 +7,7 @@
7
7
  # LICENSE file in the root directory of this source tree.
8
8
  #
9
9
 
10
- require_relative 'base'
10
+ require_relative 'open_file_base'
11
11
 
12
12
  module Burner
13
13
  module Library
@@ -16,54 +16,27 @@ module Burner
16
16
  #
17
17
  # Expected Payload[register] input: anything.
18
18
  # Payload[register] output: whatever was passed in.
19
- class Write < Base
20
- attr_reader :binary
21
-
22
- def initialize(name:, path:, binary: false, register: DEFAULT_REGISTER)
23
- super(name: name, path: path, register: register)
24
-
25
- @binary = binary || false
26
-
27
- freeze
28
- end
29
-
19
+ class Write < OpenFileBase
30
20
  def perform(output, payload)
31
- compiled_path = job_string_template(path, output, payload)
32
-
33
- ensure_directory_exists(output, compiled_path)
21
+ logical_filename = job_string_template(path, output, payload)
22
+ physical_filename = nil
34
23
 
35
- output.detail("Writing: #{compiled_path}")
24
+ output.detail("Writing: #{logical_filename}")
36
25
 
37
26
  time_in_seconds = Benchmark.measure do
38
- File.open(compiled_path, mode) { |io| io.write(payload[register]) }
27
+ physical_filename = disk.write(logical_filename, payload[register], binary: binary)
39
28
  end.real
40
29
 
30
+ output.detail("Wrote to: #{physical_filename}")
31
+
41
32
  side_effect = SideEffects::WrittenFile.new(
42
- logical_filename: compiled_path,
43
- physical_filename: compiled_path,
33
+ logical_filename: logical_filename,
34
+ physical_filename: physical_filename,
44
35
  time_in_seconds: time_in_seconds
45
36
  )
46
37
 
47
38
  payload.add_side_effect(side_effect)
48
39
  end
49
-
50
- private
51
-
52
- def ensure_directory_exists(output, compiled_path)
53
- dirname = File.dirname(compiled_path)
54
-
55
- return if File.exist?(dirname)
56
-
57
- output.detail("Outer directory does not exist, creating: #{dirname}")
58
-
59
- FileUtils.mkdir_p(dirname)
60
-
61
- nil
62
- end
63
-
64
- def mode
65
- binary ? 'wb' : 'w'
66
- end
67
40
  end
68
41
  end
69
42
  end
@@ -10,17 +10,30 @@
10
10
  module Burner
11
11
  module Library
12
12
  module Serialize
13
- # Take an array of arrays and create a CSV.
13
+ # Take an array of arrays and create a CSV. You can optionally pre-pend a byte order mark,
14
+ # see Burner::Modeling::ByteOrderMark for acceptable options.
14
15
  #
15
16
  # Expected Payload[register] input: array of arrays.
16
17
  # Payload[register] output: a serialized CSV string.
17
18
  class Csv < JobWithRegister
19
+ attr_reader :byte_order_mark
20
+
21
+ def initialize(name:, byte_order_mark: nil, register: DEFAULT_REGISTER)
22
+ super(name: name, register: register)
23
+
24
+ @byte_order_mark = Modeling::ByteOrderMark.resolve(byte_order_mark)
25
+
26
+ freeze
27
+ end
28
+
18
29
  def perform(_output, payload)
19
- payload[register] = CSV.generate(options) do |csv|
30
+ serialized_rows = CSV.generate(options) do |csv|
20
31
  array(payload[register]).each do |row|
21
32
  csv << row
22
33
  end
23
34
  end
35
+
36
+ payload[register] = "#{byte_order_mark}#{serialized_rows}"
24
37
  end
25
38
 
26
39
  private
@@ -9,6 +9,7 @@
9
9
 
10
10
  require_relative 'modeling/attribute'
11
11
  require_relative 'modeling/attribute_renderer'
12
+ require_relative 'modeling/byte_order_mark'
12
13
  require_relative 'modeling/key_index_mapping'
13
14
  require_relative 'modeling/key_mapping'
14
15
  require_relative 'modeling/validations'
@@ -0,0 +1,27 @@
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
+ # Define all acceptable byte order mark values.
13
+ module ByteOrderMark
14
+ UTF_8 = "\xEF\xBB\xBF"
15
+ UTF_16BE = "\xFE\xFF"
16
+ UTF_16LE = "\xFF\xFE"
17
+ UTF_32BE = "\x00\x00\xFE\xFF"
18
+ UTF_32LE = "\xFE\xFF\x00\x00"
19
+
20
+ class << self
21
+ def resolve(value)
22
+ value ? const_get(value.to_s.upcase.to_sym) : nil
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -8,5 +8,5 @@
8
8
  #
9
9
 
10
10
  module Burner
11
- VERSION = '1.2.0'
11
+ VERSION = '1.5.0-alpha'
12
12
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: burner
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0
4
+ version: 1.5.0.pre.alpha
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matthew Ruggio
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-11-25 00:00:00.000000000 Z
11
+ date: 2020-12-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: acts_as_hashable
@@ -80,6 +80,20 @@ dependencies:
80
80
  - - "~>"
81
81
  - !ruby/object:Gem::Version
82
82
  version: '1.3'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rubyzip
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '1.2'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '1.2'
83
97
  - !ruby/object:Gem::Dependency
84
98
  name: stringento
85
99
  requirement: !ruby/object:Gem::Requirement
@@ -220,6 +234,8 @@ files:
220
234
  - exe/burner
221
235
  - lib/burner.rb
222
236
  - lib/burner/cli.rb
237
+ - lib/burner/disks.rb
238
+ - lib/burner/disks/local.rb
223
239
  - lib/burner/job.rb
224
240
  - lib/burner/job_with_register.rb
225
241
  - lib/burner/jobs.rb
@@ -236,13 +252,16 @@ files:
236
252
  - lib/burner/library/collection/unpivot.rb
237
253
  - lib/burner/library/collection/validate.rb
238
254
  - lib/burner/library/collection/values.rb
255
+ - lib/burner/library/collection/zip.rb
256
+ - lib/burner/library/compress/row_reader.rb
239
257
  - lib/burner/library/deserialize/csv.rb
240
258
  - lib/burner/library/deserialize/json.rb
241
259
  - lib/burner/library/deserialize/yaml.rb
242
260
  - lib/burner/library/echo.rb
243
- - lib/burner/library/io/base.rb
244
261
  - lib/burner/library/io/exist.rb
262
+ - lib/burner/library/io/open_file_base.rb
245
263
  - lib/burner/library/io/read.rb
264
+ - lib/burner/library/io/row_reader.rb
246
265
  - lib/burner/library/io/write.rb
247
266
  - lib/burner/library/nothing.rb
248
267
  - lib/burner/library/serialize/csv.rb
@@ -254,6 +273,7 @@ files:
254
273
  - lib/burner/modeling.rb
255
274
  - lib/burner/modeling/attribute.rb
256
275
  - lib/burner/modeling/attribute_renderer.rb
276
+ - lib/burner/modeling/byte_order_mark.rb
257
277
  - lib/burner/modeling/key_index_mapping.rb
258
278
  - lib/burner/modeling/key_mapping.rb
259
279
  - lib/burner/modeling/validations.rb
@@ -290,9 +310,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
290
310
  version: '2.5'
291
311
  required_rubygems_version: !ruby/object:Gem::Requirement
292
312
  requirements:
293
- - - ">="
313
+ - - ">"
294
314
  - !ruby/object:Gem::Version
295
- version: '0'
315
+ version: 1.3.1
296
316
  requirements: []
297
317
  rubygems_version: 3.0.3
298
318
  signing_key: