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 +4 -4
- data/CHANGELOG.md +21 -0
- data/README.md +16 -8
- data/burner.gemspec +1 -0
- data/lib/burner.rb +3 -0
- data/lib/burner/disks.rb +26 -0
- data/lib/burner/disks/local.rb +61 -0
- data/lib/burner/jobs.rb +4 -0
- data/lib/burner/library.rb +4 -0
- data/lib/burner/library/collection/zip.rb +53 -0
- data/lib/burner/library/compress/row_reader.rb +102 -0
- data/lib/burner/library/io/exist.rb +4 -5
- data/lib/burner/library/io/{base.rb → open_file_base.rb} +9 -5
- data/lib/burner/library/io/read.rb +3 -19
- data/lib/burner/library/io/row_reader.rb +119 -0
- data/lib/burner/library/io/write.rb +10 -37
- data/lib/burner/library/serialize/csv.rb +15 -2
- data/lib/burner/modeling.rb +1 -0
- data/lib/burner/modeling/byte_order_mark.rb +27 -0
- data/lib/burner/version.rb +1 -1
- metadata +25 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5cbb761b5c317ea13a380ac7c96fe48846a5194d628c425fa0305c59738ce74e
|
4
|
+
data.tar.gz: d8d178a3c4ae9ea142610a4f341608c29e28cc8e9ad76edd10031e0983558178
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bc546faf0d6feb8828287b5c9f8893d3f21e29edb2ec638a2839e67bc05f9b5de8bbfb3ad9a3459bb4557fe052ee874f917aefbf3ad5b1829fcc499f1a4f9979
|
7
|
+
data.tar.gz: 778df0850ef5ef87a843409694577f8a75de9a541d6d32830780eec9add582bf95af932c01ec8fe17f3d29ba87d410fce8d0ad498049ca217cff7d5cb278a623
|
data/CHANGELOG.md
CHANGED
@@ -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: {
|
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
|
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: {
|
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
|
-
|
240
|
-
|
241
|
-
* **b/io/
|
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: {
|
310
|
+
message: 'The current value is: {__default_register}'
|
303
311
|
},
|
304
312
|
{
|
305
313
|
name: :parse,
|
data/burner.gemspec
CHANGED
@@ -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')
|
data/lib/burner.rb
CHANGED
@@ -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'
|
data/lib/burner/disks.rb
ADDED
@@ -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
|
data/lib/burner/jobs.rb
CHANGED
@@ -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
|
data/lib/burner/library.rb
CHANGED
@@ -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 =
|
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
|
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
|
-
@
|
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 '
|
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 <
|
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] =
|
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 '
|
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 <
|
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
|
-
|
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: #{
|
24
|
+
output.detail("Writing: #{logical_filename}")
|
36
25
|
|
37
26
|
time_in_seconds = Benchmark.measure do
|
38
|
-
|
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:
|
43
|
-
physical_filename:
|
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
|
-
|
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
|
data/lib/burner/modeling.rb
CHANGED
@@ -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
|
data/lib/burner/version.rb
CHANGED
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.
|
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
|
+
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:
|
315
|
+
version: 1.3.1
|
296
316
|
requirements: []
|
297
317
|
rubygems_version: 3.0.3
|
298
318
|
signing_key:
|