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

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: 112f223aca7d26dda5b4e9c93b1e83174885e941806de503c2d15be37047cdc1
4
+ data.tar.gz: 7d9764151388559dd0ac999b69fa4bed16ff9d02fdb796944ff4d22b30b5056d
5
5
  SHA512:
6
- metadata.gz: 1497db394e1dab96acbe34327a683e5e3390f0912461c8ae2e312def8996b7add5f025012e9871ccb3193014d123e23ba2993625af9a462e86756f00d00b1550
7
- data.tar.gz: 5e0ea223a2d1894075e6fcf07b0eecd6a7242e37263ba7bea4e01ffefe22f95c7bcaad18fef35e74ae4b7435efc8012636dcb1e0d10a349c802327726a65dcf1
6
+ metadata.gz: 0bb98a4e53e669c13fbf361d2b3e5be4572ce2b42ab99e43b555ecc8db7c89c25cd28b653b61bc1b3ab2ace6688886ba0f1d45f04d4d1bcb3b178d7183adad6a
7
+ data.tar.gz: 14ac7eebf7b0af0b6553317e1d9f1f51757554ad43b9f0827a036ed12ff9ff9b67c5e2feb5db03566cce6ce43783ad8a24e0aed42a2a5367e306283531cb0214
data/README.md CHANGED
@@ -31,7 +31,7 @@ bundle add portable
31
31
 
32
32
  ## Examples
33
33
 
34
- ### Getting Started with Exports
34
+ ### Getting Started Writing CSV Files
35
35
 
36
36
  Consider the following data set as an array of hashes:
37
37
 
@@ -45,26 +45,24 @@ patients = [
45
45
  We could configure an export like so:
46
46
 
47
47
  ````ruby
48
- export = {
49
- columns: [
50
- { header: :first },
51
- { header: :last },
52
- { header: :dob }
53
- ]
48
+ document = {
49
+ data_table: {
50
+ columns: [
51
+ { header: :first },
52
+ { header: :last },
53
+ { header: :dob }
54
+ ]
55
+ }
54
56
  }
55
57
  ````
56
58
 
57
59
  And execute the export against the example dataset in order to generate a CSV file:
58
60
 
59
61
  ````ruby
60
- writer = Portable::Writer.new(export)
62
+ writer = Portable::Csv::Writer.new(export)
61
63
  filename = File.join('tmp', 'patients.csv')
62
64
 
63
- writer.open(filename) do |writer|
64
- patients.each do |patient|
65
- writer.write(object: patient)
66
- end
67
- end
65
+ writer.open(filename) { |writer| writer.write_all(patients) }
68
66
  ````
69
67
 
70
68
  We should now have a CSV file at tmp/patients.csv that looks like this:
@@ -78,31 +76,33 @@ Frank | Rizzo | 1930-09-22
78
76
 
79
77
  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
78
 
81
- Let's expand our example above with different headers and date formatting:
79
+ Let's expand our CSV example above with different headers and date formatting:
82
80
 
83
81
  ````ruby
84
- export = {
85
- columns: [
86
- {
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
- ]
104
- }
105
- ]
82
+ 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' },
102
+ ]
103
+ }
104
+ ]
105
+ }
106
106
  }
107
107
  ````
108
108
 
@@ -115,6 +115,71 @@ Frank | Rizzo | 09/22/1930
115
115
 
116
116
  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
117
 
118
+ ### Options
119
+
120
+ Each writer can have its own set of options.
121
+
122
+ #### CSV Options
123
+
124
+ The following options are available for customizing CSV documents:
125
+
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.
127
+
128
+ ### Custom Header/Footer Rows
129
+
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:
131
+
132
+ ````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
+ }
155
+ ]
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
+ ],
166
+ }
167
+ ````
168
+
169
+ 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:
170
+
171
+ ````
172
+ Run Date | 04/05/2000
173
+ Run By | Hops the Bunny
174
+
175
+ BEGIN
176
+ First Name | Last Name | Date of Birth
177
+ ---------- | --------- | -------------
178
+ Marky | Mark | 04/05/2000
179
+ Frank | Rizzo | 09/22/1930
180
+ END
181
+ ````
182
+
118
183
  ## Contributing
119
184
 
120
185
  ### Development Environment Configuration
@@ -10,8 +10,12 @@
10
10
  require 'acts_as_hashable'
11
11
  require 'csv'
12
12
  require 'fileutils'
13
+ require 'forwardable'
13
14
  require 'objectable'
14
15
  require 'realize'
15
16
  require 'time'
16
17
 
17
- require_relative 'portable/writer'
18
+ require_relative 'portable/data_table'
19
+ require_relative 'portable/document'
20
+ require_relative 'portable/transformer'
21
+ require_relative 'portable/writers'
@@ -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 Csv
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,34 @@
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
@@ -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 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 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,109 @@
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
@@ -10,30 +10,30 @@
10
10
  require_relative 'column'
11
11
 
12
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
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
16
  acts_as_hashable
17
17
 
18
- module Bom
19
- UTF8 = "\uFEFF"
20
- end
21
- include Bom
22
-
23
- attr_reader :bom, :columns, :include_headers
18
+ attr_reader :columns
24
19
 
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
20
+ def initialize(columns: [], include_headers: true)
29
21
  @columns = Column.array(columns)
30
22
  @include_headers = include_headers || false
31
23
 
32
24
  freeze
33
25
  end
34
26
 
27
+ def include_headers?
28
+ include_headers
29
+ end
30
+
35
31
  def headers
36
32
  columns.map(&:header)
37
33
  end
34
+
35
+ private
36
+
37
+ attr_reader :include_headers
38
38
  end
39
39
  end
@@ -0,0 +1,35 @@
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
+ # Base document object model defining what all documents should include.
12
+ class Document
13
+ acts_as_hashable
14
+ extend Forwardable
15
+
16
+ attr_reader :data_table,
17
+ :footer_rows,
18
+ :header_rows
19
+
20
+ def_delegators :data_table,
21
+ :columns,
22
+ :headers,
23
+ :include_headers?,
24
+ :headers,
25
+ :transform
26
+
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 || []
31
+
32
+ freeze
33
+ end
34
+ end
35
+ end
@@ -7,8 +7,6 @@
7
7
  # LICENSE file in the root directory of this source tree.
8
8
  #
9
9
 
10
- require_relative 'export'
11
-
12
10
  module Portable
13
11
  # Internal intermediary class that knows how to combine columns specification instances with their
14
12
  # respective Realize pipelines.
@@ -8,5 +8,5 @@
8
8
  #
9
9
 
10
10
  module Portable
11
- VERSION = '1.0.0-alpha'
11
+ VERSION = '1.0.0-alpha.1'
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 'csv/writer'
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.1
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-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: acts_as_hashable
@@ -174,10 +174,15 @@ files:
174
174
  - exe/.gitkeep
175
175
  - lib/portable.rb
176
176
  - lib/portable/column.rb
177
- - lib/portable/export.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
182
+ - lib/portable/document.rb
178
183
  - lib/portable/transformer.rb
179
184
  - lib/portable/version.rb
180
- - lib/portable/writer.rb
185
+ - lib/portable/writers.rb
181
186
  - portable.gemspec
182
187
  homepage: https://github.com/bluemarblepayroll/portable
183
188
  licenses:
@@ -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