portable 1.0.0.pre.alpha → 1.0.0.pre.alpha.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +1 -1
- data/README.md +111 -50
- data/lib/portable.rb +10 -1
- data/lib/portable/data.rb +10 -0
- data/lib/portable/data/provider.rb +51 -0
- data/lib/portable/data/source.rb +38 -0
- data/lib/portable/document.rb +31 -0
- data/lib/portable/modeling/byte_order_mark.rb +27 -0
- data/lib/portable/modeling/column.rb +39 -0
- data/lib/portable/modeling/data_table.rb +28 -0
- data/lib/portable/modeling/options.rb +32 -0
- data/lib/portable/modeling/sheet.rb +48 -0
- data/lib/portable/rendering.rb +10 -0
- data/lib/portable/rendering/row.rb +40 -0
- data/lib/portable/rendering/sheet.rb +48 -0
- data/lib/portable/uniqueness.rb +23 -0
- data/lib/portable/version.rb +1 -1
- data/lib/portable/writers.rb +10 -0
- data/lib/portable/writers/base.rb +33 -0
- data/lib/portable/writers/csv.rb +85 -0
- data/lib/portable/writers/result.rb +24 -0
- data/portable.gemspec +2 -2
- metadata +23 -10
- data/lib/portable/column.rb +0 -37
- data/lib/portable/export.rb +0 -39
- data/lib/portable/transformer.rb +0 -32
- data/lib/portable/writer.rb +0 -82
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7b9fa7e8020098fb9dd2d9e2c9c984e0d71ff0aa02d4f568879bb013ae65b8d4
|
4
|
+
data.tar.gz: 1e01f15d35797f4954b5a4861b6a8d44e54f3571fc81cd73c5ff2c08ac8ef550
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 97d59eb7d25b5ea806d908995f8978c8f711d280bd9c8b1e2dd0c09ca173f1c2fcf132f828e2218c17f3637fc6d92e8f7e12b827212ae404c153b2c5722cbbeb
|
7
|
+
data.tar.gz: 3878dc2b2f38b9de6cc90c6644e3913379cecefd21adef98b35adf9cbcce3bf0fae4832491de6f3a063477b2d64416778c55e4f0383820cdfc75970d0ece5152
|
data/.rubocop.yml
CHANGED
data/README.md
CHANGED
@@ -2,18 +2,9 @@
|
|
2
2
|
|
3
3
|
[![Gem Version](https://badge.fury.io/rb/portable.svg)](https://badge.fury.io/rb/portable) [![Build Status](https://travis-ci.org/bluemarblepayroll/portable.svg?branch=master)](https://travis-ci.org/bluemarblepayroll/portable) [![Maintainability](https://api.codeclimate.com/v1/badges/4b47ce94b0c9d889e648/maintainability)](https://codeclimate.com/github/bluemarblepayroll/portable/maintainability) [![Test Coverage](https://api.codeclimate.com/v1/badges/4b47ce94b0c9d889e648/test_coverage)](https://codeclimate.com/github/bluemarblepayroll/portable/test_coverage) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
|
4
4
|
|
5
|
-
|
5
|
+
Portable is a virtual document object modeling library. Out of the box is provides a CSV writer but others for other formats like Microsoft Excel could easily be implemented and used.
|
6
6
|
|
7
|
-
|
8
|
-
|
9
|
-
1. Only supports CSV with limited options
|
10
|
-
2. Only supports writing to local file system.
|
11
|
-
|
12
|
-
Future extension considerations:
|
13
|
-
|
14
|
-
1. Support Excel and richer formatting, sheets, etc.
|
15
|
-
2. Expand CSV options: delimiter, forcing quotes, etc.
|
16
|
-
3. Support PDF
|
7
|
+
This library utilizes [the Realize library](https://github.com/bluemarblepayroll/realize) that allows you to express transformation pipelines. Essentially this library is meant to be the transformation and load steps within a larger ETL system. We currently use this in production paired up with [Dbee](https://github.com/bluemarblepayroll/dbee) for data sources to go from configurable data model + query to file.
|
17
8
|
|
18
9
|
## Installation
|
19
10
|
|
@@ -31,40 +22,40 @@ bundle add portable
|
|
31
22
|
|
32
23
|
## Examples
|
33
24
|
|
34
|
-
### Getting Started
|
25
|
+
### Getting Started Writing CSV Files
|
35
26
|
|
36
|
-
Consider the following data
|
27
|
+
Consider the following data provider/source:
|
37
28
|
|
38
29
|
````ruby
|
39
30
|
patients = [
|
40
31
|
{ first: 'Marky', last: 'Mark', dob: '2000-04-05' },
|
41
32
|
{ first: 'Frank', last: 'Rizzo', dob: '1930-09-22' }
|
42
33
|
]
|
34
|
+
|
35
|
+
data_provider = Portable::Data::Provider.new(
|
36
|
+
data_sources: {
|
37
|
+
data_rows: patients,
|
38
|
+
fields: %i[first last dob]
|
39
|
+
}
|
40
|
+
)
|
43
41
|
````
|
44
42
|
|
45
|
-
|
43
|
+
**Note:** Data::Provider and Data::Source objects are pretty basic, on purpose, so they can be easily re-implemented based on an application's specific needs.
|
44
|
+
|
45
|
+
We could configure the most basic document like so:
|
46
46
|
|
47
47
|
````ruby
|
48
|
-
|
49
|
-
columns: [
|
50
|
-
{ header: :first },
|
51
|
-
{ header: :last },
|
52
|
-
{ header: :dob }
|
53
|
-
]
|
54
|
-
}
|
48
|
+
document = nil # or {} or Portable::Document.new
|
55
49
|
````
|
56
50
|
|
57
|
-
|
51
|
+
The above document says I would like a document with one sheet, and since I did not provide a data_table specification, I would like all the fields emitted from the data source.
|
52
|
+
|
53
|
+
Combining a document + writer + data provider yields a set of documents (it may be more than one if the writer does not know how to write intra-file sheets, i.e. CSV files.)
|
58
54
|
|
59
55
|
````ruby
|
60
|
-
writer
|
61
|
-
|
62
|
-
|
63
|
-
writer.open(filename) do |writer|
|
64
|
-
patients.each do |patient|
|
65
|
-
writer.write(object: patient)
|
66
|
-
end
|
67
|
-
end
|
56
|
+
writer = Portable::Writers::Csv.new(document)
|
57
|
+
name = File.join('tmp', 'patients.csv')
|
58
|
+
written = writer.write!(filename: name, data_provider: data_provider)
|
68
59
|
````
|
69
60
|
|
70
61
|
We should now have a CSV file at tmp/patients.csv that looks like this:
|
@@ -78,29 +69,35 @@ Frank | Rizzo | 1930-09-22
|
|
78
69
|
|
79
70
|
This library uses Realize under the hood, so you have the option of configuring any transformation pipeline for each column. Reviewing [Realize's list of transformers](https://github.com/bluemarblepayroll/realize#transformer-gallery) is recommended to see what is available.
|
80
71
|
|
81
|
-
Let's expand our example above with different headers and date formatting:
|
72
|
+
Let's expand our CSV example above with different headers and date formatting:
|
82
73
|
|
83
74
|
````ruby
|
84
|
-
|
85
|
-
|
75
|
+
document = {
|
76
|
+
sheets: [
|
86
77
|
{
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
78
|
+
data_table: {
|
79
|
+
columns: [
|
80
|
+
{
|
81
|
+
header: 'First Name',
|
82
|
+
transformers: [
|
83
|
+
{ type: 'r/value/resolve', key: :first }
|
84
|
+
]
|
85
|
+
},
|
86
|
+
{
|
87
|
+
header: 'Last Name',
|
88
|
+
transformers: [
|
89
|
+
{ type: 'r/value/resolve', key: :last }
|
90
|
+
]
|
91
|
+
},
|
92
|
+
{
|
93
|
+
header: 'Date of Birth',
|
94
|
+
transformers: [
|
95
|
+
{ type: 'r/value/resolve', key: :dob },
|
96
|
+
{ type: 'r/format/date', output_format: '%m/%d/%Y' },
|
97
|
+
]
|
98
|
+
}
|
99
|
+
]
|
100
|
+
}
|
104
101
|
}
|
105
102
|
]
|
106
103
|
}
|
@@ -115,6 +112,70 @@ Frank | Rizzo | 09/22/1930
|
|
115
112
|
|
116
113
|
Realize is also [pluggable](https://github.com/bluemarblepayroll/realize#plugging-in-transformers), so you are able to create your own and plug them directly into Realize.
|
117
114
|
|
115
|
+
### Options
|
116
|
+
|
117
|
+
Each writer can choose how and which options to support.
|
118
|
+
|
119
|
+
#### CSV Options
|
120
|
+
|
121
|
+
The following options are available for customizing CSV documents:
|
122
|
+
|
123
|
+
* byte_order_mark: (optional, default is nothing): This option will write out a byte order mark identifying the encoding for the file. This is useful for ensuring applications like Microsoft Excel open CSV files properly. See Portable::Modeling::ByteOrderMark constants for acceptable values.
|
124
|
+
|
125
|
+
### Static Header/Footer Rows
|
126
|
+
|
127
|
+
The main document model can also include statically defined rows to place either at the header (above data table) or footer (below data table) locations. You can also have the data_source inject static header and footer rows as well. For example:
|
128
|
+
|
129
|
+
````ruby
|
130
|
+
patients = [
|
131
|
+
{ first: 'Marky', last: 'Mark', dob: '2000-04-05' },
|
132
|
+
{ first: 'Frank', last: 'Rizzo', dob: '1930-09-22' }
|
133
|
+
]
|
134
|
+
|
135
|
+
data_provider = Portable::Data::Provider.new(
|
136
|
+
data_sources: {
|
137
|
+
data_rows: patients,
|
138
|
+
fields: %i[first last dob],
|
139
|
+
header_rows: [
|
140
|
+
%w[FIRST_START LAST_START DOB_START]
|
141
|
+
],
|
142
|
+
footer_rows: [
|
143
|
+
%w[FIRST_END LAST_END DOB_END]
|
144
|
+
]
|
145
|
+
}
|
146
|
+
)
|
147
|
+
|
148
|
+
document = {
|
149
|
+
sheets: [
|
150
|
+
{
|
151
|
+
header_rows: [
|
152
|
+
[ 'Run Date', '04/05/2000' ],
|
153
|
+
[ 'Run By', 'Hops the Bunny' ],
|
154
|
+
[],
|
155
|
+
[ 'BEGIN' ]
|
156
|
+
],
|
157
|
+
footer_rows: [
|
158
|
+
[ 'END' ]
|
159
|
+
]
|
160
|
+
}
|
161
|
+
]
|
162
|
+
}
|
163
|
+
````
|
164
|
+
|
165
|
+
Using this document configuration would yield a CSV with four "header rows" at the top, one "data table header row", two data rows, and one "footer row". This is not easily illustrated in Markdown, but this would be the result:
|
166
|
+
|
167
|
+
````
|
168
|
+
Run Date | 04/05/2000
|
169
|
+
Run By | Hops the Bunny
|
170
|
+
|
171
|
+
BEGIN
|
172
|
+
First Name | Last Name | Date of Birth
|
173
|
+
---------- | --------- | -------------
|
174
|
+
Marky | Mark | 04/05/2000
|
175
|
+
Frank | Rizzo | 09/22/1930
|
176
|
+
END
|
177
|
+
````
|
178
|
+
|
118
179
|
## Contributing
|
119
180
|
|
120
181
|
### Development Environment Configuration
|
data/lib/portable.rb
CHANGED
@@ -8,10 +8,19 @@
|
|
8
8
|
#
|
9
9
|
|
10
10
|
require 'acts_as_hashable'
|
11
|
+
require 'benchmark'
|
11
12
|
require 'csv'
|
12
13
|
require 'fileutils'
|
14
|
+
require 'forwardable'
|
13
15
|
require 'objectable'
|
14
16
|
require 'realize'
|
15
17
|
require 'time'
|
16
18
|
|
17
|
-
|
19
|
+
# Shared modules/classes
|
20
|
+
require_relative 'portable/uniqueness'
|
21
|
+
|
22
|
+
# Main implementation points
|
23
|
+
require_relative 'portable/data'
|
24
|
+
require_relative 'portable/document'
|
25
|
+
require_relative 'portable/rendering'
|
26
|
+
require_relative 'portable/writers'
|
@@ -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 'data/provider'
|
@@ -0,0 +1,51 @@
|
|
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 'source'
|
11
|
+
|
12
|
+
module Portable
|
13
|
+
module Data
|
14
|
+
# Container of data sources that is inputted into a writer alongside a document.
|
15
|
+
# It contains all the data sources the writer will use to render a document.
|
16
|
+
class Provider
|
17
|
+
include Uniqueness
|
18
|
+
acts_as_hashable
|
19
|
+
|
20
|
+
DEFAULT_NAME = ''
|
21
|
+
|
22
|
+
def initialize(data_sources: [])
|
23
|
+
sources = Source.array(data_sources)
|
24
|
+
@data_sources_by_name = pivot_by_name(sources)
|
25
|
+
|
26
|
+
assert_no_duplicate_names(sources)
|
27
|
+
|
28
|
+
freeze
|
29
|
+
end
|
30
|
+
|
31
|
+
# Use exact name if possible, if not then use the "default" one (noted by a blank name).
|
32
|
+
# Fail hard if we cannot identify which data source to use. This should help prevent
|
33
|
+
# possible configuration issues (i.e. typos.)
|
34
|
+
def data_source(name)
|
35
|
+
data_sources_by_name[name.to_s] ||
|
36
|
+
data_sources_by_name[DEFAULT_NAME] ||
|
37
|
+
raise(ArgumentError, "data source: '#{name}' cannot be found.")
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
attr_reader :data_sources_by_name
|
43
|
+
|
44
|
+
def pivot_by_name(data_sources)
|
45
|
+
data_sources.each_with_object({}) do |data_source, memo|
|
46
|
+
memo[data_source.name] = data_source
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,38 @@
|
|
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 Portable
|
11
|
+
module Data
|
12
|
+
# A single source of data. This is meant to serve as an interface / example implementation
|
13
|
+
# with the intention of being re-implemented within applications. For example, you may
|
14
|
+
# decide more database data sources would be better, so it could be connected to ORMs or
|
15
|
+
# other data adapters; all it really needs to provide is enumerables for each attribute.
|
16
|
+
class Source
|
17
|
+
acts_as_hashable
|
18
|
+
|
19
|
+
attr_reader :header_rows,
|
20
|
+
:footer_rows,
|
21
|
+
:data_rows,
|
22
|
+
:fields,
|
23
|
+
:name
|
24
|
+
|
25
|
+
# Individial header and footer rows are arrays, while individual data_rows is an object
|
26
|
+
# like a hash, Struct, OpenStruct, or really any PORO.
|
27
|
+
def initialize(name: '', header_rows: [], footer_rows: [], data_rows: [], fields: [])
|
28
|
+
@name = name.to_s
|
29
|
+
@header_rows = header_rows || []
|
30
|
+
@footer_rows = footer_rows || []
|
31
|
+
@data_rows = data_rows || []
|
32
|
+
@fields = fields || []
|
33
|
+
|
34
|
+
freeze
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
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 'modeling/options'
|
11
|
+
require_relative 'modeling/sheet'
|
12
|
+
|
13
|
+
module Portable
|
14
|
+
# Top-level object model for a renderable document.
|
15
|
+
class Document
|
16
|
+
include Uniqueness
|
17
|
+
acts_as_hashable
|
18
|
+
|
19
|
+
attr_reader :sheets, :options
|
20
|
+
|
21
|
+
def initialize(sheets: [], options: {})
|
22
|
+
@sheets = Modeling::Sheet.array(sheets)
|
23
|
+
@sheets << Modeling::Sheet.new if @sheets.empty?
|
24
|
+
@options = Modeling::Options.make(options)
|
25
|
+
|
26
|
+
assert_no_duplicate_names(@sheets)
|
27
|
+
|
28
|
+
freeze
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -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 Portable
|
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
|
@@ -0,0 +1,39 @@
|
|
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 Portable
|
11
|
+
module Modeling
|
12
|
+
# Defines all the options a column can contain. The most basic would to just include a header
|
13
|
+
# (defaults to ''). If no transformers are defined then a simple resolver using the header
|
14
|
+
# will be used. This works well for pass-through file writes. Use the transformers to further
|
15
|
+
# customize each data point being written.
|
16
|
+
class Column
|
17
|
+
acts_as_hashable
|
18
|
+
|
19
|
+
DEFAULT_TRANSFORMER_TYPE = 'r/value/resolve'
|
20
|
+
|
21
|
+
attr_reader :header, :transformers
|
22
|
+
|
23
|
+
def initialize(header: '', transformers: [])
|
24
|
+
@header = header.to_s
|
25
|
+
@transformers = Realize::Transformers.array(transformers)
|
26
|
+
|
27
|
+
@transformers << default_transformer if @transformers.empty?
|
28
|
+
|
29
|
+
freeze
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def default_transformer
|
35
|
+
Realize::Transformers.make(type: DEFAULT_TRANSFORMER_TYPE, key: header)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -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
|
+
require_relative 'column'
|
11
|
+
|
12
|
+
module Portable
|
13
|
+
module Modeling
|
14
|
+
# Defines all the options for the data grid within an export like columns, whether or not
|
15
|
+
# you want to include headers, and more.
|
16
|
+
class DataTable
|
17
|
+
acts_as_hashable
|
18
|
+
|
19
|
+
attr_reader :columns
|
20
|
+
|
21
|
+
def initialize(columns: [])
|
22
|
+
@columns = Column.array(columns)
|
23
|
+
|
24
|
+
freeze
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
# Copyright (c) 2020-present, Blue Marble Payroll, LLC
|
5
|
+
#
|
6
|
+
# This source code is licensed under the MIT license found in the
|
7
|
+
# LICENSE file in the root directory of this source tree.
|
8
|
+
#
|
9
|
+
|
10
|
+
require_relative 'byte_order_mark'
|
11
|
+
|
12
|
+
module Portable
|
13
|
+
module Modeling
|
14
|
+
# Defines all the options for an export including static header rows, footer rows, and how
|
15
|
+
# to draw the data table.
|
16
|
+
class Options
|
17
|
+
acts_as_hashable
|
18
|
+
|
19
|
+
attr_reader :byte_order_mark
|
20
|
+
|
21
|
+
def initialize(byte_order_mark: nil)
|
22
|
+
@byte_order_mark = ByteOrderMark.resolve(byte_order_mark)
|
23
|
+
|
24
|
+
freeze
|
25
|
+
end
|
26
|
+
|
27
|
+
def byte_order_mark?
|
28
|
+
!byte_order_mark.nil?
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,48 @@
|
|
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 'data_table'
|
11
|
+
|
12
|
+
module Portable
|
13
|
+
module Modeling
|
14
|
+
# Abstract concept modeling for the notion of a "sheet" in a "document". This means different
|
15
|
+
# things given the writer. For example, all writers should support multiple sheets but
|
16
|
+
# there is no internal representation of a "sheet" within a CSV, so each sheet will emit
|
17
|
+
# one file.
|
18
|
+
class Sheet
|
19
|
+
acts_as_hashable
|
20
|
+
|
21
|
+
attr_reader :data_table,
|
22
|
+
:footer_rows,
|
23
|
+
:header_rows,
|
24
|
+
:name,
|
25
|
+
:include_headers
|
26
|
+
|
27
|
+
def initialize(
|
28
|
+
data_table: nil,
|
29
|
+
footer_rows: [],
|
30
|
+
header_rows: [],
|
31
|
+
name: '',
|
32
|
+
include_headers: true
|
33
|
+
)
|
34
|
+
@name = name.to_s
|
35
|
+
@data_table = DataTable.make(data_table)
|
36
|
+
@footer_rows = footer_rows || []
|
37
|
+
@header_rows = header_rows || []
|
38
|
+
@include_headers = include_headers || false
|
39
|
+
|
40
|
+
freeze
|
41
|
+
end
|
42
|
+
|
43
|
+
def include_headers?
|
44
|
+
include_headers
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -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 'rendering/sheet'
|
@@ -0,0 +1,40 @@
|
|
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 Portable
|
11
|
+
module Rendering
|
12
|
+
# Internal intermediary class that knows how to combine columns specification
|
13
|
+
# instances with their respective Realize pipelines.
|
14
|
+
class Row # :nodoc: all
|
15
|
+
attr_reader :column_pipelines
|
16
|
+
|
17
|
+
def initialize(columns, resolver: Objectable.resolver)
|
18
|
+
@column_pipelines = columns.each_with_object({}) do |column, memo|
|
19
|
+
memo[column] = Realize::Pipeline.new(column.transformers, resolver: resolver)
|
20
|
+
end
|
21
|
+
|
22
|
+
freeze
|
23
|
+
end
|
24
|
+
|
25
|
+
def render(object, time)
|
26
|
+
column_pipelines.each_with_object({}) do |(column, pipeline), memo|
|
27
|
+
memo[column.header] = pipeline.transform(object, time)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def columns
|
32
|
+
column_pipelines.keys
|
33
|
+
end
|
34
|
+
|
35
|
+
def headers
|
36
|
+
columns.map(&:header)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,48 @@
|
|
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 'row'
|
11
|
+
|
12
|
+
module Portable
|
13
|
+
module Rendering
|
14
|
+
# Understands the connection between a document's sheets and the internal row renderer
|
15
|
+
# necessary to render each sheet's data table.
|
16
|
+
class Sheet # :nodoc: all
|
17
|
+
attr_reader :document, :resolver
|
18
|
+
|
19
|
+
def initialize(document, resolver: Objectable.resolver)
|
20
|
+
@document = Document.make(document, nullable: false)
|
21
|
+
@resolver = resolver
|
22
|
+
|
23
|
+
@row_renderers = @document.sheets.each_with_object({}) do |sheet, memo|
|
24
|
+
next unless sheet.data_table
|
25
|
+
|
26
|
+
memo[sheet.name] = Row.new(sheet.data_table.columns, resolver: resolver)
|
27
|
+
end
|
28
|
+
|
29
|
+
freeze
|
30
|
+
end
|
31
|
+
|
32
|
+
def row_renderer(sheet_name, fields)
|
33
|
+
row_renderers.fetch(sheet_name, dynamic_row_renderer(fields))
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
attr_reader :row_renderers
|
39
|
+
|
40
|
+
def dynamic_row_renderer(fields)
|
41
|
+
fields = (fields || []).map { |f| { header: f.to_s } }
|
42
|
+
columns = Modeling::Column.array(fields)
|
43
|
+
|
44
|
+
Row.new(columns, resolver: resolver)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
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
|
+
module Portable
|
11
|
+
# Mixes in helpers for asserting uniqueness across collections
|
12
|
+
module Uniqueness
|
13
|
+
class DuplicateNameError < StandardError; end
|
14
|
+
|
15
|
+
def assert_no_duplicate_names(array)
|
16
|
+
names = array.map { |a| a.name.downcase }
|
17
|
+
|
18
|
+
return if names.uniq.length == array.length
|
19
|
+
|
20
|
+
raise DuplicateNameError, "cannot contain duplicate names: #{names}"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
data/lib/portable/version.rb
CHANGED
@@ -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 'writers/csv'
|
@@ -0,0 +1,33 @@
|
|
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 Portable
|
11
|
+
module Writers
|
12
|
+
# Abstract base for all writers to share.
|
13
|
+
class Base
|
14
|
+
attr_reader :document,
|
15
|
+
:sheet_renderer
|
16
|
+
|
17
|
+
def initialize(document, resolver: Objectable.resolver)
|
18
|
+
@document = Document.make(document, nullable: false)
|
19
|
+
@sheet_renderer = Rendering::Sheet.new(@document, resolver: resolver)
|
20
|
+
|
21
|
+
freeze
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def ensure_directory_exists(filename)
|
27
|
+
path = File.dirname(filename)
|
28
|
+
|
29
|
+
FileUtils.mkdir_p(path) unless File.exist?(path)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,85 @@
|
|
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
|
+
require_relative 'result'
|
12
|
+
|
13
|
+
module Portable
|
14
|
+
module Writers
|
15
|
+
# Can write documents to a CSV file.
|
16
|
+
class Csv < Base
|
17
|
+
def write!(filename:, data_provider: Data::Provider.new, time: Time.now.utc)
|
18
|
+
raise ArgumentError, 'filename is required' if filename.to_s.empty?
|
19
|
+
|
20
|
+
ensure_directory_exists(filename)
|
21
|
+
|
22
|
+
sheet_filenames = extrapolate_filenames(filename, document.sheets.length)
|
23
|
+
|
24
|
+
document.sheets.map.with_index do |sheet, index|
|
25
|
+
data_source = data_provider.data_source(sheet.name)
|
26
|
+
sheet_filename = sheet_filenames[index]
|
27
|
+
|
28
|
+
time_in_seconds = Benchmark.measure do
|
29
|
+
write_sheet(sheet_filename, sheet, data_source, time)
|
30
|
+
end.real
|
31
|
+
|
32
|
+
Result.new(sheet_filename, time_in_seconds)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def write_sheet(sheet_filename, sheet, data_source, time)
|
39
|
+
CSV.open(sheet_filename, 'w') do |csv|
|
40
|
+
csv.to_io.write(document.options.byte_order_mark) if document.options.byte_order_mark?
|
41
|
+
|
42
|
+
write_head(csv, sheet, data_source)
|
43
|
+
write_data_table(csv, sheet, data_source, time)
|
44
|
+
write_foot(csv, sheet, data_source)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def write_head(csv, sheet, data_source)
|
49
|
+
sheet.header_rows.each { |row| csv << row }
|
50
|
+
|
51
|
+
data_source.header_rows.each { |row| csv << row }
|
52
|
+
end
|
53
|
+
|
54
|
+
def write_data_table(csv, sheet, data_source, time)
|
55
|
+
row_renderer = sheet_renderer.row_renderer(sheet.name, data_source.fields)
|
56
|
+
|
57
|
+
csv << row_renderer.headers if sheet.include_headers?
|
58
|
+
|
59
|
+
data_source.data_rows.each do |row|
|
60
|
+
csv << row_renderer.render(row, time).values
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def write_foot(csv, sheet, data_source)
|
65
|
+
data_source.footer_rows.each { |row| csv << row }
|
66
|
+
|
67
|
+
sheet.footer_rows.each { |row| csv << row }
|
68
|
+
end
|
69
|
+
|
70
|
+
def extrapolate_filenames(filename, count)
|
71
|
+
dir = File.dirname(filename)
|
72
|
+
ext = File.extname(filename)
|
73
|
+
basename = File.basename(filename, ext)
|
74
|
+
|
75
|
+
(0..count).map do |index|
|
76
|
+
if index.positive?
|
77
|
+
File.join(dir, "#{basename}.#{index}#{ext}")
|
78
|
+
else
|
79
|
+
filename
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,24 @@
|
|
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 Portable
|
11
|
+
module Writers
|
12
|
+
# Result return object from a Writer#write! call.
|
13
|
+
class Result
|
14
|
+
attr_reader :filename, :time_in_seconds
|
15
|
+
|
16
|
+
def initialize(filename, time_in_seconds)
|
17
|
+
@filename = filename
|
18
|
+
@time_in_seconds = time_in_seconds
|
19
|
+
|
20
|
+
freeze
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
data/portable.gemspec
CHANGED
@@ -5,10 +5,10 @@ require './lib/portable/version'
|
|
5
5
|
Gem::Specification.new do |s|
|
6
6
|
s.name = 'portable'
|
7
7
|
s.version = Portable::VERSION
|
8
|
-
s.summary = '
|
8
|
+
s.summary = 'Virtual Document Modeling and Rendering Engine'
|
9
9
|
|
10
10
|
s.description = <<-DESCRIPTION
|
11
|
-
|
11
|
+
Portable is a virtual document object modeling library. Out of the box is provides a CSV writer but others for other formats like Microsoft Excel could easily be implemented and used.
|
12
12
|
DESCRIPTION
|
13
13
|
|
14
14
|
s.authors = ['Matthew Ruggio']
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: portable
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.0.pre.alpha
|
4
|
+
version: 1.0.0.pre.alpha.5
|
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-08-
|
11
|
+
date: 2020-08-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: acts_as_hashable
|
@@ -150,9 +150,9 @@ dependencies:
|
|
150
150
|
- - "~>"
|
151
151
|
- !ruby/object:Gem::Version
|
152
152
|
version: 0.7.0
|
153
|
-
description: "
|
154
|
-
|
155
|
-
|
153
|
+
description: " Portable is a virtual document object modeling library. Out of
|
154
|
+
the box is provides a CSV writer but others for other formats like Microsoft Excel
|
155
|
+
could easily be implemented and used.\n"
|
156
156
|
email:
|
157
157
|
- mruggio@bluemarblepayroll.com
|
158
158
|
executables: []
|
@@ -173,11 +173,24 @@ files:
|
|
173
173
|
- bin/console
|
174
174
|
- exe/.gitkeep
|
175
175
|
- lib/portable.rb
|
176
|
-
- lib/portable/
|
177
|
-
- lib/portable/
|
178
|
-
- lib/portable/
|
176
|
+
- lib/portable/data.rb
|
177
|
+
- lib/portable/data/provider.rb
|
178
|
+
- lib/portable/data/source.rb
|
179
|
+
- lib/portable/document.rb
|
180
|
+
- lib/portable/modeling/byte_order_mark.rb
|
181
|
+
- lib/portable/modeling/column.rb
|
182
|
+
- lib/portable/modeling/data_table.rb
|
183
|
+
- lib/portable/modeling/options.rb
|
184
|
+
- lib/portable/modeling/sheet.rb
|
185
|
+
- lib/portable/rendering.rb
|
186
|
+
- lib/portable/rendering/row.rb
|
187
|
+
- lib/portable/rendering/sheet.rb
|
188
|
+
- lib/portable/uniqueness.rb
|
179
189
|
- lib/portable/version.rb
|
180
|
-
- lib/portable/
|
190
|
+
- lib/portable/writers.rb
|
191
|
+
- lib/portable/writers/base.rb
|
192
|
+
- lib/portable/writers/csv.rb
|
193
|
+
- lib/portable/writers/result.rb
|
181
194
|
- portable.gemspec
|
182
195
|
homepage: https://github.com/bluemarblepayroll/portable
|
183
196
|
licenses:
|
@@ -206,5 +219,5 @@ requirements: []
|
|
206
219
|
rubygems_version: 3.0.3
|
207
220
|
signing_key:
|
208
221
|
specification_version: 4
|
209
|
-
summary:
|
222
|
+
summary: Virtual Document Modeling and Rendering Engine
|
210
223
|
test_files: []
|
data/lib/portable/column.rb
DELETED
@@ -1,37 +0,0 @@
|
|
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 Portable
|
11
|
-
# Defines all the options a column can contain. The most basic would to just include a header
|
12
|
-
# (defaults to ''). If no transformers are defined then a simple resolver using the header
|
13
|
-
# will be used. This works well for pass-through file writes. Use the transformers to further
|
14
|
-
# customize each data point being written.
|
15
|
-
class Column
|
16
|
-
acts_as_hashable
|
17
|
-
|
18
|
-
DEFAULT_TRANSFORMER_TYPE = 'r/value/resolve'
|
19
|
-
|
20
|
-
attr_reader :header, :transformers
|
21
|
-
|
22
|
-
def initialize(header: '', transformers: [])
|
23
|
-
@header = header.to_s
|
24
|
-
@transformers = Realize::Transformers.array(transformers)
|
25
|
-
|
26
|
-
@transformers << default_transformer if @transformers.empty?
|
27
|
-
|
28
|
-
freeze
|
29
|
-
end
|
30
|
-
|
31
|
-
private
|
32
|
-
|
33
|
-
def default_transformer
|
34
|
-
Realize::Transformers.make(type: DEFAULT_TRANSFORMER_TYPE, key: header)
|
35
|
-
end
|
36
|
-
end
|
37
|
-
end
|
data/lib/portable/export.rb
DELETED
@@ -1,39 +0,0 @@
|
|
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 'column'
|
11
|
-
|
12
|
-
module Portable
|
13
|
-
# Defines all the options for an export like columns, whether or not you want to include
|
14
|
-
# headers, and more.
|
15
|
-
class Export
|
16
|
-
acts_as_hashable
|
17
|
-
|
18
|
-
module Bom
|
19
|
-
UTF8 = "\uFEFF"
|
20
|
-
end
|
21
|
-
include Bom
|
22
|
-
|
23
|
-
attr_reader :bom, :columns, :include_headers
|
24
|
-
|
25
|
-
alias include_headers? include_headers
|
26
|
-
|
27
|
-
def initialize(bom: nil, columns: [], include_headers: true)
|
28
|
-
@bom = bom ? Bom.const_get(bom.to_s.upcase.to_sym) : nil
|
29
|
-
@columns = Column.array(columns)
|
30
|
-
@include_headers = include_headers || false
|
31
|
-
|
32
|
-
freeze
|
33
|
-
end
|
34
|
-
|
35
|
-
def headers
|
36
|
-
columns.map(&:header)
|
37
|
-
end
|
38
|
-
end
|
39
|
-
end
|
data/lib/portable/transformer.rb
DELETED
@@ -1,32 +0,0 @@
|
|
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 'export'
|
11
|
-
|
12
|
-
module Portable
|
13
|
-
# Internal intermediary class that knows how to combine columns specification instances with their
|
14
|
-
# respective Realize pipelines.
|
15
|
-
class Transformer # :nodoc: all
|
16
|
-
attr_reader :column_pipelines
|
17
|
-
|
18
|
-
def initialize(columns, resolver: Objectable.resolver)
|
19
|
-
@column_pipelines = columns.each_with_object({}) do |column, memo|
|
20
|
-
memo[column] = Realize::Pipeline.new(column.transformers, resolver: resolver)
|
21
|
-
end
|
22
|
-
|
23
|
-
freeze
|
24
|
-
end
|
25
|
-
|
26
|
-
def transform(object, time)
|
27
|
-
column_pipelines.each_with_object({}) do |(column, pipeline), memo|
|
28
|
-
memo[column.header] = pipeline.transform(object, time)
|
29
|
-
end
|
30
|
-
end
|
31
|
-
end
|
32
|
-
end
|
data/lib/portable/writer.rb
DELETED
@@ -1,82 +0,0 @@
|
|
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 'export'
|
11
|
-
require_relative 'transformer'
|
12
|
-
|
13
|
-
module Portable
|
14
|
-
# Main API for writing files. There are two main patterns to choose from:
|
15
|
-
# 1. calling #open, #write, and #close manually.
|
16
|
-
# 2. calling #open and passing a block and having #close automatically called.
|
17
|
-
class Writer
|
18
|
-
class AlreadyOpenError < StandardError; end
|
19
|
-
class NotOpenError < StandardError; end
|
20
|
-
|
21
|
-
attr_reader :csv, :export, :transformer
|
22
|
-
|
23
|
-
def initialize(export, resolver: Objectable.resolver)
|
24
|
-
@export = Export.make(export, nullable: false)
|
25
|
-
@transformer = Transformer.new(@export.columns, resolver: resolver)
|
26
|
-
end
|
27
|
-
|
28
|
-
def open?
|
29
|
-
!csv.nil?
|
30
|
-
end
|
31
|
-
|
32
|
-
# Will raise a AlreadyOpenError exception if a writer has already been opened but
|
33
|
-
# not yet closed.
|
34
|
-
def open(filename)
|
35
|
-
raise AlreadyOpenError, 'writer is already open' if open?
|
36
|
-
|
37
|
-
initialize_csv(filename)
|
38
|
-
|
39
|
-
if block_given?
|
40
|
-
yield self
|
41
|
-
close
|
42
|
-
end
|
43
|
-
|
44
|
-
self
|
45
|
-
end
|
46
|
-
|
47
|
-
# Will raise a NotOpenError exception if a writer has not yet been opened.
|
48
|
-
def write(object: {}, time: Time.now.utc)
|
49
|
-
raise NotOpenError, 'writer is not open' unless open?
|
50
|
-
|
51
|
-
csv << transformer.transform(object, time).values
|
52
|
-
|
53
|
-
self
|
54
|
-
end
|
55
|
-
|
56
|
-
# Will raise a NotOpenError exception if a writer has not yet been opened.
|
57
|
-
def close
|
58
|
-
raise NotOpenError, 'writer is not open' unless open?
|
59
|
-
|
60
|
-
@csv.close
|
61
|
-
@csv = nil
|
62
|
-
self
|
63
|
-
end
|
64
|
-
|
65
|
-
private
|
66
|
-
|
67
|
-
def ensure_directory_exists(filename)
|
68
|
-
path = File.dirname(filename)
|
69
|
-
|
70
|
-
FileUtils.mkdir_p(path) unless File.exist?(path)
|
71
|
-
end
|
72
|
-
|
73
|
-
def initialize_csv(filename)
|
74
|
-
ensure_directory_exists(filename)
|
75
|
-
|
76
|
-
@csv = CSV.open(filename, 'w')
|
77
|
-
|
78
|
-
csv.to_io.write(export.bom) if export.bom
|
79
|
-
csv << export.headers if export.include_headers?
|
80
|
-
end
|
81
|
-
end
|
82
|
-
end
|