portable 1.0.0.pre.alpha → 1.0.0.pre.alpha.5

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