excel_templating 0.3.2

Sign up to get free protection for your applications and to get access to all the features.
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