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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 112f223aca7d26dda5b4e9c93b1e83174885e941806de503c2d15be37047cdc1
4
- data.tar.gz: 7d9764151388559dd0ac999b69fa4bed16ff9d02fdb796944ff4d22b30b5056d
3
+ metadata.gz: fe43368608c03ce86c76422bdd7cdaba14cc7df66452780ff8af6aedc3c3c411
4
+ data.tar.gz: d90e21a8382237941ec558890ccd5b731734a4548baa054b78832989102141eb
5
5
  SHA512:
6
- metadata.gz: 0bb98a4e53e669c13fbf361d2b3e5be4572ce2b42ab99e43b555ecc8db7c89c25cd28b653b61bc1b3ab2ace6688886ba0f1d45f04d4d1bcb3b178d7183adad6a
7
- data.tar.gz: 14ac7eebf7b0af0b6553317e1d9f1f51757554ad43b9f0827a036ed12ff9ff9b67c5e2feb5db03566cce6ce43783ad8a24e0aed42a2a5367e306283531cb0214
6
+ metadata.gz: 076c5e70d9ef53d132dfea0691917afc5776bc5e9322ee50d56284f22eb37e7ce035d09218def74b7a702dcf5cea312e8cbab960932dcfe09a70fad9e52c534d
7
+ data.tar.gz: a726ceb297e8109de9996d2a9dde20561e843a83fc2a8ea9393acfe780eddc3e2c42ede6cb373c84e246292342655083bea6698ccd37c3b31a779fbe69db0863
@@ -20,7 +20,7 @@ Metrics/MethodLength:
20
20
  Max: 30
21
21
 
22
22
  Metrics/AbcSize:
23
- Max: 16
23
+ Max: 17
24
24
 
25
25
  Metrics/ClassLength:
26
26
  Max: 125
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
- This library provides a configuration layer that allows you to express transformations, using [Realize](https://github.com/bluemarblepayroll/realize), and will write the transformed data down to disk. Essentially it 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) to go from configurable data model + query to file.
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
- Current limitations:
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 set as an array of hashes:
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
- We could configure an export like so:
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
- And execute the export against the example dataset in order to generate a CSV file:
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
- ````ruby
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
- writer.open(filename) { |writer| writer.write_all(patients) }
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
- data_table: {
84
- columns: [
85
- {
86
- header: 'First Name',
87
- transformers: [
88
- { type: 'r/value/resolve', key: :first }
89
- ]
90
- },
91
- {
92
- header: 'Last Name',
93
- transformers: [
94
- { type: 'r/value/resolve', key: :last }
95
- ]
96
- },
97
- {
98
- header: 'Date of Birth',
99
- transformers: [
100
- { type: 'r/value/resolve', key: :dob },
101
- { type: 'r/format/date', output_format: '%m/%d/%Y' },
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 have its own set of options.
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::Csv::ByteOrderMark constants for acceptable values.
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
- ### Custom Header/Footer Rows
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
- document = {
134
- data_table: {
135
- columns: [
136
- {
137
- header: 'First Name',
138
- transformers: [
139
- { type: 'r/value/resolve', key: :first }
140
- ]
141
- },
142
- {
143
- header: 'Last Name',
144
- transformers: [
145
- { type: 'r/value/resolve', key: :last }
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
- header_rows: [
158
- [ 'Run Date', '04/05/2000' ],
159
- [ 'Run By', 'Hops the Bunny' ],
160
- [],
161
- [ 'BEGIN' ]
162
- ],
163
- header_rows: [
164
- [ 'END' ]
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
 
@@ -15,7 +15,7 @@ require 'objectable'
15
15
  require 'realize'
16
16
  require 'time'
17
17
 
18
- require_relative 'portable/data_table'
18
+ require_relative 'portable/data'
19
19
  require_relative 'portable/document'
20
- require_relative 'portable/transformer'
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
@@ -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
- # Base document object model defining what all documents should include.
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
- def_delegators :data_table,
21
- :columns,
22
- :headers,
23
- :include_headers?,
24
- :headers,
25
- :transform
18
+ attr_reader :sheets, :options
26
19
 
27
- def initialize(data_table: {}, footer_rows: [], header_rows: [])
28
- @data_table = Datagrid.make(data_table)
29
- @footer_rows = footer_rows || []
30
- @header_rows = header_rows || []
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
@@ -8,7 +8,7 @@
8
8
  #
9
9
 
10
10
  module Portable
11
- module Csv
11
+ module Modeling
12
12
  # Define all acceptable byte order mark values.
13
13
  module ByteOrderMark
14
14
  UTF_8 = "\xEF\xBB\xBF"
@@ -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
@@ -10,7 +10,7 @@
10
10
  require_relative 'byte_order_mark'
11
11
 
12
12
  module Portable
13
- module Csv
13
+ module Modeling
14
14
  # Defines all the options for an export including static header rows, footer rows, and how
15
15
  # to draw the data table.
16
16
  class Options
@@ -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
@@ -8,5 +8,5 @@
8
8
  #
9
9
 
10
10
  module Portable
11
- VERSION = '1.0.0-alpha.1'
11
+ VERSION = '1.0.0-alpha.2'
12
12
  end
@@ -7,4 +7,4 @@
7
7
  # LICENSE file in the root directory of this source tree.
8
8
  #
9
9
 
10
- require_relative 'csv/writer'
10
+ require_relative 'writers/csv'
@@ -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
@@ -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 = 'Transformable export writer'
8
+ s.summary = 'Virtual Document Modeling and Rendering Engine'
9
9
 
10
10
  s.description = <<-DESCRIPTION
11
- This library allows you to configure exports, using Realize pipelines, creating a transformation and writing layer. It is meant to serve as an intermediary library within a much larger ETL framework.
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.1
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-04 00:00:00.000000000 Z
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: " This library allows you to configure exports, using Realize pipelines,
154
- creating a transformation and writing layer. It is meant to serve as an intermediary
155
- library within a much larger ETL framework.\n"
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/column.rb
177
- - lib/portable/csv/byte_order_mark.rb
178
- - lib/portable/csv/document.rb
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/transformer.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
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: Transformable export writer
220
+ summary: Virtual Document Modeling and Rendering Engine
215
221
  test_files: []
@@ -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
@@ -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
@@ -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
@@ -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