excel_templating 0.3.2

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.
Files changed (59) hide show
  1. checksums.yaml +7 -0
  2. data/.document +3 -0
  3. data/.gitignore +4 -0
  4. data/.rspec +1 -0
  5. data/.rubocop.hound.yml +261 -0
  6. data/.rubocop.ph.yml +44 -0
  7. data/.rubocop.yml +3 -0
  8. data/.yardopts +1 -0
  9. data/ChangeLog.md +8 -0
  10. data/Gemfile +10 -0
  11. data/LICENSE.txt +3 -0
  12. data/README.md +133 -0
  13. data/Rakefile +43 -0
  14. data/excel_templating.gemspec +32 -0
  15. data/lib/excel_templating/document/data_source_registry/registry_list.rb +48 -0
  16. data/lib/excel_templating/document/data_source_registry/registry_renderer.rb +74 -0
  17. data/lib/excel_templating/document/data_source_registry.rb +64 -0
  18. data/lib/excel_templating/document/sheet/repeated_row.rb +39 -0
  19. data/lib/excel_templating/document/sheet.rb +133 -0
  20. data/lib/excel_templating/document.rb +71 -0
  21. data/lib/excel_templating/document_dsl.rb +85 -0
  22. data/lib/excel_templating/excel_abstraction/active_cell_reference.rb +59 -0
  23. data/lib/excel_templating/excel_abstraction/cell.rb +23 -0
  24. data/lib/excel_templating/excel_abstraction/cell_range.rb +26 -0
  25. data/lib/excel_templating/excel_abstraction/cell_reference.rb +39 -0
  26. data/lib/excel_templating/excel_abstraction/date.rb +36 -0
  27. data/lib/excel_templating/excel_abstraction/row.rb +29 -0
  28. data/lib/excel_templating/excel_abstraction/sheet.rb +102 -0
  29. data/lib/excel_templating/excel_abstraction/spread_sheet.rb +28 -0
  30. data/lib/excel_templating/excel_abstraction/time.rb +42 -0
  31. data/lib/excel_templating/excel_abstraction/work_book.rb +47 -0
  32. data/lib/excel_templating/excel_abstraction.rb +16 -0
  33. data/lib/excel_templating/render_helper.rb +14 -0
  34. data/lib/excel_templating/renderer.rb +251 -0
  35. data/lib/excel_templating/rspec_excel_matcher.rb +129 -0
  36. data/lib/excel_templating/version.rb +4 -0
  37. data/lib/excel_templating.rb +4 -0
  38. data/spec/assets/alphalist_7_4.mustache.xlsx +0 -0
  39. data/spec/assets/alphalist_seven_four_expected.xlsx +0 -0
  40. data/spec/assets/valid_cell.mustache.xlsx +0 -0
  41. data/spec/assets/valid_cell_expected.xlsx +0 -0
  42. data/spec/assets/valid_cell_expected_inline.xlsx +0 -0
  43. data/spec/assets/valid_column_expected.xlsx +0 -0
  44. data/spec/cell_validation_spec.rb +114 -0
  45. data/spec/column_validation_spec.rb +47 -0
  46. data/spec/excel_abstraction/active_cell_reference_spec.rb +73 -0
  47. data/spec/excel_abstraction/cell_range_spec.rb +36 -0
  48. data/spec/excel_abstraction/cell_reference_spec.rb +69 -0
  49. data/spec/excel_abstraction/cell_spec.rb +54 -0
  50. data/spec/excel_abstraction/date_spec.rb +27 -0
  51. data/spec/excel_abstraction/row_spec.rb +42 -0
  52. data/spec/excel_abstraction/sheet_spec.rb +83 -0
  53. data/spec/excel_abstraction/spread_sheet_spec.rb +35 -0
  54. data/spec/excel_abstraction/time_spec.rb +27 -0
  55. data/spec/excel_abstraction/work_book_spec.rb +22 -0
  56. data/spec/excel_helper.rb +16 -0
  57. data/spec/excel_templating_spec.rb +141 -0
  58. data/spec/spec_helper.rb +13 -0
  59. metadata +281 -0
@@ -0,0 +1,64 @@
1
+ module ExcelTemplating
2
+ # A registry for validation data sources within the excel spreadsheet DSL
3
+ # Supports Enumerable#each for iterating the registry entries.
4
+ class Document::DataSourceRegistry
5
+ include Enumerable
6
+ extend Forwardable
7
+
8
+ # Create an empty DataSourceRegistry
9
+ def initialize
10
+ @source_symbols = {}
11
+ end
12
+
13
+ # @param [Hash] data
14
+ # @return [RegistryRenderer]
15
+ def renderer(data:)
16
+ RegistryRenderer.new(self, data)
17
+ end
18
+
19
+ # @param [Symbol] source_symbol
20
+ # @param [String] title
21
+ # @param [Array<String>|Symbol] list
22
+ # @param [TrueClass|FalseClass] inline
23
+ def add_list(source_symbol, title, list, inline)
24
+ source_symbols[source_symbol] = RegistryList.new(source_symbols.size + 1, source_symbol, title, list, inline)
25
+ end
26
+
27
+ # @param [Symbol] source_symbol
28
+ # @return [RegistryList]
29
+ def [](source_symbol)
30
+ source_symbols[source_symbol]
31
+ end
32
+
33
+ # @param [Symbol] source_symbol
34
+ def has_registry?(source_symbol)
35
+ source_symbols.has_key?(source_symbol)
36
+ end
37
+
38
+ # @return [TrueClass|FalseClass]
39
+ def any_data_sheet_symbols?
40
+ select {|info|
41
+ info.data_sheet?
42
+ }.any?
43
+ end
44
+
45
+ # @return [Array<Symbol>]
46
+ def supported_registries
47
+ source_symbols.keys
48
+ end
49
+
50
+ delegate [:each] => :ordered_registries
51
+
52
+ private
53
+
54
+ attr_reader :source_symbols
55
+
56
+ def ordered_registries
57
+ source_symbols.values.sort_by(&:order)
58
+ end
59
+
60
+ end
61
+ end
62
+
63
+ require_relative 'data_source_registry/registry_renderer'
64
+ require_relative 'data_source_registry/registry_list'
@@ -0,0 +1,39 @@
1
+ module ExcelTemplating
2
+ # Simple class for representing a repeated row on a sheet.
3
+ class Document::Sheet::RepeatedRow
4
+ # @param [Integer] row_number
5
+ # @param [Symol] data_attribute
6
+ def initialize(row_number, data_attribute)
7
+ @row_number = row_number
8
+ @data_attribute = data_attribute
9
+ @column_validations = {}
10
+ end
11
+
12
+ ### Dsl Methods ###
13
+
14
+ # Validate a particular row in a repeated set as being part of a declared data source
15
+ # @example
16
+ # validate_column 5, with: :valid_foos
17
+ # @param [Integer] column_number
18
+ # @param [Symbol] with
19
+ def validate_column(column_number, with:)
20
+ @column_validations[column_number] = with
21
+ end
22
+
23
+ ### Non Dsl Methods ###
24
+
25
+ attr_reader :row_number, :data_attribute
26
+
27
+ # @param [Integer] column_number
28
+ # @return [Symbol] Registered source at that column
29
+ def validated_column_source(column_number)
30
+ @column_validations[column_number]
31
+ end
32
+
33
+ # @param [Integer] column_number
34
+ # @return [TrueClass|FalseClass]
35
+ def validated_column?(column_number)
36
+ @column_validations.has_key?(column_number)
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,133 @@
1
+ module ExcelTemplating
2
+ # Define a sheet on a document
3
+ # @example
4
+ # sheet 1 do
5
+ # repeat_row 17, with: :people
6
+ # end
7
+ class Document::Sheet
8
+ # @param [Integer] sheet_number
9
+ def initialize(sheet_number)
10
+ @sheet_number = sheet_number
11
+ @repeated_rows = {}
12
+ @validated_cells = {}
13
+ end
14
+
15
+ ### Sheet Dsl Methods ####
16
+
17
+ # @param [Float] decimal_inches
18
+ # @return [Float] inches converted to excel integer size.
19
+ def inches(decimal_inches)
20
+ # empirically determined number. 30.0 seems to be the measurement for 2.6 inches
21
+ # in open office.
22
+ (30.0 / 2.6) * decimal_inches
23
+ end
24
+
25
+ # @param [Hash] default default styling for all columns.
26
+ # @param [Hash] columns specific styling for numbered columns.
27
+ def style_columns(default:, columns: {})
28
+ @default_column_style = default
29
+ @column_styles = columns
30
+ end
31
+
32
+ # @param [Hash] default default styling for all rows.
33
+ # @param [Hash] rows specific styling for numbered rows.
34
+ def style_rows(default:, rows: {})
35
+ @default_row_style = default
36
+ @row_styles = rows
37
+ end
38
+
39
+ # Repeat a numbered row in the template using an array from the data
40
+ # will result in expanding the produced excel document by a number of rows.
41
+ # it is expected that the sheet specific data will contain :with as an Array.
42
+ # @example
43
+ # repeat_row 17, with: :employee_data
44
+ # @param [Integer] row_number
45
+ # @param [Symbol] with
46
+ def repeat_row(row_number, with:, &block)
47
+ repeated_rows[row_number] = RepeatedRow.new(row_number, with)
48
+ repeated_rows[row_number].instance_eval(&block) if block_given?
49
+ end
50
+
51
+ # Validate a particular cell using a declared data source
52
+ # @example
53
+ # validate_cell row: 1, column :5, with: :valid_foos
54
+ # @param [Integer] row
55
+ # @param [Integer] column
56
+ # @param [Symbol] with
57
+ def validate_cell(row:, column:, with:)
58
+ validated_cells["#{row}:#{column}"] = with
59
+ end
60
+
61
+ #### Non DSL Methods ###
62
+
63
+ def default_column_style
64
+ @default_column_style || {}
65
+ end
66
+
67
+ def column_styles
68
+ @column_styles || {}
69
+ end
70
+
71
+ def default_row_style
72
+ @default_row_style || {}
73
+ end
74
+
75
+ def row_styles
76
+ @row_styles || {}
77
+ end
78
+
79
+ def sheet_data(data)
80
+ data[sheet_number] || {}
81
+ end
82
+
83
+ # @param [Integer] row_number
84
+ def repeated_row?(row_number)
85
+ repeated_rows.has_key?(row_number)
86
+ end
87
+
88
+ # @param [Integer] row_number
89
+ # @param [Integer] column_number
90
+ def validated_cell?(row_number, column_number)
91
+ (repeated_row?(row_number) && repeated_rows[row_number].validated_column?(column_number)) ||
92
+ validated_cells.has_key?("#{row_number}:#{column_number}")
93
+ end
94
+
95
+ # @param [Integer] row_number
96
+ # @param [Integer] column_number
97
+ # @return [Symbol] The registered symbol for that row & column or Nil
98
+ def validation_source_name(row_number, column_number)
99
+ if repeated_row?(row_number)
100
+ repeated_rows[row_number].validated_column_source(column_number)
101
+ else
102
+ validated_cells["#{row_number}:#{column_number}"]
103
+ end
104
+ end
105
+
106
+ # Repeat each row of the data if it is repeated, yielding each item in succession.
107
+ # @param [Integer] row_number
108
+ # @param [Hash] sheet_data Data for this sheet
109
+ def each_row_at(row_number, sheet_data)
110
+ if repeated_row?(row_number)
111
+ repeater = repeated_rows[row_number]
112
+ verify_array!(sheet_data, repeater.data_attribute)
113
+ sheet_data[repeater.data_attribute].each_with_index do |row_data, index|
114
+ yield({ index: index }.merge(row_data).merge(sheet_data))
115
+ end
116
+ else
117
+ yield sheet_data
118
+ end
119
+ end
120
+
121
+ private
122
+
123
+ attr_reader :sheet_number, :repeated_rows, :validated_cells
124
+
125
+ def verify_array!(sheet_data, attribute)
126
+ unless sheet_data[attribute].is_a?(Array)
127
+ raise ArgumentError, "Data for sheet #{sheet_number} did not contain #{attribute} array as expected!"
128
+ end
129
+ end
130
+ end
131
+ end
132
+
133
+ require_relative 'sheet/repeated_row'
@@ -0,0 +1,71 @@
1
+ require_relative 'document_dsl'
2
+
3
+ module ExcelTemplating
4
+ # The base document class for an ExcelTemplating.
5
+ # Inherit from document to create your own ExcelTemplating that you may then use to generate
6
+ # Excel Spreadsheets from the template you supply.
7
+ # @example
8
+ # class MyTemplate < ExcelTemplating::Document
9
+ # template "my_template.mustache.xlsx"
10
+ # title "My title: {{my_company}}"
11
+ # organization "{{organization_name}}"
12
+ # default_styling(
13
+ # text_wrap: 0,
14
+ # font: "Calibri",
15
+ # size: 10,
16
+ # align: :left,
17
+ # )
18
+ # sheet 1 do
19
+ # repeat_row 17, with: :people
20
+ # end
21
+ # end
22
+ class Document
23
+ extend DocumentDsl
24
+ class << self
25
+ ## The non Dsl Methods, not expected to be used as part of the document description
26
+ # @return [String] The document title
27
+ def document_title
28
+ @document_title
29
+ end
30
+
31
+ # @return [String] The document organization
32
+ def document_organization
33
+ @document_organization
34
+ end
35
+
36
+ # @return [Hash] The default styling for the document
37
+ def document_default_styling
38
+ @default_styling || default_styling
39
+ end
40
+ end
41
+ # Create a new document with given data. 'all_sheets' is available to the template on each sheet.
42
+ # otherwise each numeric key in 'sheet_data' provides the data for that specific sheet.
43
+ # For example {all_sheets: {foo: 'bar'}, 1 => {var1: "foo"}}
44
+ # @param [Hash] data Hash with variables for rendering.
45
+ def initialize(data)
46
+ @data = data
47
+ end
48
+
49
+ # Render this template.
50
+ # @example
51
+ # instance = MyTemplate.new(all_sheets: {foo: 1},1 => {bar: "foo"})
52
+ # instance.render do |file_path|
53
+ # FileUtils.cp(file_path, somewhere_else)
54
+ # end
55
+ def render(&block)
56
+ new_renderer.render(&block)
57
+ end
58
+
59
+ attr_reader :data
60
+
61
+ private
62
+
63
+ def new_renderer
64
+ ExcelTemplating::Renderer.new(self)
65
+ end
66
+
67
+ end
68
+ end
69
+
70
+ require_relative 'document/sheet'
71
+ require_relative 'document/data_source_registry'
@@ -0,0 +1,85 @@
1
+ module ExcelTemplating
2
+ # The descriptor module for how to define your template class
3
+ module DocumentDsl
4
+ # @return [String] The template path.
5
+ def template_path
6
+ @template_path
7
+ end
8
+
9
+ # @param [String] path Set the path to the template for this document class.
10
+ def template(path)
11
+ raise ArgumentError, "Template path must be a string." unless path.is_a?(String)
12
+ @template_path = path
13
+ end
14
+
15
+ # @return [Array<ExcelTemplating::Document::Sheet>] The sheets defined for this document class.
16
+ def sheets
17
+ @sheets ||= []
18
+ end
19
+
20
+ # Define a sheet on this document
21
+ # @example
22
+ # sheet 1 do
23
+ # repeat_row 17, with: :people
24
+ # end
25
+ # @param [Integer] sheet_number
26
+ # @param [Proc] block
27
+ def sheet(sheet_number, &block)
28
+ sheet = ExcelTemplating::Document::Sheet.new(sheet_number)
29
+ sheets << sheet
30
+ sheet.instance_eval(&block)
31
+ nil
32
+ end
33
+
34
+ def protect_document(protect=true)
35
+ @protected = protect
36
+ end
37
+
38
+ def protected?
39
+ !!@protected
40
+ end
41
+
42
+ # Add a list validator to the excel document
43
+ # @example
44
+ # list_source :valid_foos, title: "Valid Foos", list: ['foo','bar'], inline: false
45
+ # @param [Symbol] source_symbol symbol to registry for the validator
46
+ # @param [String] title Title to show when displaying this validator
47
+ # @param [Array<String>|Symbol] list items to use for validation, you may also use :from_data and at render time
48
+ # the validation items will be fetched from key 'source_symbol'
49
+ # @param [TrueClass|FalseClass] inline If true then the validator will be written to the document inline.
50
+ # Otherwise it will be written to a 'DataSheet'
51
+ def list_source(source_symbol, title:, list: :from_data, inline: false)
52
+ data_source_registry.add_list(source_symbol, title, list, inline)
53
+ end
54
+
55
+ def data_source_registry
56
+ @data_source_registry ||= ExcelTemplating::Document::DataSourceRegistry.new
57
+ end
58
+
59
+ # Define a title for this workbook. You may use mustaching here.
60
+ # @param [String] string
61
+ def title(string)
62
+ @document_title = string
63
+ end
64
+
65
+ # Define the default styling to use when writing
66
+ # a column to the worksheet. See Writeexcel::Format
67
+ # @param [String] font Set the font name
68
+ # @param [Integer] size font size
69
+ # @param [Symbol] align :left, :right, or :center
70
+ # @param [Hash] options Additional options to pass to Format
71
+ def default_styling(font: "Calibri", size: 10, align: :left, ** options)
72
+ @default_styling = {
73
+ name: font,
74
+ size: size,
75
+ align: align
76
+ }.merge(options)
77
+ end
78
+
79
+ # Define an organization for the workbook. May use mustaching.
80
+ # @param [String] string
81
+ def organization(string)
82
+ @document_organization = string
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,59 @@
1
+ module ExcelAbstraction
2
+ class ActiveCellReference
3
+ extend Forwardable
4
+
5
+ def_delegators :position, :row, :col
6
+
7
+ def initialize(attrs = {})
8
+ @position = ExcelAbstraction::CellReference.new(attrs)
9
+ end
10
+
11
+ def move(directions = {})
12
+ directions.each do |type, times|
13
+ self.respond_to?(type) ? self.__send__(type, times) : raise(ArgumentError.new("Movement direction is not valid."))
14
+ end
15
+ position
16
+ end
17
+
18
+ def up(times = 1)
19
+ goto(row - times, col)
20
+ end
21
+
22
+ def down(times = 1)
23
+ goto(row + times, col)
24
+ end
25
+
26
+ def left(times = 1)
27
+ goto(row, col - times)
28
+ end
29
+
30
+ def right(times = 1)
31
+ goto(row, col + times)
32
+ end
33
+
34
+ def carriage_return
35
+ goto(row, 0)
36
+ end
37
+
38
+ def linefeed
39
+ down
40
+ end
41
+
42
+ def newline
43
+ carriage_return
44
+ linefeed
45
+ end
46
+
47
+ def goto(row, col)
48
+ self.position = ExcelAbstraction::CellReference.new(row: row, col: col)
49
+ end
50
+
51
+ def reset
52
+ self.position = ExcelAbstraction::CellReference.new(row: 0, col: 0)
53
+ end
54
+
55
+ protected
56
+
57
+ attr_accessor :position
58
+ end
59
+ end
@@ -0,0 +1,23 @@
1
+ module ExcelAbstraction
2
+ class Cell
3
+ attr_reader :position, :val, :styles
4
+
5
+ def initialize(attrs = {})
6
+ @position = Integer(attrs.fetch(:position) { raise ArgumentError.new("Position absent for ExcelAbstraction cell") })
7
+ @val = attrs.fetch(:val) { raise ArgumentError.new("Value absent for ExcelAbstraction cell") }
8
+ @styles = attrs.fetch(:styles) { {} }
9
+ end
10
+
11
+ def <=>(other)
12
+ position <=> other.position
13
+ end
14
+
15
+ def ==(other)
16
+ position == other.position && val == other.val && styles == other.styles
17
+ end
18
+
19
+ def to_cell
20
+ self
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,26 @@
1
+ module ExcelAbstraction
2
+ class CellRange
3
+ include Enumerable
4
+
5
+ alias_method :first, :min
6
+ alias_method :last, :max
7
+
8
+ def initialize
9
+ @cell_references = []
10
+ end
11
+
12
+ def each(&block)
13
+ cell_references.each { |cell_reference| yield(cell_reference) }
14
+ end
15
+
16
+ def <<(attrs)
17
+ cell_reference = ExcelAbstraction::CellReference.new(attrs)
18
+ raise(ArgumentError, "Must be a CellReference belonging to the same row") if last && last.row != cell_reference.row
19
+ self.cell_references << cell_reference
20
+ end
21
+
22
+ protected
23
+
24
+ attr_reader :cell_references
25
+ end
26
+ end
@@ -0,0 +1,39 @@
1
+ module ExcelAbstraction
2
+ class CellReference
3
+ include Comparable
4
+
5
+ COLS = ('A'..'ZZ').to_a
6
+
7
+ attr_accessor :row, :col
8
+
9
+ def initialize(attrs = {})
10
+ @row = attrs.fetch(:row) { 0 }
11
+ @col = attrs.fetch(:col) { 0 }
12
+ end
13
+
14
+ def <=>(other)
15
+ other = other.to_cell_reference
16
+ (self.row == other.row) ? (self.col <=> other.col) : (self.row <=> other.row)
17
+ end
18
+
19
+ def succ
20
+ self.class.new(row: self.row, col: self.col + 1)
21
+ end
22
+
23
+ def to_s
24
+ COLS[col] + (row + 1).to_s
25
+ end
26
+
27
+ def to_cell_reference
28
+ self
29
+ end
30
+
31
+ def to_ary
32
+ [row, col]
33
+ end
34
+
35
+ def to_a
36
+ to_ary
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,36 @@
1
+ require 'delegate'
2
+
3
+ module ExcelAbstraction
4
+ class Date < DelegateClass(Float)
5
+ ADJUSTMENT = ::Date.parse("1900-03-01")
6
+ REFERENCE = ::Date.parse("1900-01-01")
7
+
8
+ attr_reader :value
9
+
10
+ def initialize(raw_value)
11
+ super(convert(raw_value))
12
+ end
13
+
14
+ def to_excel_date
15
+ self
16
+ end
17
+
18
+ private
19
+
20
+ def reference
21
+ REFERENCE
22
+ end
23
+
24
+ def adjustment
25
+ ADJUSTMENT
26
+ end
27
+
28
+ def adjust(raw_value)
29
+ adjustment < raw_value ? 2 : 1
30
+ end
31
+
32
+ def convert(raw_value)
33
+ (raw_value - reference + adjust(raw_value)).to_f
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,29 @@
1
+ module ExcelAbstraction
2
+ class Row
3
+ include Enumerable
4
+ extend Forwardable
5
+
6
+ attr_accessor :styles
7
+ delegate [:each] => :cells
8
+
9
+ alias :first :min
10
+ alias :last :max
11
+
12
+ def initialize
13
+ @cells = []
14
+ @styles = {}
15
+ end
16
+
17
+ def [](index)
18
+ find { |cell| cell.position == index }
19
+ end
20
+
21
+ def <<(attrs)
22
+ @cells << ExcelAbstraction::Cell.new(attrs)
23
+ end
24
+
25
+ protected
26
+
27
+ attr_accessor :cells
28
+ end
29
+ end