portable 1.0.0.pre.alpha.2 → 1.0.0.pre.alpha.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fe43368608c03ce86c76422bdd7cdaba14cc7df66452780ff8af6aedc3c3c411
4
- data.tar.gz: d90e21a8382237941ec558890ccd5b731734a4548baa054b78832989102141eb
3
+ metadata.gz: e9a80aeca8e54c0e02bf414ee6c79b6e110e126a57fc64a3a9d3f8990481f940
4
+ data.tar.gz: 7ee0c481c4d15148857726d3644230a76a52f8ae178425c5e1c5defbc485c81b
5
5
  SHA512:
6
- metadata.gz: 076c5e70d9ef53d132dfea0691917afc5776bc5e9322ee50d56284f22eb37e7ce035d09218def74b7a702dcf5cea312e8cbab960932dcfe09a70fad9e52c534d
7
- data.tar.gz: a726ceb297e8109de9996d2a9dde20561e843a83fc2a8ea9393acfe780eddc3e2c42ede6cb373c84e246292342655083bea6698ccd37c3b31a779fbe69db0863
6
+ metadata.gz: 2e45f20154ff8471104bcdbd5fbbc32d8cf3c892606c077f418b3d29456c8221c080bc7ae2719f63920e54a8f139ad62b2104ecfee1a39699d54467134f6a9be
7
+ data.tar.gz: d11bdfc431b996f82b8f6b14f53862ff98236ad08f6c7e79d87fdd3153137c42bc71687506bf2bfa544a9c1a62e745ba9658a8e1ded37eba3e9e705cb173ed49
@@ -20,7 +20,7 @@ Metrics/MethodLength:
20
20
  Max: 30
21
21
 
22
22
  Metrics/AbcSize:
23
- Max: 17
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
@@ -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,6 +16,10 @@ require 'objectable'
15
16
  require 'realize'
16
17
  require 'time'
17
18
 
19
+ # Shared modules/classes
20
+ require_relative 'portable/util'
21
+
22
+ # Main implementation points
18
23
  require_relative 'portable/data'
19
24
  require_relative 'portable/document'
20
25
  require_relative 'portable/rendering'
@@ -14,27 +14,29 @@ module Portable
14
14
  # Container of data sources that is inputted into a writer alongside a document.
15
15
  # It contains all the data sources the writer will use to render a document.
16
16
  class Provider
17
+ include Util::Pivotable
18
+ include Util::Uniqueness
17
19
  acts_as_hashable
18
20
 
19
21
  def initialize(data_sources: [])
20
- @data_sources_by_name = pivot_by_name(Source.array(data_sources))
22
+ sources = Source.array(data_sources)
23
+ @data_sources_by_name = pivot_by_name(sources)
24
+
25
+ assert_no_duplicate_names(sources)
21
26
 
22
27
  freeze
23
28
  end
24
29
 
30
+ # Fail hard if we cannot identify which data source to use. This should help prevent
31
+ # possible configuration issues (i.e. typos.)
25
32
  def data_source(name)
26
- data_sources_by_name.fetch(name.to_s, Source.new)
33
+ data_sources_by_name[name.to_s] ||
34
+ raise(ArgumentError, "data source: '#{name}' cannot be found.")
27
35
  end
28
36
 
29
37
  private
30
38
 
31
39
  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
40
  end
39
41
  end
40
42
  end
@@ -13,16 +13,37 @@ require_relative 'modeling/sheet'
13
13
  module Portable
14
14
  # Top-level object model for a renderable document.
15
15
  class Document
16
+ include Util::Pivotable
17
+ include Util::Uniqueness
16
18
  acts_as_hashable
17
19
 
18
- attr_reader :sheets, :options
20
+ attr_reader :options
19
21
 
20
22
  def initialize(sheets: [], options: {})
21
- @sheets = Modeling::Sheet.array(sheets)
22
- @sheets << Modeling::Sheet.new if @sheets.empty?
23
- @options = Modeling::Options.make(options)
23
+ @sheets_by_name = make_unique_sheets_by_name(sheets)
24
+ @options = Modeling::Options.make(options, nullable: false)
24
25
 
25
26
  freeze
26
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
27
48
  end
28
49
  end
@@ -16,10 +16,15 @@ module Portable
16
16
  class DataTable
17
17
  acts_as_hashable
18
18
 
19
- attr_reader :columns
19
+ attr_reader :auto, :columns, :include_headers
20
20
 
21
- def initialize(columns: [])
22
- @columns = Column.array(columns)
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
23
28
 
24
29
  freeze
25
30
  end
@@ -17,31 +17,45 @@ module Portable
17
17
  # one file.
18
18
  class Sheet
19
19
  acts_as_hashable
20
+ extend Forwardable
20
21
 
21
- attr_reader :data_table,
22
+ def_delegators :data_table,
23
+ :auto?,
24
+ :columns,
25
+ :include_headers?
26
+
27
+ attr_reader :data_source_name,
28
+ :data_table,
22
29
  :footer_rows,
23
30
  :header_rows,
24
- :name,
25
- :include_headers
31
+ :name
26
32
 
27
33
  def initialize(
34
+ data_source_name: '',
28
35
  data_table: nil,
29
36
  footer_rows: [],
30
37
  header_rows: [],
31
- name: '',
32
- include_headers: true
38
+ name: ''
33
39
  )
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
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 || []
39
45
 
40
46
  freeze
41
47
  end
42
48
 
43
- def include_headers?
44
- include_headers
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
45
59
  end
46
60
  end
47
61
  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
+ module Portable
11
+ module Rendering
12
+ class Column # :nodoc: all
13
+ extend Forwardable
14
+
15
+ attr_reader :column, :pipeline
16
+
17
+ def_delegators :column, :header
18
+
19
+ def_delegators :pipeline, :transform
20
+
21
+ def initialize(column, resolver: Objectable.resolver)
22
+ raise ArgumentError, 'column is required' unless column
23
+
24
+ @column = column
25
+ @pipeline = Realize::Pipeline.new(column.transformers, resolver: resolver)
26
+
27
+ freeze
28
+ end
29
+ end
30
+ end
31
+ end
@@ -7,34 +7,37 @@
7
7
  # LICENSE file in the root directory of this source tree.
8
8
  #
9
9
 
10
+ require_relative 'column'
11
+
10
12
  module Portable
11
13
  module Rendering
12
14
  # Internal intermediary class that knows how to combine columns specification
13
15
  # instances with their respective Realize pipelines.
14
16
  class Row # :nodoc: all
15
- attr_reader :column_pipelines
17
+ attr_reader :columns, :resolver
16
18
 
17
19
  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
20
+ @resolver = resolver
21
+ @columns = columns.map { |column| Column.new(column, resolver: resolver) }
21
22
 
22
23
  freeze
23
24
  end
24
25
 
25
26
  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
27
+ columns.map { |column| column.transform(object, time) }
33
28
  end
34
29
 
35
30
  def headers
36
31
  columns.map(&:header)
37
32
  end
33
+
34
+ def model_columns
35
+ columns.map(&:column)
36
+ end
37
+
38
+ def merge(other)
39
+ self.class.new(model_columns + other.model_columns, resolver: resolver)
40
+ end
38
41
  end
39
42
  end
40
43
  end
@@ -21,27 +21,32 @@ module Portable
21
21
  @resolver = resolver
22
22
 
23
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)
24
+ memo[sheet.name] = Row.new(sheet.columns, resolver: resolver)
27
25
  end
28
26
 
29
27
  freeze
30
28
  end
31
29
 
32
30
  def row_renderer(sheet_name, fields)
33
- row_renderers.fetch(sheet_name, dynamic_row_renderer(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)
34
37
  end
35
38
 
36
39
  private
37
40
 
38
41
  attr_reader :row_renderers
39
42
 
40
- def dynamic_row_renderer(fields)
41
- fields = (fields || []).map { |f| { header: f.to_s } }
42
- columns = Modeling::Column.array(fields)
43
+ def fields_to_columns(fields)
44
+ fields = (fields || []).map { |f| { header: f.to_s } }
45
+ Modeling::Column.array(fields)
46
+ end
43
47
 
44
- Row.new(columns, resolver: resolver)
48
+ def dynamic_row_renderer(fields)
49
+ Row.new(fields_to_columns(fields), resolver: resolver)
45
50
  end
46
51
  end
47
52
  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.2'
11
+ VERSION = '1.0.0-alpha.7'
12
12
  end
@@ -20,6 +20,14 @@ module Portable
20
20
 
21
21
  freeze
22
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
23
31
  end
24
32
  end
25
33
  end
@@ -8,6 +8,7 @@
8
8
  #
9
9
 
10
10
  require_relative 'base'
11
+ require_relative 'result'
11
12
 
12
13
  module Portable
13
14
  module Writers
@@ -18,16 +19,18 @@ module Portable
18
19
 
19
20
  ensure_directory_exists(filename)
20
21
 
21
- sheet_filenames = extrapolate_filenames(filename)
22
+ sheet_filenames = extrapolate_filenames(filename, document.sheets.length)
22
23
 
23
- document.sheets.each do |sheet|
24
- data_source = data_provider.data_source(sheet.name)
25
- sheet_filename = sheet_filenames[sheet.name]
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]
26
27
 
27
- write_sheet(sheet_filename, sheet, data_source, time)
28
- end
28
+ time_in_seconds = Benchmark.measure do
29
+ write_sheet(sheet_filename, sheet, data_source, time)
30
+ end.real
29
31
 
30
- sheet_filenames.values
32
+ Result.new(sheet_filename, time_in_seconds)
33
+ end
31
34
  end
32
35
 
33
36
  private
@@ -54,7 +57,7 @@ module Portable
54
57
  csv << row_renderer.headers if sheet.include_headers?
55
58
 
56
59
  data_source.data_rows.each do |row|
57
- csv << row_renderer.render(row, time).values
60
+ csv << row_renderer.render(row, time)
58
61
  end
59
62
  end
60
63
 
@@ -64,27 +67,17 @@ module Portable
64
67
  sheet.footer_rows.each { |row| csv << row }
65
68
  end
66
69
 
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
70
+ def extrapolate_filenames(filename, count)
75
71
  dir = File.dirname(filename)
76
72
  ext = File.extname(filename)
77
73
  basename = File.basename(filename, ext)
78
74
 
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
75
+ (0..count).map do |index|
76
+ if index.positive?
77
+ File.join(dir, "#{basename}.#{index}#{ext}")
78
+ else
79
+ filename
80
+ end
88
81
  end
89
82
  end
90
83
  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
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.2
4
+ version: 1.0.0.pre.alpha.7
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-07 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
@@ -183,12 +183,17 @@ files:
183
183
  - lib/portable/modeling/options.rb
184
184
  - lib/portable/modeling/sheet.rb
185
185
  - lib/portable/rendering.rb
186
+ - lib/portable/rendering/column.rb
186
187
  - lib/portable/rendering/row.rb
187
188
  - lib/portable/rendering/sheet.rb
189
+ - lib/portable/util.rb
190
+ - lib/portable/util/pivotable.rb
191
+ - lib/portable/util/uniqueness.rb
188
192
  - lib/portable/version.rb
189
193
  - lib/portable/writers.rb
190
194
  - lib/portable/writers/base.rb
191
195
  - lib/portable/writers/csv.rb
196
+ - lib/portable/writers/result.rb
192
197
  - portable.gemspec
193
198
  homepage: https://github.com/bluemarblepayroll/portable
194
199
  licenses: