portable 1.0.0.pre.alpha.1 → 1.0.0.pre.alpha.2
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 +80 -84
- data/lib/portable.rb +2 -2
- data/lib/portable/data.rb +10 -0
- data/lib/portable/data/provider.rb +40 -0
- data/lib/portable/data/source.rb +38 -0
- data/lib/portable/document.rb +9 -16
- data/lib/portable/{csv → modeling}/byte_order_mark.rb +1 -1
- data/lib/portable/modeling/column.rb +39 -0
- data/lib/portable/modeling/data_table.rb +28 -0
- data/lib/portable/{csv → modeling}/options.rb +1 -1
- 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/version.rb +1 -1
- data/lib/portable/writers.rb +1 -1
- data/lib/portable/writers/base.rb +25 -0
- data/lib/portable/writers/csv.rb +92 -0
- data/portable.gemspec +2 -2
- metadata +19 -13
- data/lib/portable/column.rb +0 -37
- data/lib/portable/csv/document.rb +0 -34
- data/lib/portable/csv/writer.rb +0 -109
- data/lib/portable/data_table.rb +0 -39
- data/lib/portable/transformer.rb +0 -30
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fe43368608c03ce86c76422bdd7cdaba14cc7df66452780ff8af6aedc3c3c411
|
4
|
+
data.tar.gz: d90e21a8382237941ec558890ccd5b731734a4548baa054b78832989102141eb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 076c5e70d9ef53d132dfea0691917afc5776bc5e9322ee50d56284f22eb37e7ce035d09218def74b7a702dcf5cea312e8cbab960932dcfe09a70fad9e52c534d
|
7
|
+
data.tar.gz: a726ceb297e8109de9996d2a9dde20561e843a83fc2a8ea9393acfe780eddc3e2c42ede6cb373c84e246292342655083bea6698ccd37c3b31a779fbe69db0863
|
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
|
|
@@ -33,36 +24,38 @@ bundle add portable
|
|
33
24
|
|
34
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
|
-
document = {
|
49
|
-
data_table: {
|
50
|
-
columns: [
|
51
|
-
{ header: :first },
|
52
|
-
{ header: :last },
|
53
|
-
{ header: :dob }
|
54
|
-
]
|
55
|
-
}
|
56
|
-
}
|
48
|
+
document = nil # or {} or Portable::Document.new
|
57
49
|
````
|
58
50
|
|
59
|
-
|
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.
|
60
52
|
|
61
|
-
|
62
|
-
writer = Portable::Csv::Writer.new(export)
|
63
|
-
filename = File.join('tmp', 'patients.csv')
|
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.)
|
64
54
|
|
65
|
-
|
55
|
+
````ruby
|
56
|
+
writer = Portable::Writers::Csv.new(document)
|
57
|
+
name = File.join('tmp', 'patients.csv')
|
58
|
+
written = writer.write!(filename: name, data_provider: data_provider)
|
66
59
|
````
|
67
60
|
|
68
61
|
We should now have a CSV file at tmp/patients.csv that looks like this:
|
@@ -80,29 +73,33 @@ Let's expand our CSV example above with different headers and date formatting:
|
|
80
73
|
|
81
74
|
````ruby
|
82
75
|
document = {
|
83
|
-
|
84
|
-
|
85
|
-
{
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
76
|
+
sheets: [
|
77
|
+
{
|
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
|
+
}
|
102
99
|
]
|
103
100
|
}
|
104
|
-
|
105
|
-
|
101
|
+
}
|
102
|
+
]
|
106
103
|
}
|
107
104
|
````
|
108
105
|
|
@@ -117,52 +114,51 @@ Realize is also [pluggable](https://github.com/bluemarblepayroll/realize#pluggin
|
|
117
114
|
|
118
115
|
### Options
|
119
116
|
|
120
|
-
Each writer can
|
117
|
+
Each writer can choose how and which options to support.
|
121
118
|
|
122
119
|
#### CSV Options
|
123
120
|
|
124
121
|
The following options are available for customizing CSV documents:
|
125
122
|
|
126
|
-
* 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::
|
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.
|
127
124
|
|
128
|
-
###
|
125
|
+
### Static Header/Footer Rows
|
129
126
|
|
130
|
-
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. For example:
|
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:
|
131
128
|
|
132
129
|
````ruby
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
},
|
148
|
-
{
|
149
|
-
header: 'Date of Birth',
|
150
|
-
transformers: [
|
151
|
-
{ type: 'r/value/resolve', key: :dob },
|
152
|
-
{ type: 'r/format/date', output_format: '%m/%d/%Y' },
|
153
|
-
]
|
154
|
-
}
|
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]
|
155
144
|
]
|
156
|
-
}
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
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
|
+
]
|
166
162
|
}
|
167
163
|
````
|
168
164
|
|
data/lib/portable.rb
CHANGED
@@ -15,7 +15,7 @@ require 'objectable'
|
|
15
15
|
require 'realize'
|
16
16
|
require 'time'
|
17
17
|
|
18
|
-
require_relative 'portable/
|
18
|
+
require_relative 'portable/data'
|
19
19
|
require_relative 'portable/document'
|
20
|
-
require_relative 'portable/
|
20
|
+
require_relative 'portable/rendering'
|
21
21
|
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,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
|
+
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
|
+
acts_as_hashable
|
18
|
+
|
19
|
+
def initialize(data_sources: [])
|
20
|
+
@data_sources_by_name = pivot_by_name(Source.array(data_sources))
|
21
|
+
|
22
|
+
freeze
|
23
|
+
end
|
24
|
+
|
25
|
+
def data_source(name)
|
26
|
+
data_sources_by_name.fetch(name.to_s, Source.new)
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
attr_reader :data_sources_by_name
|
32
|
+
|
33
|
+
def pivot_by_name(data_sources)
|
34
|
+
data_sources.each_with_object({}) do |data_source, memo|
|
35
|
+
memo[data_source.name] = data_source
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
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
|
data/lib/portable/document.rb
CHANGED
@@ -7,27 +7,20 @@
|
|
7
7
|
# LICENSE file in the root directory of this source tree.
|
8
8
|
#
|
9
9
|
|
10
|
+
require_relative 'modeling/options'
|
11
|
+
require_relative 'modeling/sheet'
|
12
|
+
|
10
13
|
module Portable
|
11
|
-
#
|
14
|
+
# Top-level object model for a renderable document.
|
12
15
|
class Document
|
13
16
|
acts_as_hashable
|
14
|
-
extend Forwardable
|
15
|
-
|
16
|
-
attr_reader :data_table,
|
17
|
-
:footer_rows,
|
18
|
-
:header_rows
|
19
17
|
|
20
|
-
|
21
|
-
:columns,
|
22
|
-
:headers,
|
23
|
-
:include_headers?,
|
24
|
-
:headers,
|
25
|
-
:transform
|
18
|
+
attr_reader :sheets, :options
|
26
19
|
|
27
|
-
def initialize(
|
28
|
-
@
|
29
|
-
@
|
30
|
-
@
|
20
|
+
def initialize(sheets: [], options: {})
|
21
|
+
@sheets = Modeling::Sheet.array(sheets)
|
22
|
+
@sheets << Modeling::Sheet.new if @sheets.empty?
|
23
|
+
@options = Modeling::Options.make(options)
|
31
24
|
|
32
25
|
freeze
|
33
26
|
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,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
|
data/lib/portable/version.rb
CHANGED
data/lib/portable/writers.rb
CHANGED
@@ -0,0 +1,25 @@
|
|
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
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
# Copyright (c) 2020-present, Blue Marble Payroll, LLC
|
5
|
+
#
|
6
|
+
# This source code is licensed under the MIT license found in the
|
7
|
+
# LICENSE file in the root directory of this source tree.
|
8
|
+
#
|
9
|
+
|
10
|
+
require_relative 'base'
|
11
|
+
|
12
|
+
module Portable
|
13
|
+
module Writers
|
14
|
+
# Can write documents to a CSV file.
|
15
|
+
class Csv < Base
|
16
|
+
def write!(filename:, data_provider: Data::Provider.new, time: Time.now.utc)
|
17
|
+
raise ArgumentError, 'filename is required' if filename.to_s.empty?
|
18
|
+
|
19
|
+
ensure_directory_exists(filename)
|
20
|
+
|
21
|
+
sheet_filenames = extrapolate_filenames(filename)
|
22
|
+
|
23
|
+
document.sheets.each do |sheet|
|
24
|
+
data_source = data_provider.data_source(sheet.name)
|
25
|
+
sheet_filename = sheet_filenames[sheet.name]
|
26
|
+
|
27
|
+
write_sheet(sheet_filename, sheet, data_source, time)
|
28
|
+
end
|
29
|
+
|
30
|
+
sheet_filenames.values
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def write_sheet(sheet_filename, sheet, data_source, time)
|
36
|
+
CSV.open(sheet_filename, 'w') do |csv|
|
37
|
+
csv.to_io.write(document.options.byte_order_mark) if document.options.byte_order_mark?
|
38
|
+
|
39
|
+
write_head(csv, sheet, data_source)
|
40
|
+
write_data_table(csv, sheet, data_source, time)
|
41
|
+
write_foot(csv, sheet, data_source)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def write_head(csv, sheet, data_source)
|
46
|
+
sheet.header_rows.each { |row| csv << row }
|
47
|
+
|
48
|
+
data_source.header_rows.each { |row| csv << row }
|
49
|
+
end
|
50
|
+
|
51
|
+
def write_data_table(csv, sheet, data_source, time)
|
52
|
+
row_renderer = sheet_renderer.row_renderer(sheet.name, data_source.fields)
|
53
|
+
|
54
|
+
csv << row_renderer.headers if sheet.include_headers?
|
55
|
+
|
56
|
+
data_source.data_rows.each do |row|
|
57
|
+
csv << row_renderer.render(row, time).values
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def write_foot(csv, sheet, data_source)
|
62
|
+
data_source.footer_rows.each { |row| csv << row }
|
63
|
+
|
64
|
+
sheet.footer_rows.each { |row| csv << row }
|
65
|
+
end
|
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 extrapolate_filenames(filename)
|
74
|
+
index = 0
|
75
|
+
dir = File.dirname(filename)
|
76
|
+
ext = File.extname(filename)
|
77
|
+
basename = File.basename(filename, ext)
|
78
|
+
|
79
|
+
document.sheets.each_with_object({}) do |sheet, memo|
|
80
|
+
memo[sheet.name] =
|
81
|
+
if index.positive?
|
82
|
+
File.join(dir, "#{basename}.#{index}#{ext}")
|
83
|
+
else
|
84
|
+
filename
|
85
|
+
end
|
86
|
+
|
87
|
+
index += 1
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
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.2
|
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-07 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,16 +173,22 @@ files:
|
|
173
173
|
- bin/console
|
174
174
|
- exe/.gitkeep
|
175
175
|
- lib/portable.rb
|
176
|
-
- lib/portable/
|
177
|
-
- lib/portable/
|
178
|
-
- lib/portable/
|
179
|
-
- lib/portable/csv/options.rb
|
180
|
-
- lib/portable/csv/writer.rb
|
181
|
-
- lib/portable/data_table.rb
|
176
|
+
- lib/portable/data.rb
|
177
|
+
- lib/portable/data/provider.rb
|
178
|
+
- lib/portable/data/source.rb
|
182
179
|
- lib/portable/document.rb
|
183
|
-
- lib/portable/
|
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
|
184
188
|
- lib/portable/version.rb
|
185
189
|
- lib/portable/writers.rb
|
190
|
+
- lib/portable/writers/base.rb
|
191
|
+
- lib/portable/writers/csv.rb
|
186
192
|
- portable.gemspec
|
187
193
|
homepage: https://github.com/bluemarblepayroll/portable
|
188
194
|
licenses:
|
@@ -211,5 +217,5 @@ requirements: []
|
|
211
217
|
rubygems_version: 3.0.3
|
212
218
|
signing_key:
|
213
219
|
specification_version: 4
|
214
|
-
summary:
|
220
|
+
summary: Virtual Document Modeling and Rendering Engine
|
215
221
|
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
|
@@ -1,34 +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 'options'
|
11
|
-
|
12
|
-
module Portable
|
13
|
-
module Csv
|
14
|
-
# Defines all the options for an export including static header rows, footer rows, and how
|
15
|
-
# to draw the data table.
|
16
|
-
class Document < Portable::Document
|
17
|
-
attr_reader :options
|
18
|
-
|
19
|
-
def_delegators :options,
|
20
|
-
:byte_order_mark,
|
21
|
-
:byte_order_mark?
|
22
|
-
|
23
|
-
def initialize(data_table: {}, footer_rows: [], header_rows: [], options: {})
|
24
|
-
@options = Options.make(options)
|
25
|
-
|
26
|
-
super(
|
27
|
-
data_table: data_table,
|
28
|
-
footer_rows: footer_rows,
|
29
|
-
header_rows: header_rows
|
30
|
-
)
|
31
|
-
end
|
32
|
-
end
|
33
|
-
end
|
34
|
-
end
|
data/lib/portable/csv/writer.rb
DELETED
@@ -1,109 +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 'document'
|
11
|
-
|
12
|
-
module Portable
|
13
|
-
module Csv
|
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, :document, :time, :transformer
|
22
|
-
|
23
|
-
def initialize(document, resolver: Objectable.resolver, time: Time.now.utc)
|
24
|
-
@document = Document.make(document, nullable: false)
|
25
|
-
@time = time || Time.now.utc
|
26
|
-
@transformer = Transformer.new(@document.columns, resolver: resolver)
|
27
|
-
end
|
28
|
-
|
29
|
-
def open?
|
30
|
-
!csv.nil?
|
31
|
-
end
|
32
|
-
|
33
|
-
# Will raise a AlreadyOpenError exception if a writer has already been opened but
|
34
|
-
# not yet closed.
|
35
|
-
def open(filename)
|
36
|
-
raise AlreadyOpenError, 'writer is already open' if open?
|
37
|
-
|
38
|
-
initialize_csv(filename)
|
39
|
-
write_head
|
40
|
-
|
41
|
-
if block_given?
|
42
|
-
yield self
|
43
|
-
close
|
44
|
-
end
|
45
|
-
|
46
|
-
self
|
47
|
-
end
|
48
|
-
|
49
|
-
# Will raise a NotOpenError exception if a writer has not yet been opened.
|
50
|
-
def write_all(objects = [])
|
51
|
-
raise NotOpenError, 'writer is not open' unless open?
|
52
|
-
|
53
|
-
objects.each { |o| write(o) }
|
54
|
-
|
55
|
-
self
|
56
|
-
end
|
57
|
-
|
58
|
-
# Will raise a NotOpenError exception if a writer has not yet been opened.
|
59
|
-
def write(object = {})
|
60
|
-
raise NotOpenError, 'writer is not open' unless open?
|
61
|
-
|
62
|
-
csv << transformer.transform(object, time).values
|
63
|
-
|
64
|
-
self
|
65
|
-
end
|
66
|
-
|
67
|
-
# Will raise a NotOpenError exception if a writer has not yet been opened.
|
68
|
-
def close
|
69
|
-
raise NotOpenError, 'writer is not open' unless open?
|
70
|
-
|
71
|
-
write_foot
|
72
|
-
|
73
|
-
@csv.close
|
74
|
-
@csv = nil
|
75
|
-
self
|
76
|
-
end
|
77
|
-
|
78
|
-
private
|
79
|
-
|
80
|
-
def ensure_directory_exists(filename)
|
81
|
-
path = File.dirname(filename)
|
82
|
-
|
83
|
-
FileUtils.mkdir_p(path) unless File.exist?(path)
|
84
|
-
end
|
85
|
-
|
86
|
-
def initialize_csv(filename)
|
87
|
-
ensure_directory_exists(filename)
|
88
|
-
|
89
|
-
@csv = CSV.open(filename, 'w')
|
90
|
-
|
91
|
-
csv.to_io.write(document.byte_order_mark) if document.byte_order_mark?
|
92
|
-
end
|
93
|
-
|
94
|
-
def write_head
|
95
|
-
raw_write(document.header_rows)
|
96
|
-
|
97
|
-
csv << document.headers if document.include_headers?
|
98
|
-
end
|
99
|
-
|
100
|
-
def write_foot
|
101
|
-
raw_write(document.footer_rows)
|
102
|
-
end
|
103
|
-
|
104
|
-
def raw_write(rows)
|
105
|
-
rows.each { |row| csv << row }
|
106
|
-
end
|
107
|
-
end
|
108
|
-
end
|
109
|
-
end
|
data/lib/portable/data_table.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 the data grid within an export like columns, whether or not
|
14
|
-
# you want to include headers, and more.
|
15
|
-
class Datagrid
|
16
|
-
acts_as_hashable
|
17
|
-
|
18
|
-
attr_reader :columns
|
19
|
-
|
20
|
-
def initialize(columns: [], include_headers: true)
|
21
|
-
@columns = Column.array(columns)
|
22
|
-
@include_headers = include_headers || false
|
23
|
-
|
24
|
-
freeze
|
25
|
-
end
|
26
|
-
|
27
|
-
def include_headers?
|
28
|
-
include_headers
|
29
|
-
end
|
30
|
-
|
31
|
-
def headers
|
32
|
-
columns.map(&:header)
|
33
|
-
end
|
34
|
-
|
35
|
-
private
|
36
|
-
|
37
|
-
attr_reader :include_headers
|
38
|
-
end
|
39
|
-
end
|
data/lib/portable/transformer.rb
DELETED
@@ -1,30 +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
|
-
# Internal intermediary class that knows how to combine columns specification instances with their
|
12
|
-
# respective Realize pipelines.
|
13
|
-
class Transformer # :nodoc: all
|
14
|
-
attr_reader :column_pipelines
|
15
|
-
|
16
|
-
def initialize(columns, resolver: Objectable.resolver)
|
17
|
-
@column_pipelines = columns.each_with_object({}) do |column, memo|
|
18
|
-
memo[column] = Realize::Pipeline.new(column.transformers, resolver: resolver)
|
19
|
-
end
|
20
|
-
|
21
|
-
freeze
|
22
|
-
end
|
23
|
-
|
24
|
-
def transform(object, time)
|
25
|
-
column_pipelines.each_with_object({}) do |(column, pipeline), memo|
|
26
|
-
memo[column.header] = pipeline.transform(object, time)
|
27
|
-
end
|
28
|
-
end
|
29
|
-
end
|
30
|
-
end
|