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

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: 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: