portable 1.0.0.pre.alpha.1 → 1.0.0.pre.alpha.6

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: ab583e6beab687062935129db41650e353f23ec7ca2af9c610f49d8ac60a4811
4
+ data.tar.gz: 5999aab0a2a8bb37337d0923a2c06bffe9876dd8e8b4b879b799112090b4f393
5
5
  SHA512:
6
- metadata.gz: 0bb98a4e53e669c13fbf361d2b3e5be4572ce2b42ab99e43b555ecc8db7c89c25cd28b653b61bc1b3ab2ace6688886ba0f1d45f04d4d1bcb3b178d7183adad6a
7
- data.tar.gz: 14ac7eebf7b0af0b6553317e1d9f1f51757554ad43b9f0827a036ed12ff9ff9b67c5e2feb5db03566cce6ce43783ad8a24e0aed42a2a5367e306283531cb0214
6
+ metadata.gz: b485fead56c787bc169135b0c084289fcc690c49a9bface4f5d394c5494ed1139096ea5da5916001a2a845a1817c057f40d8d498d005288958e1792ebfa96b72
7
+ data.tar.gz: 93a3912588240c7c3172e06f47d8226627e50016ab9444a43c4d60aa536919542e8cbf35cb6e7369a226e15796b1b25e9af77e168dcb16a3c1a8a6193a8b481b
@@ -20,7 +20,7 @@ Metrics/MethodLength:
20
20
  Max: 30
21
21
 
22
22
  Metrics/AbcSize:
23
- Max: 16
23
+ Max: 20
24
24
 
25
25
  Metrics/ClassLength:
26
26
  Max: 125
@@ -2,8 +2,6 @@ env:
2
2
  global:
3
3
  - CC_TEST_REPORTER_ID=f40f0e6f9946420b05b247f1640b2f5fcef181ca86659a2e71c747f790fcecdd
4
4
  language: ruby
5
- services:
6
- - mysql
7
5
  rvm:
8
6
  # Build on the latest stable of all supported Rubies (https://www.ruby-lang.org/en/downloads/):
9
7
  - 2.5.8
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
 
@@ -8,6 +8,7 @@
8
8
  #
9
9
 
10
10
  require 'acts_as_hashable'
11
+ require 'benchmark'
11
12
  require 'csv'
12
13
  require 'fileutils'
13
14
  require 'forwardable'
@@ -15,7 +16,11 @@ require 'objectable'
15
16
  require 'realize'
16
17
  require 'time'
17
18
 
18
- require_relative 'portable/data_table'
19
+ # Shared modules/classes
20
+ require_relative 'portable/util'
21
+
22
+ # Main implementation points
23
+ require_relative 'portable/data'
19
24
  require_relative 'portable/document'
20
- require_relative 'portable/transformer'
25
+ require_relative 'portable/rendering'
21
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,42 @@
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 Util::Pivotable
18
+ include Util::Uniqueness
19
+ acts_as_hashable
20
+
21
+ def initialize(data_sources: [])
22
+ sources = Source.array(data_sources)
23
+ @data_sources_by_name = pivot_by_name(sources)
24
+
25
+ assert_no_duplicate_names(sources)
26
+
27
+ freeze
28
+ end
29
+
30
+ # Fail hard if we cannot identify which data source to use. This should help prevent
31
+ # possible configuration issues (i.e. typos.)
32
+ def data_source(name)
33
+ data_sources_by_name[name.to_s] ||
34
+ raise(ArgumentError, "data source: '#{name}' cannot be found.")
35
+ end
36
+
37
+ private
38
+
39
+ attr_reader :data_sources_by_name
40
+ end
41
+ end
42
+ 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,29 +7,43 @@
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
16
+ include Util::Pivotable
17
+ include Util::Uniqueness
13
18
  acts_as_hashable
14
- extend Forwardable
15
-
16
- attr_reader :data_table,
17
- :footer_rows,
18
- :header_rows
19
19
 
20
- def_delegators :data_table,
21
- :columns,
22
- :headers,
23
- :include_headers?,
24
- :headers,
25
- :transform
20
+ attr_reader :options
26
21
 
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 || []
22
+ def initialize(sheets: [], options: {})
23
+ @sheets_by_name = make_unique_sheets_by_name(sheets)
24
+ @options = Modeling::Options.make(options, nullable: false)
31
25
 
32
26
  freeze
33
27
  end
28
+
29
+ def sheet(name)
30
+ sheets_by_name.fetch(name.to_s)
31
+ end
32
+
33
+ def sheets
34
+ sheets_by_name.values
35
+ end
36
+
37
+ private
38
+
39
+ attr_reader :sheets_by_name
40
+
41
+ def make_unique_sheets_by_name(sheets)
42
+ sheets = Modeling::Sheet.array(sheets)
43
+ sheets << Modeling::Sheet.new if sheets.empty?
44
+
45
+ assert_no_duplicate_names(sheets)
46
+ pivot_by_name(sheets)
47
+ end
34
48
  end
35
49
  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,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
+ 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 :auto, :columns, :include_headers
20
+
21
+ alias include_headers? include_headers
22
+ alias auto? auto
23
+
24
+ def initialize(auto: true, columns: [], include_headers: true)
25
+ @auto = auto || false
26
+ @columns = Column.array(columns)
27
+ @include_headers = include_headers || false
28
+
29
+ freeze
30
+ end
31
+ end
32
+ end
33
+ 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,62 @@
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
+ extend Forwardable
21
+
22
+ def_delegators :data_table,
23
+ :auto?,
24
+ :columns,
25
+ :include_headers?
26
+
27
+ attr_reader :data_source_name,
28
+ :data_table,
29
+ :footer_rows,
30
+ :header_rows,
31
+ :name
32
+
33
+ def initialize(
34
+ data_source_name: '',
35
+ data_table: nil,
36
+ footer_rows: [],
37
+ header_rows: [],
38
+ name: ''
39
+ )
40
+ @data_source_name = decide_data_source_name(data_source_name, name)
41
+ @name = name.to_s
42
+ @data_table = DataTable.make(data_table, nullable: false)
43
+ @footer_rows = footer_rows || []
44
+ @header_rows = header_rows || []
45
+
46
+ freeze
47
+ end
48
+
49
+ # Use exact name if possible, if not then use the sheet name or else use the
50
+ # "default" one (noted by a blank name).
51
+ def decide_data_source_name(data_source_name, sheet_name)
52
+ if !data_source_name.to_s.empty?
53
+ data_source_name.to_s
54
+ elsif !sheet_name.to_s.empty?
55
+ sheet_name.to_s
56
+ else
57
+ ''
58
+ end
59
+ end
60
+ end
61
+ end
62
+ 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,46 @@
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, :resolver
16
+
17
+ def initialize(columns, resolver: Objectable.resolver)
18
+ @resolver = resolver
19
+
20
+ @column_pipelines = columns.each_with_object({}) do |column, memo|
21
+ memo[column] = Realize::Pipeline.new(column.transformers, resolver: resolver)
22
+ end
23
+
24
+ freeze
25
+ end
26
+
27
+ def render(object, time)
28
+ column_pipelines.each_with_object({}) do |(column, pipeline), memo|
29
+ memo[column.header] = pipeline.transform(object, time)
30
+ end
31
+ end
32
+
33
+ def columns
34
+ column_pipelines.keys
35
+ end
36
+
37
+ def headers
38
+ columns.map(&:header)
39
+ end
40
+
41
+ def merge(other)
42
+ self.class.new(columns + other.columns, resolver: resolver)
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright (c) 2020-present, Blue Marble Payroll, LLC
5
+ #
6
+ # This source code is licensed under the MIT license found in the
7
+ # LICENSE file in the root directory of this source tree.
8
+ #
9
+
10
+ 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
+ memo[sheet.name] = Row.new(sheet.columns, resolver: resolver)
25
+ end
26
+
27
+ freeze
28
+ end
29
+
30
+ def row_renderer(sheet_name, fields)
31
+ sheet = document.sheet(sheet_name)
32
+ row_renderer = row_renderers.fetch(sheet_name.to_s)
33
+
34
+ return row_renderer unless sheet.auto?
35
+
36
+ dynamic_row_renderer(fields).merge(row_renderer)
37
+ end
38
+
39
+ private
40
+
41
+ attr_reader :row_renderers
42
+
43
+ def fields_to_columns(fields)
44
+ fields = (fields || []).map { |f| { header: f.to_s } }
45
+ Modeling::Column.array(fields)
46
+ end
47
+
48
+ def dynamic_row_renderer(fields)
49
+ Row.new(fields_to_columns(fields), resolver: resolver)
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,11 @@
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 'util/pivotable'
11
+ require_relative 'util/uniqueness'
@@ -0,0 +1,19 @@
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 Util # :nodoc: all
12
+ # Mixes in helpers for asserting uniqueness across collections
13
+ module Pivotable
14
+ def pivot_by_name(array)
15
+ array.each_with_object({}) { |object, memo| memo[object.name] = object }
16
+ end
17
+ end
18
+ end
19
+ end
@@ -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 Util # :nodoc: all
12
+ # Mixes in helpers for asserting uniqueness across collections
13
+ module Uniqueness
14
+ class DuplicateNameError < StandardError; end
15
+
16
+ def assert_no_duplicate_names(array)
17
+ names = array.map { |a| a.name.downcase }
18
+
19
+ return if names.uniq.length == array.length
20
+
21
+ raise DuplicateNameError, "cannot contain duplicate names: #{names}"
22
+ end
23
+ end
24
+ end
25
+ 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.6'
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,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.data_source_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
@@ -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.6
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-10 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,26 @@ 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
188
+ - lib/portable/util.rb
189
+ - lib/portable/util/pivotable.rb
190
+ - lib/portable/util/uniqueness.rb
184
191
  - lib/portable/version.rb
185
192
  - lib/portable/writers.rb
193
+ - lib/portable/writers/base.rb
194
+ - lib/portable/writers/csv.rb
195
+ - lib/portable/writers/result.rb
186
196
  - portable.gemspec
187
197
  homepage: https://github.com/bluemarblepayroll/portable
188
198
  licenses:
@@ -211,5 +221,5 @@ requirements: []
211
221
  rubygems_version: 3.0.3
212
222
  signing_key:
213
223
  specification_version: 4
214
- summary: Transformable export writer
224
+ summary: Virtual Document Modeling and Rendering Engine
215
225
  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