repolho 0.1.8 → 0.2.1

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: fd3898009be42319daf8afaa54b0a064b8952d107a8cef3ad0d3d5692c2d17ad
4
- data.tar.gz: dabe5fac8684e094140bf276d14cddfef3c3ba03693b66049ea5d2a68de30fd5
3
+ metadata.gz: 8f4aac6b18333e8a6d790ad2e130f681419393b0771b98b35368cc34bb11ec7f
4
+ data.tar.gz: 327ddea1d9ecf3bcae6897eed05daba40fc6c1897e5390d0feb4552c19decbc3
5
5
  SHA512:
6
- metadata.gz: d89cf51d6df90425a07e33e666f3a6e63434e0ec7085b1701d10777e371cef6149e43d16b408f809360211ea934f1e514037747dcdede233938f752146e11661
7
- data.tar.gz: 3f9ec0ebbeaa07dc650c05f6a7ed488d289febc9712c5ac96547a4cc6875a63093b254c1f58ed4bbf723263eb43b6f459e75f3a923b8f7803b2c3921e6bcb510
6
+ metadata.gz: d9f15437ff86ee242205fdd24dd52f994a9da1c2dcd0d83814a5ef6b6ca04c69927acc4a014d9c3946686dd2063e31c16b10f246107cfbad95b9d7d8f13eb00a
7
+ data.tar.gz: 4cf688c47fc6658d4c112246f1073f10bad0db3dfa10b85edf47151dd3379c0e002a12a6fdb45ce7cf90bcdc1cff59bd102f8d6f048a4ee32e63021384a9af36
checksums.yaml.gz.sig CHANGED
@@ -1,4 +1,3 @@
1
- S�Jtg��+Ը�,R�� �|;ez4`jp���}����D7��H!
2
- ���$�ۚ5�*�P�p-O�߼�ֺ4-���9���#�5���H�Cp�ܰ;����o�-�8W ;�׊wLY,)g���
3
- ��,S����3��q�鞟g�(� �bq���7��,@|��v��>��M!���(i:����VՈ��t
4
- R@���<��;σZ��Ϧ Yp��}�Zv12ؽV�5.�\0�iH����܎&q(��<���Zڹ�r�(܌���7t�uQ��B��@��w���� "Y����x����ކ�"��2�l������Oۅ����d�gk��)V�l�qK���B:�G�w>D���)��t����򪎵tm��be�fL
1
+ H�2�q�=V���8�Vό��_�Q��ˢ�y-�s�;B���� hUdCi�0C���3���x�
2
+ }�C���σ���G�+D�Y�ص�`�s���NN��Ak�d��+�,d�`�:�>>��`eB�.��c� S�y�^����B��qw�˧4������9��`����q8����Va,(E�Wi<��>>�K�W�P(���0��
3
+ >��^�AZ$I��,��Yx!oWM��2`��.����;zq>�
data/README.md CHANGED
@@ -48,19 +48,89 @@ render html: html.html_safe
48
48
  # or File.write("report.html", html)
49
49
  ```
50
50
 
51
+ ## Generating a template
52
+
53
+ `Repolho::TemplateGenerator` creates a `.rep.erb` file from a base template
54
+ and column metadata. The base template must already contain a `<report-page>`
55
+ element (the page is required, matching the runtime) and two markers:
56
+
57
+ - `<!-- REPORT:HEADERS -->` — replaced by the generated `<th>` cells
58
+ - `<!-- REPORT:ROW -->` — replaced by the generated `<report-row>`
59
+
60
+ Column names are taken from the data source without executing the full query
61
+ when possible (`ActiveRecord::Result#columns`, a Relation's selected columns
62
+ or model column names, an array of hashes/models, or an explicit array of
63
+ column names).
64
+
65
+ ```ruby
66
+ # From a Relation or Result (columns only)
67
+ Repolho::TemplateGenerator.new(
68
+ "app/views/reports/base.rep.erb",
69
+ Order.where(status: "paid")
70
+ ).write("app/views/reports/orders.rep.erb")
71
+
72
+ # Explicit columns + custom labels + numeric formatting
73
+ Repolho::TemplateGenerator.new(
74
+ "app/views/reports/base.rep.erb",
75
+ %w[id customer_name total],
76
+ labels: { "total" => "Total (R$)" },
77
+ numeric: %w[total]
78
+ ).write("app/views/reports/orders.rep.erb")
79
+ ```
80
+
81
+ When running under Rails (or whenever `I18n` is available), header labels are
82
+ looked up case-insensitively under
83
+ `activerecord.attributes.<model>.<column>` (if a `model:` is given) or
84
+ `repolho.attributes.<column>`. The lookup tries the original key, the
85
+ downcase form and the upcase form. If no translation is found, a simple
86
+ humanized name is used.
87
+
88
+ Base template skeleton:
89
+
90
+ ```html
91
+ <report>
92
+ <report-page rows="40">
93
+ <report-header>
94
+ <tr>
95
+ <!-- REPORT:HEADERS -->
96
+ </tr>
97
+ </report-header>
98
+
99
+ <!-- REPORT:ROW -->
100
+
101
+ <report-footer>
102
+ <!-- optional page totals -->
103
+ </report-footer>
104
+ </report-page>
105
+ </report>
106
+ ```
107
+
51
108
  ## Template (`.rep.erb`)
52
109
 
53
110
  Repolho uses a few custom elements inside a normal HTML document. Everything
54
111
  outside `<report>…</report>` is copied as-is (the `previous` and `after`
55
112
  sections). Everything inside the custom elements is processed as ERB.
56
113
 
114
+ ### Why `<report>` instead of `<repolho>`?
115
+
116
+ The element names are intentionally generic (`<report>`, `<report-page>`,
117
+ `<report-group>`, etc.) rather than namespaced under the gem (`<repolho>`,
118
+ `<repolho-page>`, …).
119
+
120
+ Using `repolho` would tie the markup to this specific tool. With `report`,
121
+ the template describes a report structure, not a Ruby library. Someone
122
+ working in another language or with another report engine can reuse the same
123
+ markup (or a close subset of it) without carrying Repolho-specific names.
124
+ The goal is a small, readable convention for report templates — not a
125
+ proprietary dialect.
126
+
57
127
  ### Custom elements
58
128
 
59
129
  | Element | Purpose | Attributes |
60
130
  |---------|---------|------------|
61
- | `<report>` | Root container of the report | `pagination` (optional, `"true"`): enables client-side page navigation. Also accepts `pagination-first`, `pagination-prev`, `pagination-next`, `pagination-last` for custom labels.<br>`orientation` (optional, `"portrait"` or `"landscape"`): page orientation for print.<br>`paper` (optional, e.g. `"A4"`, `"Letter"`): paper size for print. If either is present, a `@media print` style is injected at the end of `<body>`. |
131
+ | `<report>` | Root container of the report | `pagination` (optional, `"true"`): enables client-side page navigation. Also accepts `pagination-first`, `pagination-prev`, `pagination-next`, `pagination-last` for custom labels.<br>`orientation` (optional, `"portrait"` or `"landscape"`): page orientation for print.<br>`paper` (optional, e.g. `"A4"`, `"Letter"`): paper size for print. If either is present, a `@media print` style is injected at the end of `<body>`.<br>`min-row-for-break` (optional, integer): when a group has `page-break-after="true"`, the forced break is skipped if the current page row number is less than or equal to this value. Prevents an extra break when a page has just started. |
62
132
  | `<report-page>` | Defines a page (required, only one) | `rows` (required): max data rows per page |
63
- | `<report-group>` | Nested grouping | `id` (required), `attr` (required), `reprint-header-on-page-break` (optional, `"true"`) |
133
+ | `<report-group>` | Nested grouping | `id` (required), `attr` (required), `reprint-header-on-page-break` (optional, `"true"`), `page-break-after` (optional, `"true"`): after the group ends and its footer is emitted on the current page, fill the remaining rows with blanks and force a page break so the next group starts on a fresh page. Respects `min-row-for-break` on `<report>`. |
64
134
  | `<report-header>` | Header of the current container | — |
65
135
  | `<report-footer>` | Footer of the current container | — |
66
136
  | `<report-row>` | Template for each data row (inside the innermost group) | — |
@@ -6,7 +6,7 @@ require_relative "stats"
6
6
 
7
7
  module Repolho
8
8
  class Container
9
- attr_accessor :header, :footer, :containers, :row_template, :attr, :page, :report, :status, :reprint_header_on_page_break
9
+ attr_accessor :header, :footer, :containers, :row_template, :attr, :page, :report, :status, :reprint_header_on_page_break, :page_break_after, :footer_rows
10
10
  attr_reader :id, :number, :stats
11
11
 
12
12
  UNSTARTED = 0
@@ -28,6 +28,8 @@ module Repolho
28
28
  @attr = nil
29
29
  @page = nil
30
30
  @report = nil
31
+ @page_break_after = false
32
+ @footer_rows = 0
31
33
 
32
34
  create_locals
33
35
  end
@@ -164,12 +166,32 @@ module Repolho
164
166
  container.status = FINISHED
165
167
  end
166
168
 
169
+ # Emit this group's footer on the current page (it stays with its data).
167
170
  @page << finish
168
171
  reset_stats
169
172
  @status = FINISHED
173
+
174
+ # Then, if requested, fill the rest of the page with blanks and force
175
+ # a break so the *next* group starts on a fresh page.
176
+ # min-row-for-break skips this when the page has barely started
177
+ # (e.g. the footer itself already caused a break).
178
+ if page_break_after && should_force_page_break?
179
+ @page.pad_and_break
180
+ end
170
181
  end
171
182
  end
172
183
 
184
+ def should_force_page_break?
185
+ return false unless @page
186
+
187
+ min = @page.min_row_for_break
188
+ return true if min.nil? || min <= 0
189
+
190
+ # Only force when the current page has progressed past the threshold.
191
+ # Prevents a second break when the page has just started (e.g. row 5 < 35).
192
+ @page.row_number > min
193
+ end
194
+
173
195
  def check_page_break
174
196
  return unless self.is_a?(Page)
175
197
  return unless @row_number >= @valid_rows
data/lib/repolho/main.rb CHANGED
@@ -247,6 +247,10 @@ module Repolho
247
247
  @orientation = report_node["orientation"]&.downcase
248
248
  @paper = report_node["paper"]
249
249
 
250
+ if report_node["min-row-for-break"]
251
+ @page.min_row_for_break = report_node["min-row-for-break"].to_i
252
+ end
253
+
250
254
  if @pagination
251
255
  @pagination_labels[:first] = report_node["pagination-first"] if report_node["pagination-first"]
252
256
  @pagination_labels[:prev] = report_node["pagination-prev"] if report_node["pagination-prev"]
@@ -297,14 +301,23 @@ module Repolho
297
301
  def build_group(element)
298
302
  container = Container.new(element["id"], @locals, make_stats)
299
303
  container.header = info(element.search("> report-header").first)
300
- container.footer = info(element.search("> report-footer").first)
304
+ footer_info = info(element.search("> report-footer").first)
305
+ container.footer = footer_info
306
+ container.footer_rows = count_trs(footer_info[:content])
301
307
  container.attr = element["attr"]
302
308
  container.reprint_header_on_page_break = element["reprint-header-on-page-break"] == "true"
309
+ container.page_break_after = element["page-break-after"] == "true"
303
310
  container.page = @page
304
311
  container.report = @report
305
312
  container
306
313
  end
307
314
 
315
+ def count_trs(html)
316
+ return 0 if html.nil? || html.strip.empty?
317
+
318
+ Nokogiri::HTML(html).search("tr").size
319
+ end
320
+
308
321
  def info(element)
309
322
  return { content: "" } unless element
310
323
 
data/lib/repolho/page.rb CHANGED
@@ -4,6 +4,7 @@
4
4
  module Repolho
5
5
  class Page < Container
6
6
  attr_reader :content, :row_number, :rows, :page_number
7
+ attr_accessor :min_row_for_break
7
8
 
8
9
  def initialize(id, locals, stats)
9
10
  super(id, locals, stats)
@@ -43,6 +44,35 @@ module Repolho
43
44
  end
44
45
  end
45
46
 
47
+ # Fills the current page with blank rows up to the limit and forces a break
48
+ # using the existing page-break path. Called *after* a group footer has
49
+ # already been emitted, so the footer stays on the current page and the
50
+ # next group starts on a fresh page.
51
+ #
52
+ # After the break, the new page is opened and its header is emitted
53
+ # immediately so subsequent content (the group footer) appears *after*
54
+ # the page header, not before it.
55
+ def pad_and_break
56
+ remaining = @valid_rows - @row_number
57
+ if remaining > 0
58
+ blank = %(<tr><td colspan="200">&nbsp;</td></tr>)
59
+ remaining.times { self << blank }
60
+ end
61
+ check_page_break
62
+
63
+ # Open the new page and emit its header right away.
64
+ # check_page_break left status as FINISHED; we start the page now
65
+ # so later check_started does not emit the header again (late).
66
+ return if @page_open
67
+
68
+ open_page
69
+ if @status == FINISHED
70
+ header_html = start
71
+ self << header_html if header_html && !header_html.to_s.strip.empty?
72
+ @status = STARTED
73
+ end
74
+ end
75
+
46
76
  def <<(content)
47
77
  return unless content
48
78
 
@@ -0,0 +1,151 @@
1
+ # typed: false
2
+ # frozen_string_literal: true
3
+
4
+ require "cgi"
5
+
6
+ module Repolho
7
+ # Generates a .rep.erb template from a base template and column metadata.
8
+ #
9
+ # The base template must contain a <report-page> element and the markers
10
+ # <!-- REPORT:HEADERS --> and <!-- REPORT:ROW -->. The page element is
11
+ # required, matching the runtime expectation.
12
+ #
13
+ # Column labels are resolved case-insensitively (original, downcase, upcase)
14
+ # via I18n when available. Otherwise a simple humanized form of the column
15
+ # name is used.
16
+ class TemplateGenerator
17
+ HEADERS_MARKER = "<!-- REPORT:HEADERS -->"
18
+ ROW_MARKER = "<!-- REPORT:ROW -->"
19
+
20
+ def initialize(base_path, data, model: nil, labels: {}, numeric: [])
21
+ @base_path = base_path
22
+ @data = data
23
+ @model = model
24
+ @labels = labels.transform_keys(&:to_s)
25
+ @numeric = Array(numeric).map(&:to_s)
26
+ end
27
+
28
+ # Returns the generated template content as a String.
29
+ def generate
30
+ content = File.read(@base_path)
31
+ validate_base!(content)
32
+
33
+ columns = extract_columns(@data)
34
+ raise ArgumentError, "No columns could be extracted from the given data" if columns.empty?
35
+
36
+ headers = generate_headers(columns)
37
+ row = generate_row(columns)
38
+
39
+ content
40
+ .gsub(HEADERS_MARKER, headers)
41
+ .gsub(ROW_MARKER, row)
42
+ end
43
+
44
+ # Writes the generated template to output_path.
45
+ def write(output_path)
46
+ File.write(output_path, generate)
47
+ end
48
+
49
+ private
50
+
51
+ def validate_base!(content)
52
+ unless content.include?("<report-page")
53
+ raise ArgumentError, "Base template must contain a <report-page> element"
54
+ end
55
+
56
+ unless content.include?(HEADERS_MARKER)
57
+ raise ArgumentError, "Base template must contain the marker #{HEADERS_MARKER}"
58
+ end
59
+
60
+ unless content.include?(ROW_MARKER)
61
+ raise ArgumentError, "Base template must contain the marker #{ROW_MARKER}"
62
+ end
63
+ end
64
+
65
+ def extract_columns(data)
66
+ return data.map(&:to_s) if explicit_column_list?(data)
67
+
68
+ if data.respond_to?(:columns)
69
+ return data.columns.map(&:to_s)
70
+ end
71
+
72
+ if data.respond_to?(:klass) && data.klass.respond_to?(:column_names)
73
+ if data.respond_to?(:select_values) && data.select_values.any?
74
+ return data.select_values.map { |v| extract_column_name(v) }
75
+ end
76
+ return data.klass.column_names.map(&:to_s)
77
+ end
78
+
79
+ extract_from_rows(data)
80
+ end
81
+
82
+ def explicit_column_list?(data)
83
+ data.is_a?(Array) && data.all? { |c| c.is_a?(String) || c.is_a?(Symbol) }
84
+ end
85
+
86
+ def extract_column_name(value)
87
+ value.to_s.split(".").last.gsub(/["'`]/, "").strip
88
+ end
89
+
90
+ def extract_from_rows(data)
91
+ rows = data.respond_to?(:to_a) ? data.to_a : Array(data)
92
+ return [] if rows.empty?
93
+
94
+ first = rows.first
95
+ if first.respond_to?(:attributes)
96
+ first.attributes.keys.map(&:to_s)
97
+ elsif first.is_a?(Hash)
98
+ first.keys.map(&:to_s)
99
+ else
100
+ []
101
+ end
102
+ end
103
+
104
+ def generate_headers(columns)
105
+ columns.map { |col| "<th>#{CGI.escapeHTML(column_label(col))}</th>" }.join("\n ")
106
+ end
107
+
108
+ def generate_row(columns)
109
+ cells = columns.map do |col|
110
+ expr = if @numeric.include?(col)
111
+ %(<%= number_with_precision(attr(:#{col}), precision: 2) %>)
112
+ else
113
+ %(<%= attr(:#{col}) %>)
114
+ end
115
+ "<td>#{expr}</td>"
116
+ end
117
+
118
+ <<~HTML.strip
119
+ <report-row>
120
+ <tr>
121
+ #{cells.join("\n ")}
122
+ </tr>
123
+ </report-row>
124
+ HTML
125
+ end
126
+
127
+ def column_label(column)
128
+ column = column.to_s
129
+ return @labels[column] if @labels.key?(column)
130
+
131
+ return humanize(column) unless defined?(I18n)
132
+
133
+ [column, column.downcase, column.upcase].uniq.each do |key|
134
+ full_key = if @model
135
+ "activerecord.attributes.#{@model.model_name.i18n_key}.#{key}"
136
+ else
137
+ "repolho.attributes.#{key}"
138
+ end
139
+
140
+ result = I18n.t(full_key, default: nil)
141
+ return result if result
142
+ end
143
+
144
+ humanize(column)
145
+ end
146
+
147
+ def humanize(column)
148
+ column.to_s.tr("_", " ").capitalize
149
+ end
150
+ end
151
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Repolho
4
- VERSION = "0.1.8"
4
+ VERSION = "0.2.1"
5
5
  end
data/lib/repolho.rb CHANGED
@@ -6,6 +6,7 @@ require_relative "repolho/container"
6
6
  require_relative "repolho/page"
7
7
  require_relative "repolho/report"
8
8
  require_relative "repolho/main"
9
+ require_relative "repolho/template_generator"
9
10
 
10
11
  module Repolho
11
12
  # Includes Rails helper modules into Container so they become available
@@ -14,6 +15,11 @@ module Repolho
14
15
  # By default includes ActionView::Helpers::NumberHelper.
15
16
  # Pass additional modules if needed.
16
17
  #
18
+ # The method is idempotent: modules already included are skipped.
19
+ #
20
+ # Outside a full Rails environment you must load the helpers correctly
21
+ # before calling this method (see README).
22
+ #
17
23
  # Example:
18
24
  # Repolho.include_rails_helpers!
19
25
  # Repolho.include_rails_helpers!(ActionView::Helpers::NumberHelper, ActionView::Helpers::TextHelper)
@@ -24,5 +30,7 @@ module Repolho
24
30
 
25
31
  Container.include(mod)
26
32
  end
33
+
27
34
  end
35
+
28
36
  end
data/test/main_test.rb CHANGED
@@ -457,5 +457,71 @@ module Repolho
457
457
  ensure
458
458
  template&.unlink
459
459
  end
460
+
461
+ def test_page_break_after_group_pads_and_breaks
462
+ data = [
463
+ { "id" => 1, "name" => "Alice", "value" => 10.0, "state" => "SP" },
464
+ { "id" => 2, "name" => "Bob", "value" => 20.0, "state" => "SP" },
465
+ { "id" => 3, "name" => "Carol", "value" => 15.0, "state" => "RJ" },
466
+ { "id" => 4, "name" => "Dave", "value" => 25.0, "state" => "RJ" }
467
+ ]
468
+
469
+ template_content = GROUP_TEMPLATE.gsub(
470
+ '<report-group id="state" attr="state">',
471
+ '<report-group id="state" attr="state" page-break-after="true">'
472
+ ).gsub(
473
+ '<report-page rows="10">',
474
+ '<report-page rows="6">'
475
+ )
476
+
477
+ template = write_temp_template(template_content)
478
+ main = Main.new(template.path, data)
479
+ main.run
480
+ html = main.content.join
481
+ doc = html_doc(html)
482
+
483
+ pages = page_tbodies(doc)
484
+ assert pages.size >= 2, "Expected a forced page break after group, got #{pages.size}"
485
+
486
+ # Blank padding rows should be present
487
+ assert_includes html, "&nbsp;"
488
+
489
+ # Group footer of the first group should appear, and the second group header too
490
+ assert_includes html, "State total"
491
+ assert_includes html, "State: RJ"
492
+ ensure
493
+ template&.unlink
494
+ end
495
+
496
+ def test_min_row_for_break_skips_early_page
497
+ data = [
498
+ { "id" => 1, "name" => "Alice", "value" => 10.0, "state" => "SP" },
499
+ { "id" => 2, "name" => "Bob", "value" => 20.0, "state" => "RJ" }
500
+ ]
501
+
502
+ # High threshold: page has barely started, so no forced break
503
+ template_content = GROUP_TEMPLATE.gsub(
504
+ "<report>",
505
+ '<report min-row-for-break="20">'
506
+ ).gsub(
507
+ '<report-group id="state" attr="state">',
508
+ '<report-group id="state" attr="state" page-break-after="true">'
509
+ ).gsub(
510
+ '<report-page rows="10">',
511
+ '<report-page rows="30">'
512
+ )
513
+
514
+ template = write_temp_template(template_content)
515
+ main = Main.new(template.path, data)
516
+ main.run
517
+ html = main.content.join
518
+ doc = html_doc(html)
519
+
520
+ pages = page_tbodies(doc)
521
+ assert_equal 1, pages.size, "Threshold should have prevented an early forced break"
522
+ refute_includes html, "&nbsp;"
523
+ ensure
524
+ template&.unlink
525
+ end
460
526
  end
461
527
  end
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "test_helper"
4
+
5
+ class TemplateGeneratorTest < Minitest::Test
6
+ include TestHelpers
7
+
8
+ def setup
9
+ @base = <<~HTML
10
+ <!DOCTYPE html>
11
+ <html>
12
+ <body>
13
+ <table>
14
+ <report>
15
+ <report-page rows="40">
16
+ <report-header>
17
+ <tr>
18
+ <!-- REPORT:HEADERS -->
19
+ </tr>
20
+ </report-header>
21
+
22
+ <!-- REPORT:ROW -->
23
+
24
+ <report-footer>
25
+ <tr><td colspan="99">Footer</td></tr>
26
+ </report-footer>
27
+ </report-page>
28
+ </report>
29
+ </table>
30
+ </body>
31
+ </html>
32
+ HTML
33
+ end
34
+
35
+ def test_generates_headers_and_row_from_array_of_hashes
36
+ data = [{ "id" => 1, "name" => "Alice", "total" => 10.5 }]
37
+ file = write_temp_template(@base)
38
+
39
+ generator = Repolho::TemplateGenerator.new(file.path, data)
40
+ result = generator.generate
41
+
42
+ assert_includes result, "<th>Id</th>"
43
+ assert_includes result, "<th>Name</th>"
44
+ assert_includes result, "<th>Total</th>"
45
+ assert_includes result, "<report-row>"
46
+ assert_includes result, "<%= attr(:id) %>"
47
+ assert_includes result, "<%= attr(:name) %>"
48
+ assert_includes result, "<%= attr(:total) %>"
49
+ refute_includes result, "<!-- REPORT:HEADERS -->"
50
+ refute_includes result, "<!-- REPORT:ROW -->"
51
+ ensure
52
+ file.unlink
53
+ end
54
+
55
+ def test_uses_explicit_column_list
56
+ file = write_temp_template(@base)
57
+ generator = Repolho::TemplateGenerator.new(file.path, %w[id customer_name])
58
+ result = generator.generate
59
+
60
+ assert_includes result, "<th>Id</th>"
61
+ assert_includes result, "<th>Customer name</th>"
62
+ assert_includes result, "<%= attr(:customer_name) %>"
63
+ ensure
64
+ file.unlink
65
+ end
66
+
67
+ def test_custom_labels_override
68
+ file = write_temp_template(@base)
69
+ generator = Repolho::TemplateGenerator.new(
70
+ file.path,
71
+ %w[id total],
72
+ labels: { "total" => "Total (R$)" }
73
+ )
74
+ result = generator.generate
75
+
76
+ assert_includes result, "<th>Total (R$)</th>"
77
+ assert_includes result, "<th>Id</th>"
78
+ ensure
79
+ file.unlink
80
+ end
81
+
82
+ def test_numeric_columns_use_number_helper
83
+ file = write_temp_template(@base)
84
+ generator = Repolho::TemplateGenerator.new(
85
+ file.path,
86
+ %w[id total],
87
+ numeric: %w[total]
88
+ )
89
+ result = generator.generate
90
+
91
+ assert_includes result, "<%= number_with_precision(attr(:total), precision: 2) %>"
92
+ assert_includes result, "<%= attr(:id) %>"
93
+ ensure
94
+ file.unlink
95
+ end
96
+
97
+ def test_raises_when_page_missing
98
+ bad = @base.gsub("<report-page rows=\"40\">", "").gsub("</report-page>", "")
99
+ file = write_temp_template(bad)
100
+
101
+ generator = Repolho::TemplateGenerator.new(file.path, %w[id])
102
+ error = assert_raises(ArgumentError) { generator.generate }
103
+ assert_match(/report-page/, error.message)
104
+ ensure
105
+ file.unlink
106
+ end
107
+
108
+ def test_raises_when_markers_missing
109
+ bad = @base.gsub("<!-- REPORT:HEADERS -->", "").gsub("<!-- REPORT:ROW -->", "")
110
+ file = write_temp_template(bad)
111
+
112
+ generator = Repolho::TemplateGenerator.new(file.path, %w[id])
113
+ error = assert_raises(ArgumentError) { generator.generate }
114
+ assert_match(/REPORT:HEADERS/, error.message)
115
+ ensure
116
+ file.unlink
117
+ end
118
+
119
+ def test_write_creates_file
120
+ file = write_temp_template(@base)
121
+ output = Tempfile.new(["out", ".rep.erb"])
122
+ output.close
123
+
124
+ generator = Repolho::TemplateGenerator.new(file.path, %w[id name])
125
+ generator.write(output.path)
126
+
127
+ content = File.read(output.path)
128
+ assert_includes content, "<report-row>"
129
+ assert_includes content, "<%= attr(:name) %>"
130
+ ensure
131
+ file.unlink
132
+ output.unlink
133
+ end
134
+ end
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: repolho
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.8
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Eustáquio Rangel
@@ -97,6 +97,7 @@ files:
97
97
  - lib/repolho/page.rb
98
98
  - lib/repolho/report.rb
99
99
  - lib/repolho/stats.rb
100
+ - lib/repolho/template_generator.rb
100
101
  - lib/repolho/version.rb
101
102
  - test/container_test.rb
102
103
  - test/fixtures/simple.rep.erb
@@ -104,6 +105,7 @@ files:
104
105
  - test/page_test.rb
105
106
  - test/report_test.rb
106
107
  - test/stats_test.rb
108
+ - test/template_generator_test.rb
107
109
  - test/test_helper.rb
108
110
  homepage: https://github.com/taq/repolho
109
111
  licenses:
metadata.gz.sig CHANGED
Binary file