rails-excel-reporter 0.3.0 → 1.0.0

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: 980f4f3ce34363580563962788c6d6066d88bca4c156d11bfc6a4305a9f23e03
4
- data.tar.gz: 218cff5d4b639629b76e48262e17ec3ffe180c773c83b3ec7cd558a822129d1e
3
+ metadata.gz: 9b6f920ce797a160c37ecc253bc0046b4212df1857966fec30100d4fcdad15d2
4
+ data.tar.gz: 5f9d145e21d5a5970178d8da52a1972345e78a6b64777bc05a7954dcf71b1cef
5
5
  SHA512:
6
- metadata.gz: 16418a929a485fd2e1215280024b0a366b6cefc65a08b9df506a5fff40cb255473ab6ae52fd28406cd19699e2ce44a0f8b1ac098e8691f201006ea7d31c5be24
7
- data.tar.gz: 4acfa9b21a2f59426b7a19abb3471f960c4042f51eac7fb4fd4dd74356ed3f86ed28a3cbbf8dd5d9376bfe91e06b83dc161ed29feba7018cae4939a94b2581b8
6
+ metadata.gz: 23ee34c51036cc83cd9030ea76466b76a1af1778e6817ceac117d760569ea327e9b93c18c222ee4909349f346bda6c4a1334813db47fc452e28061f1ca71c9b7
7
+ data.tar.gz: 62953ee7433e7dc98483013549b1a1d32d5e80b5a90a7296a1754856b3269baab298db3a927c1e7eb3d17ed4ad51a1dcce52422426185dcb84eec8a7a3cc84a0
data/Gemfile CHANGED
@@ -3,3 +3,8 @@ source 'https://rubygems.org'
3
3
  gemspec
4
4
 
5
5
  gem 'rake', '~> 13.0'
6
+
7
+ group :development, :test do
8
+ gem 'rubocop', '~> 1.78'
9
+ gem 'rubocop-performance', '~> 1.24'
10
+ end
data/Gemfile.lock CHANGED
@@ -1,10 +1,10 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- rails-excel-reporter (0.2.0)
5
- activesupport (>= 7.0)
4
+ rails-excel-reporter (1.0.0)
5
+ activesupport (>= 8.0)
6
6
  caxlsx (~> 4.0)
7
- rails (>= 7.0)
7
+ rails (>= 8.0)
8
8
 
9
9
  GEM
10
10
  remote: https://rubygems.org/
@@ -125,6 +125,7 @@ GEM
125
125
  marcel (1.0.4)
126
126
  method_source (1.1.0)
127
127
  mini_mime (1.1.5)
128
+ mini_portile2 (2.8.9)
128
129
  minitest (5.25.5)
129
130
  net-imap (0.5.9)
130
131
  date
@@ -247,6 +248,10 @@ GEM
247
248
  rubocop-ast (1.45.1)
248
249
  parser (>= 3.3.7.2)
249
250
  prism (~> 1.4)
251
+ rubocop-performance (1.25.0)
252
+ lint_roller (~> 1.1)
253
+ rubocop (>= 1.75.0, < 2.0)
254
+ rubocop-ast (>= 1.38.0, < 2.0)
250
255
  ruby-progressbar (1.13.0)
251
256
  rubyzip (2.4.1)
252
257
  securerandom (0.4.1)
@@ -256,6 +261,8 @@ GEM
256
261
  simplecov_json_formatter (~> 0.1)
257
262
  simplecov-html (0.13.1)
258
263
  simplecov_json_formatter (0.1.4)
264
+ sqlite3 (1.7.3)
265
+ mini_portile2 (~> 2.8.0)
259
266
  sqlite3 (1.7.3-aarch64-linux)
260
267
  sqlite3 (1.7.3-arm-linux)
261
268
  sqlite3 (1.7.3-arm64-darwin)
@@ -297,7 +304,8 @@ DEPENDENCIES
297
304
  rake (~> 13.0)
298
305
  rspec (~> 3.0)
299
306
  rspec-rails (~> 5.0)
300
- rubocop (~> 1.0)
307
+ rubocop (~> 1.78)
308
+ rubocop-performance (~> 1.24)
301
309
  simplecov (~> 0.21)
302
310
  sqlite3 (~> 1.4)
303
311
  yard (~> 0.9)
data/README.md CHANGED
@@ -463,8 +463,8 @@ end
463
463
 
464
464
  ## Requirements
465
465
 
466
- - Ruby 2.7+
467
- - Rails 7.0+
466
+ - Ruby 3.2+
467
+ - Rails 8.0+
468
468
  - caxlsx ~> 4.0
469
469
 
470
470
  ## Contributing
@@ -23,16 +23,18 @@ module RailsExcelReporter
23
23
  if attrs.empty?
24
24
  @attributes ||= []
25
25
  else
26
- @attributes = attrs.map do |attr|
27
- case attr
28
- when Symbol
29
- { name: attr, header: attr.to_s.humanize }
30
- when Hash
31
- symbolize_hash_keys(attr)
32
- else
33
- { name: attr.to_sym, header: attr.to_s.humanize }
34
- end
35
- end
26
+ @attributes = attrs.map { |attr| process_attribute attr }
27
+ end
28
+ end
29
+
30
+ def self.process_attribute(attr)
31
+ case attr
32
+ when Symbol
33
+ { name: attr, header: attr.to_s.humanize }
34
+ when Hash
35
+ symbolize_hash_keys(attr)
36
+ else
37
+ { name: attr.to_sym, header: attr.to_s.humanize }
36
38
  end
37
39
  end
38
40
 
@@ -51,7 +53,7 @@ module RailsExcelReporter
51
53
 
52
54
  def self.inherited(subclass)
53
55
  super
54
- subclass.instance_variable_set(:@attributes, @attributes.dup) if @attributes
56
+ subclass.instance_variable_set :@attributes, @attributes.dup if @attributes
55
57
  end
56
58
 
57
59
  def attributes
@@ -70,18 +72,18 @@ module RailsExcelReporter
70
72
  end
71
73
 
72
74
  def save_to(path)
73
- File.open(path, 'wb') do |file|
74
- file.write(to_xlsx)
75
+ File.open path, 'wb' do |file|
76
+ file.write to_xlsx
75
77
  end
76
78
  end
77
79
 
78
80
  def filename
79
- timestamp = Time.now.strftime(RailsExcelReporter.config.date_format).gsub('-', '_')
81
+ timestamp = Time.now.strftime(RailsExcelReporter.config.date_format).gsub '-', '_'
80
82
  "#{worksheet_name.parameterize}_report_#{timestamp}.xlsx"
81
83
  end
82
84
 
83
85
  def stream
84
- StringIO.new(to_xlsx)
86
+ StringIO.new to_xlsx
85
87
  end
86
88
 
87
89
  def to_h
@@ -114,25 +116,29 @@ module RailsExcelReporter
114
116
 
115
117
  def render
116
118
  validate_attributes!
117
-
118
119
  before_render
119
120
 
120
- @tempfile = Tempfile.new([filename.gsub('.xlsx', ''), '.xlsx'],
121
- RailsExcelReporter.config.temp_directory)
122
-
123
- package = ::Axlsx::Package.new
124
- workbook = package.workbook
121
+ create_tempfile
122
+ create_worksheet
125
123
 
126
- worksheet = workbook.add_worksheet(name: worksheet_name)
124
+ @rendered = true
125
+ after_render
126
+ end
127
127
 
128
- add_headers(worksheet)
129
- add_data_rows(worksheet)
128
+ def create_tempfile
129
+ @tempfile = Tempfile.new [filename.gsub('.xlsx', ''), '.xlsx'],
130
+ RailsExcelReporter.config.temp_directory
131
+ end
130
132
 
131
- package.serialize(@tempfile.path)
133
+ def create_worksheet
134
+ package = ::Axlsx::Package.new
135
+ workbook = package.workbook
136
+ worksheet = workbook.add_worksheet name: worksheet_name
132
137
 
133
- @rendered = true
138
+ add_headers worksheet
139
+ add_data_rows worksheet
134
140
 
135
- after_render
141
+ package.serialize @tempfile.path
136
142
  end
137
143
 
138
144
  def validate_attributes!
@@ -141,56 +147,79 @@ module RailsExcelReporter
141
147
 
142
148
  def add_headers(worksheet)
143
149
  header_values = attributes.map { |attr| attr[:header] }
144
- header_style = build_caxlsx_style(get_header_style)
150
+ add_header_row worksheet, header_values
151
+ add_auto_filter worksheet
152
+ end
153
+
154
+ def add_header_row(worksheet, header_values)
155
+ header_style = build_caxlsx_style get_header_style
145
156
 
146
157
  if header_style.any?
147
- style_id = worksheet.workbook.styles.add_style(header_style)
148
- worksheet.add_row(header_values, style: style_id)
158
+ style_id = worksheet.workbook.styles.add_style header_style
159
+ worksheet.add_row header_values, style: style_id
149
160
  else
150
- worksheet.add_row(header_values)
161
+ worksheet.add_row header_values
151
162
  end
163
+ end
152
164
 
153
- worksheet.auto_filter = "A1:#{column_letter(attributes.size)}1"
165
+ def add_auto_filter(worksheet)
166
+ worksheet.auto_filter = "A1:#{column_letter attributes.size}1"
154
167
  end
155
168
 
156
169
  def add_data_rows(worksheet)
157
170
  with_progress_tracking do |object, _progress|
158
- before_row(object)
159
-
160
- row_values = attributes.map do |attr|
161
- get_attribute_value(object, attr[:name])
162
- end
171
+ process_data_row worksheet, object
172
+ end
173
+ end
163
174
 
164
- row_styles = attributes.map do |attr|
165
- style_options = build_caxlsx_style(get_column_style(attr[:name]))
166
- worksheet.workbook.styles.add_style(style_options) if style_options.any?
167
- end
175
+ def process_data_row(worksheet, object)
176
+ before_row object
177
+ row_values = build_row_values object
178
+ row_styles = build_row_styles worksheet
179
+ worksheet.add_row row_values, style: row_styles
180
+ after_row object
181
+ end
168
182
 
169
- worksheet.add_row(row_values, style: row_styles)
183
+ def build_row_values(object)
184
+ attributes.map do |attr|
185
+ get_attribute_value object, attr[:name]
186
+ end
187
+ end
170
188
 
171
- after_row(object)
189
+ def build_row_styles(worksheet)
190
+ attributes.map do |attr|
191
+ style_options = build_caxlsx_style get_column_style(attr[:name])
192
+ worksheet.workbook.styles.add_style style_options if style_options.any?
172
193
  end
173
194
  end
174
195
 
175
196
  def get_attribute_value(object, attribute_name)
176
- if respond_to?(attribute_name)
177
- @object = object
178
- result = send(attribute_name)
179
- @object = nil
180
- result
181
- elsif object.respond_to?(attribute_name)
182
- object.send(attribute_name)
183
- elsif object.respond_to?(:[])
184
- object[attribute_name] || object[attribute_name.to_s]
197
+ if respond_to? attribute_name
198
+ call_custom_method object, attribute_name
199
+ elsif object.respond_to? attribute_name
200
+ object.send attribute_name
201
+ elsif object.respond_to? :[]
202
+ get_hash_value object, attribute_name
185
203
  end
186
204
  end
187
205
 
206
+ def call_custom_method(object, attribute_name)
207
+ @object = object
208
+ result = send attribute_name
209
+ @object = nil
210
+ result
211
+ end
212
+
213
+ def get_hash_value(object, attribute_name)
214
+ object[attribute_name] || object[attribute_name.to_s]
215
+ end
216
+
188
217
  attr_reader :object
189
218
 
190
219
  def collection_data
191
220
  @collection_data ||= stream_data.map do |item|
192
221
  attributes.map do |attr|
193
- get_attribute_value(item, attr[:name])
222
+ get_attribute_value item, attr[:name]
194
223
  end
195
224
  end
196
225
  end
@@ -3,17 +3,7 @@ module RailsExcelReporter
3
3
  attr_accessor :default_styles, :date_format, :streaming_threshold, :temp_directory
4
4
 
5
5
  def initialize
6
- @default_styles = {
7
- header: {
8
- bg_color: '4472C4',
9
- fg_color: 'FFFFFF',
10
- bold: true,
11
- border: { style: :thin, color: '000000' }
12
- },
13
- cell: {
14
- border: { style: :thin, color: 'CCCCCC' }
15
- }
16
- }
6
+ @default_styles = default_style_config
17
7
  @date_format = '%Y-%m-%d'
18
8
  @streaming_threshold = 1000
19
9
  @temp_directory = nil
@@ -22,6 +12,30 @@ module RailsExcelReporter
22
12
  def temp_directory
23
13
  @temp_directory || Dir.tmpdir
24
14
  end
15
+
16
+ private
17
+
18
+ def default_style_config
19
+ {
20
+ header: header_style,
21
+ cell: cell_style
22
+ }
23
+ end
24
+
25
+ def header_style
26
+ {
27
+ bg_color: '4472C4',
28
+ fg_color: 'FFFFFF',
29
+ bold: true,
30
+ border: { style: :thin, color: '000000' }
31
+ }
32
+ end
33
+
34
+ def cell_style
35
+ {
36
+ border: { style: :thin, color: 'CCCCCC' }
37
+ }
38
+ end
25
39
  end
26
40
 
27
41
  class << self
@@ -30,7 +44,7 @@ module RailsExcelReporter
30
44
  end
31
45
 
32
46
  def configure
33
- yield(configuration)
47
+ yield configuration
34
48
  end
35
49
 
36
50
  def config
@@ -4,31 +4,38 @@ module RailsExcelReporter
4
4
  filename = options[:filename] || report.filename
5
5
  disposition = options[:disposition] || 'attachment'
6
6
 
7
- send_data(
7
+ send_data \
8
8
  report.to_xlsx,
9
9
  filename: filename,
10
- type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
10
+ type: excel_content_type,
11
11
  disposition: disposition
12
- )
13
12
  end
14
13
 
15
14
  def stream_excel_report(report, options = {})
16
15
  filename = options[:filename] || report.filename
17
-
18
- response.headers['Content-Type'] = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
19
- response.headers['Content-Disposition'] = "attachment; filename=\"#{filename}\""
20
- response.headers['Content-Transfer-Encoding'] = 'binary'
21
- response.headers['Last-Modified'] = Time.now.httpdate
22
-
16
+ set_excel_response_headers filename
23
17
  self.response_body = report.stream
24
18
  end
25
19
 
26
20
  def excel_report_response(report, options = {})
27
21
  if report.should_stream?
28
- stream_excel_report(report, options)
22
+ stream_excel_report report, options
29
23
  else
30
- send_excel_report(report, options)
24
+ send_excel_report report, options
31
25
  end
32
26
  end
27
+
28
+ private
29
+
30
+ def set_excel_response_headers(filename)
31
+ response.headers['Content-Type'] = excel_content_type
32
+ response.headers['Content-Disposition'] = "attachment; filename=\"#{filename}\""
33
+ response.headers['Content-Transfer-Encoding'] = 'binary'
34
+ response.headers['Last-Modified'] = Time.now.httpdate
35
+ end
36
+
37
+ def excel_content_type
38
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
39
+ end
33
40
  end
34
41
  end
@@ -7,29 +7,42 @@ module RailsExcelReporter
7
7
  end
8
8
 
9
9
  initializer 'rails_excel_reporter.include_controller_helpers' do
10
- ActiveSupport.on_load(:action_controller) do
10
+ ActiveSupport.on_load :action_controller do
11
11
  include RailsExcelReporter::ControllerHelpers
12
12
  end
13
13
  end
14
14
 
15
15
  config.after_initialize do
16
- if defined?(Rails.application) && Rails.application
17
- begin
18
- reports_path = Rails.root.join('app/reports')
19
-
20
- if Rails.application.paths['app/reports']
21
- unless Rails.application.paths['app/reports'].paths.include?(reports_path.to_s)
22
- Rails.application.paths['app/reports'] << reports_path.to_s
23
- end
24
-
25
- if Rails.application.paths['app/reports'].respond_to?(:eager_load!)
26
- Rails.application.paths['app/reports'].eager_load!
27
- end
28
- end
29
- rescue StandardError => e
30
- Rails.logger.warn("RailsExcelReporter: Failed to configure app/reports path: #{e.message}") if Rails.logger
31
- end
16
+ configure_reports_path if rails_application_available?
17
+ end
18
+
19
+ private
20
+
21
+ def self.rails_application_available?
22
+ defined?(Rails.application) && Rails.application
23
+ end
24
+
25
+ def self.configure_reports_path
26
+ reports_path = Rails.root.join 'app/reports'
27
+ setup_reports_path reports_path if Rails.application.paths['app/reports']
28
+ rescue StandardError => e
29
+ log_configuration_warning e
30
+ end
31
+
32
+ def self.setup_reports_path(reports_path)
33
+ app_reports_paths = Rails.application.paths['app/reports']
34
+
35
+ unless app_reports_paths.paths.include? reports_path.to_s
36
+ app_reports_paths << reports_path.to_s
32
37
  end
38
+
39
+ app_reports_paths.eager_load! if app_reports_paths.respond_to? :eager_load!
40
+ end
41
+
42
+ def self.log_configuration_warning(error)
43
+ return unless Rails.logger
44
+
45
+ Rails.logger.warn "RailsExcelReporter: Failed to configure app/reports path: #{error.message}"
33
46
  end
34
47
  end
35
48
  end
@@ -1,7 +1,7 @@
1
1
  module RailsExcelReporter
2
2
  module Streaming
3
3
  def self.included(base)
4
- base.extend(ClassMethods)
4
+ base.extend ClassMethods
5
5
  end
6
6
 
7
7
  module ClassMethods
@@ -21,19 +21,23 @@ module RailsExcelReporter
21
21
  def collection_size
22
22
  return @collection_size if defined?(@collection_size)
23
23
 
24
- @collection_size = if @collection.respond_to?(:count)
25
- @collection.count
26
- elsif @collection.respond_to?(:size)
27
- @collection.size
28
- elsif @collection.respond_to?(:length)
29
- @collection.length
30
- else
31
- @collection.to_a.size
32
- end
24
+ @collection_size = calculate_collection_size
33
25
  end
34
26
 
27
+ private
28
+
29
+ def calculate_collection_size
30
+ return @collection.count if @collection.respond_to? :count
31
+ return @collection.size if @collection.respond_to? :size
32
+ return @collection.length if @collection.respond_to? :length
33
+
34
+ @collection.to_a.size
35
+ end
36
+
37
+ public
38
+
35
39
  def stream_data(&block)
36
- return enum_for(:stream_data) unless block_given?
40
+ return enum_for :stream_data unless block_given?
37
41
 
38
42
  if should_stream?
39
43
  stream_large_dataset(&block)
@@ -43,27 +47,26 @@ module RailsExcelReporter
43
47
  end
44
48
 
45
49
  def with_progress_tracking
46
- return enum_for(:with_progress_tracking) unless block_given?
47
-
48
- total = collection_size
49
- current = 0
50
+ return enum_for :with_progress_tracking unless block_given?
50
51
 
52
+ total, current = collection_size, 0
51
53
  stream_data do |item|
52
54
  current += 1
53
- progress = OpenStruct.new(current: current, total: total, percentage: (current.to_f / total * 100).round(2))
54
-
55
- @progress_callback&.call(progress)
56
-
55
+ progress = build_progress_info current, total
56
+ @progress_callback&.call progress
57
57
  yield item, progress
58
58
  end
59
59
  end
60
60
 
61
- private
61
+ def build_progress_info(current, total)
62
+ percentage = (current.to_f / total * 100).round 2
63
+ OpenStruct.new current: current, total: total, percentage: percentage
64
+ end
62
65
 
63
66
  def stream_large_dataset(&block)
64
- if @collection.respond_to?(:find_each)
67
+ if @collection.respond_to? :find_each
65
68
  @collection.find_each(batch_size: 1000, &block)
66
- elsif @collection.respond_to?(:each)
69
+ elsif @collection.respond_to? :each
67
70
  @collection.each(&block)
68
71
  else
69
72
  @collection.to_a.each(&block)
@@ -71,7 +74,7 @@ module RailsExcelReporter
71
74
  end
72
75
 
73
76
  def stream_small_dataset(&block)
74
- if @collection.respond_to?(:each)
77
+ if @collection.respond_to? :each
75
78
  @collection.each(&block)
76
79
  else
77
80
  @collection.to_a.each(&block)
@@ -1,7 +1,7 @@
1
1
  module RailsExcelReporter
2
2
  module Styling
3
3
  def self.included(base)
4
- base.extend(ClassMethods)
4
+ base.extend ClassMethods
5
5
  end
6
6
 
7
7
  module ClassMethods
@@ -16,7 +16,7 @@ module RailsExcelReporter
16
16
 
17
17
  def inherited(subclass)
18
18
  super
19
- subclass.instance_variable_set(:@styles, @styles.dup) if @styles
19
+ subclass.instance_variable_set :@styles, @styles.dup if @styles
20
20
  end
21
21
  end
22
22
 
@@ -24,36 +24,37 @@ module RailsExcelReporter
24
24
  style_options = self.class.styles[style_name.to_sym] || {}
25
25
  return unless style_options.any?
26
26
 
27
- worksheet.add_style(cell_range, style_options)
27
+ worksheet.add_style cell_range, style_options
28
28
  end
29
29
 
30
30
  def build_caxlsx_style(style_options)
31
31
  caxlsx_options = {}
32
+ apply_style_mappings caxlsx_options, style_options
33
+ caxlsx_options
34
+ end
32
35
 
33
- caxlsx_options[:bg_color] = style_options[:bg_color] if style_options[:bg_color]
34
-
35
- caxlsx_options[:fg_color] = style_options[:fg_color] if style_options[:fg_color]
36
-
37
- caxlsx_options[:b] = style_options[:bold] if style_options[:bold]
38
-
39
- caxlsx_options[:i] = style_options[:italic] if style_options[:italic]
40
-
41
- caxlsx_options[:alignment] = style_options[:alignment] if style_options[:alignment]
42
-
43
- caxlsx_options[:border] = style_options[:border] if style_options[:border]
44
-
45
- caxlsx_options[:sz] = style_options[:font_size] if style_options[:font_size]
36
+ private
46
37
 
47
- caxlsx_options[:font_name] = style_options[:font_name] if style_options[:font_name]
38
+ def apply_style_mappings(caxlsx_options, style_options)
39
+ style_mappings.each do |from_key, to_key|
40
+ caxlsx_options[to_key] = style_options[from_key] if style_options[from_key]
41
+ end
42
+ end
48
43
 
49
- caxlsx_options
44
+ def style_mappings
45
+ {
46
+ bg_color: :bg_color, fg_color: :fg_color, bold: :b, italic: :i,
47
+ alignment: :alignment, border: :border, font_size: :sz, font_name: :font_name
48
+ }
50
49
  end
51
50
 
51
+ public
52
+
52
53
  def merge_styles(*style_names)
53
54
  merged = {}
54
55
  style_names.each do |style_name|
55
56
  style_options = self.class.styles[style_name.to_sym] || {}
56
- merged = deep_merge_hashes(merged, style_options)
57
+ merged = deep_merge_hashes merged, style_options
57
58
  end
58
59
  merged
59
60
  end
@@ -61,13 +62,13 @@ module RailsExcelReporter
61
62
  def get_column_style(column_name)
62
63
  column_style = self.class.styles[column_name.to_sym] || {}
63
64
  default_style = RailsExcelReporter.config.default_styles[:cell] || {}
64
- deep_merge_hashes(default_style, column_style)
65
+ deep_merge_hashes default_style, column_style
65
66
  end
66
67
 
67
68
  def get_header_style
68
69
  header_style = self.class.styles[:header] || {}
69
70
  default_style = RailsExcelReporter.config.default_styles[:header] || {}
70
- deep_merge_hashes(default_style, header_style)
71
+ deep_merge_hashes default_style, header_style
71
72
  end
72
73
 
73
74
  private
@@ -75,13 +76,17 @@ module RailsExcelReporter
75
76
  def deep_merge_hashes(hash1, hash2)
76
77
  result = hash1.dup
77
78
  hash2.each do |key, value|
78
- result[key] = if result[key].is_a?(Hash) && value.is_a?(Hash)
79
- deep_merge_hashes(result[key], value)
80
- else
81
- value
82
- end
79
+ result[key] = merge_hash_value result[key], value
83
80
  end
84
81
  result
85
82
  end
83
+
84
+ def merge_hash_value(existing_value, new_value)
85
+ if existing_value.is_a?(Hash) && new_value.is_a?(Hash)
86
+ deep_merge_hashes existing_value, new_value
87
+ else
88
+ new_value
89
+ end
90
+ end
86
91
  end
87
92
  end
@@ -1,3 +1,3 @@
1
1
  module RailsExcelReporter
2
- VERSION = '0.3.0'
2
+ VERSION = '1.0.0'
3
3
  end
@@ -10,7 +10,7 @@ Gem::Specification.new do |spec|
10
10
  spec.description = 'A Ruby gem that integrates seamlessly with Ruby on Rails to generate Excel reports using a simple DSL. Features include streaming, styling, callbacks, and Rails helpers.'
11
11
  spec.homepage = 'https://github.com/EliSebastian/rails-excel-reporter.git'
12
12
  spec.license = 'MIT'
13
- spec.required_ruby_version = Gem::Requirement.new('>= 2.7.0')
13
+ spec.required_ruby_version = Gem::Requirement.new '>= 3.2.0'
14
14
 
15
15
  spec.metadata['allowed_push_host'] = 'https://rubygems.org'
16
16
  spec.metadata['homepage_uri'] = spec.homepage
@@ -22,14 +22,13 @@ Gem::Specification.new do |spec|
22
22
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
23
23
  spec.require_paths = ['lib']
24
24
 
25
- spec.add_dependency 'activesupport', '>= 7.0'
25
+ spec.add_dependency 'activesupport', '>= 8.0'
26
26
  spec.add_dependency 'caxlsx', '~> 4.0'
27
- spec.add_dependency 'rails', '>= 7.0'
27
+ spec.add_dependency 'rails', '>= 8.0'
28
28
 
29
29
  spec.add_development_dependency 'pry', '~> 0.14'
30
30
  spec.add_development_dependency 'rspec', '~> 3.0'
31
31
  spec.add_development_dependency 'rspec-rails', '~> 5.0'
32
- spec.add_development_dependency 'rubocop', '~> 1.0'
33
32
  spec.add_development_dependency 'simplecov', '~> 0.21'
34
33
  spec.add_development_dependency 'sqlite3', '~> 1.4'
35
34
  spec.add_development_dependency 'yard', '~> 0.9'
@@ -1,7 +1,7 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  RSpec.describe RailsExcelReporter::Base do
4
- let(:sample_data) do
4
+ let :sample_data do
5
5
  [
6
6
  OpenStruct.new(id: 1, name: 'John Doe', email: 'john@example.com', created_at: Time.parse('2024-01-01')),
7
7
  OpenStruct.new(id: 2, name: 'Jane Smith', email: 'jane@example.com', created_at: Time.parse('2024-01-02')),
@@ -9,8 +9,8 @@ RSpec.describe RailsExcelReporter::Base do
9
9
  ]
10
10
  end
11
11
 
12
- let(:report_class) do
13
- Class.new(RailsExcelReporter::Base) do
12
+ let :report_class do
13
+ Class.new RailsExcelReporter::Base do
14
14
  attributes :id, :name, :email
15
15
 
16
16
  def name
@@ -19,7 +19,7 @@ RSpec.describe RailsExcelReporter::Base do
19
19
  end
20
20
  end
21
21
 
22
- let(:report) { report_class.new(sample_data) }
22
+ let(:report) { report_class.new sample_data }
23
23
 
24
24
  describe 'class methods' do
25
25
  describe '.attributes' do
@@ -34,7 +34,7 @@ RSpec.describe RailsExcelReporter::Base do
34
34
 
35
35
  describe '.attribute' do
36
36
  it 'adds a single attribute with custom header' do
37
- klass = Class.new(RailsExcelReporter::Base)
37
+ klass = Class.new RailsExcelReporter::Base
38
38
  klass.attribute :custom_field, header: 'Custom Header'
39
39
 
40
40
  expect(klass.attributes).to contain_exactly(
@@ -51,7 +51,7 @@ RSpec.describe RailsExcelReporter::Base do
51
51
  end
52
52
 
53
53
  it 'accepts custom worksheet name' do
54
- custom_report = report_class.new(sample_data, worksheet_name: 'Custom Sheet')
54
+ custom_report = report_class.new sample_data, worksheet_name: 'Custom Sheet'
55
55
  expect(custom_report.worksheet_name).to eq('Custom Sheet')
56
56
  end
57
57
 
@@ -94,8 +94,8 @@ RSpec.describe RailsExcelReporter::Base do
94
94
  end
95
95
 
96
96
  it 'returns true for large collections' do
97
- large_data = (1..2000).map { |i| OpenStruct.new(id: i, name: "User #{i}") }
98
- large_report = report_class.new(large_data)
97
+ large_data = (1..2000).map { |i| OpenStruct.new id: i, name: "User #{i}" }
98
+ large_report = report_class.new large_data
99
99
  expect(large_report.should_stream?).to be true
100
100
  end
101
101
  end
@@ -116,10 +116,10 @@ RSpec.describe RailsExcelReporter::Base do
116
116
 
117
117
  it 'saves to file' do
118
118
  temp_path = '/tmp/test_report.xlsx'
119
- report.save_to(temp_path)
119
+ report.save_to temp_path
120
120
  expect(File.exist?(temp_path)).to be true
121
121
  expect(File.size(temp_path)).to be > 0
122
- File.delete(temp_path)
122
+ File.delete temp_path
123
123
  end
124
124
  end
125
125
 
@@ -135,16 +135,16 @@ RSpec.describe RailsExcelReporter::Base do
135
135
 
136
136
  describe 'error handling' do
137
137
  it 'raises error when no attributes are defined' do
138
- empty_class = Class.new(RailsExcelReporter::Base)
139
- empty_report = empty_class.new([])
138
+ empty_class = Class.new RailsExcelReporter::Base
139
+ empty_report = empty_class.new []
140
140
 
141
141
  expect { empty_report.to_xlsx }.to raise_error(RuntimeError, /No attributes defined/)
142
142
  end
143
143
  end
144
144
 
145
145
  describe 'callbacks' do
146
- let(:callback_class) do
147
- Class.new(RailsExcelReporter::Base) do
146
+ let :callback_class do
147
+ Class.new RailsExcelReporter::Base do
148
148
  attributes :id, :name
149
149
 
150
150
  attr_reader :before_render_called, :after_render_called,
@@ -176,7 +176,7 @@ RSpec.describe RailsExcelReporter::Base do
176
176
  end
177
177
  end
178
178
 
179
- let(:callback_report) { callback_class.new(sample_data) }
179
+ let(:callback_report) { callback_class.new sample_data }
180
180
 
181
181
  it 'calls all callbacks in correct order' do
182
182
  callback_report.to_xlsx
@@ -1,7 +1,7 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  RSpec.describe RailsExcelReporter::ControllerHelpers do
4
- let(:controller_class) do
4
+ let :controller_class do
5
5
  Class.new do
6
6
  include RailsExcelReporter::ControllerHelpers
7
7
 
@@ -24,7 +24,7 @@ RSpec.describe RailsExcelReporter::ControllerHelpers do
24
24
  end
25
25
  end
26
26
 
27
- let(:mock_response) do
27
+ let :mock_response do
28
28
  Class.new do
29
29
  attr_accessor :headers, :body
30
30
 
@@ -36,28 +36,28 @@ RSpec.describe RailsExcelReporter::ControllerHelpers do
36
36
 
37
37
  let(:controller) { controller_class.new }
38
38
 
39
- let(:report_class) do
40
- Class.new(RailsExcelReporter::Base) do
39
+ let :report_class do
40
+ Class.new RailsExcelReporter::Base do
41
41
  attributes :id, :name
42
42
  end
43
43
  end
44
44
 
45
- let(:sample_data) do
45
+ let :sample_data do
46
46
  [
47
47
  OpenStruct.new(id: 1, name: 'John'),
48
48
  OpenStruct.new(id: 2, name: 'Jane')
49
49
  ]
50
50
  end
51
51
 
52
- let(:report) { report_class.new(sample_data) }
52
+ let(:report) { report_class.new sample_data }
53
53
 
54
54
  before do
55
- stub_const('MockResponse', mock_response)
55
+ stub_const 'MockResponse', mock_response
56
56
  end
57
57
 
58
58
  describe '#send_excel_report' do
59
59
  it 'sends Excel data with default filename' do
60
- controller.send_excel_report(report)
60
+ controller.send_excel_report report
61
61
 
62
62
  expect(controller.sent_data).to be_a(String)
63
63
  expect(controller.send_options[:filename]).to match(/\.xlsx$/)
@@ -66,13 +66,13 @@ RSpec.describe RailsExcelReporter::ControllerHelpers do
66
66
  end
67
67
 
68
68
  it 'sends Excel data with custom filename' do
69
- controller.send_excel_report(report, filename: 'custom_report.xlsx')
69
+ controller.send_excel_report report, filename: 'custom_report.xlsx'
70
70
 
71
71
  expect(controller.send_options[:filename]).to eq('custom_report.xlsx')
72
72
  end
73
73
 
74
74
  it 'sends Excel data with custom disposition' do
75
- controller.send_excel_report(report, disposition: 'inline')
75
+ controller.send_excel_report report, disposition: 'inline'
76
76
 
77
77
  expect(controller.send_options[:disposition]).to eq('inline')
78
78
  end
@@ -80,9 +80,10 @@ RSpec.describe RailsExcelReporter::ControllerHelpers do
80
80
 
81
81
  describe '#stream_excel_report' do
82
82
  it 'sets up streaming response headers' do
83
- controller.stream_excel_report(report)
83
+ controller.stream_excel_report report
84
84
 
85
- expect(controller.response.headers['Content-Type']).to eq('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
85
+ expected_content_type = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
86
+ expect(controller.response.headers['Content-Type']).to eq(expected_content_type)
86
87
  expect(controller.response.headers['Content-Disposition']).to match(/attachment; filename=/)
87
88
  expect(controller.response.headers['Content-Transfer-Encoding']).to eq('binary')
88
89
  expect(controller.response.headers['Last-Modified']).to be_present
@@ -90,7 +91,7 @@ RSpec.describe RailsExcelReporter::ControllerHelpers do
90
91
  end
91
92
 
92
93
  it 'uses custom filename in Content-Disposition' do
93
- controller.stream_excel_report(report, filename: 'custom_stream.xlsx')
94
+ controller.stream_excel_report report, filename: 'custom_stream.xlsx'
94
95
 
95
96
  expect(controller.response.headers['Content-Disposition']).to include('custom_stream.xlsx')
96
97
  end
@@ -101,21 +102,21 @@ RSpec.describe RailsExcelReporter::ControllerHelpers do
101
102
  allow(report).to receive(:should_stream?).and_return(false)
102
103
  expect(controller).to receive(:send_excel_report).with(report, {})
103
104
 
104
- controller.excel_report_response(report)
105
+ controller.excel_report_response report
105
106
  end
106
107
 
107
108
  it 'uses stream_excel_report for large reports' do
108
109
  allow(report).to receive(:should_stream?).and_return(true)
109
110
  expect(controller).to receive(:stream_excel_report).with(report, {})
110
111
 
111
- controller.excel_report_response(report)
112
+ controller.excel_report_response report
112
113
  end
113
114
 
114
115
  it 'passes options to the appropriate method' do
115
116
  allow(report).to receive(:should_stream?).and_return(false)
116
117
  expect(controller).to receive(:send_excel_report).with(report, { filename: 'test.xlsx' })
117
118
 
118
- controller.excel_report_response(report, filename: 'test.xlsx')
119
+ controller.excel_report_response report, filename: 'test.xlsx'
119
120
  end
120
121
  end
121
122
  end
@@ -1,16 +1,16 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  RSpec.describe RailsExcelReporter::Streaming do
4
- let(:small_data) do
5
- (1..10).map { |i| OpenStruct.new(id: i, name: "User #{i}") }
4
+ let :small_data do
5
+ (1..10).map { |i| OpenStruct.new id: i, name: "User #{i}" }
6
6
  end
7
7
 
8
- let(:large_data) do
9
- (1..2000).map { |i| OpenStruct.new(id: i, name: "User #{i}") }
8
+ let :large_data do
9
+ (1..2000).map { |i| OpenStruct.new id: i, name: "User #{i}" }
10
10
  end
11
11
 
12
- let(:report_class) do
13
- Class.new(RailsExcelReporter::Base) do
12
+ let :report_class do
13
+ Class.new RailsExcelReporter::Base do
14
14
  attributes :id, :name
15
15
  end
16
16
  end
@@ -28,44 +28,44 @@ RSpec.describe RailsExcelReporter::Streaming do
28
28
 
29
29
  describe '#should_stream?' do
30
30
  it 'returns false for small collections' do
31
- report = report_class.new(small_data)
31
+ report = report_class.new small_data
32
32
  expect(report.should_stream?).to be false
33
33
  end
34
34
 
35
35
  it 'returns true for large collections' do
36
- report = report_class.new(large_data)
36
+ report = report_class.new large_data
37
37
  expect(report.should_stream?).to be true
38
38
  end
39
39
  end
40
40
 
41
41
  describe '#collection_size' do
42
42
  it 'calculates size for arrays' do
43
- report = report_class.new(small_data)
43
+ report = report_class.new small_data
44
44
  expect(report.collection_size).to eq(10)
45
45
  end
46
46
 
47
47
  it 'uses count method when available' do
48
- mock_collection = double('Collection')
48
+ mock_collection = double 'Collection'
49
49
  allow(mock_collection).to receive(:count).and_return(100)
50
50
 
51
- report = report_class.new(mock_collection)
51
+ report = report_class.new mock_collection
52
52
  expect(report.collection_size).to eq(100)
53
53
  end
54
54
 
55
55
  it 'falls back to size method' do
56
- mock_collection = double('Collection')
56
+ mock_collection = double 'Collection'
57
57
  allow(mock_collection).to receive(:respond_to?).with(:count).and_return(false)
58
58
  allow(mock_collection).to receive(:respond_to?).with(:size).and_return(true)
59
59
  allow(mock_collection).to receive(:size).and_return(50)
60
60
 
61
- report = report_class.new(mock_collection)
61
+ report = report_class.new mock_collection
62
62
  expect(report.collection_size).to eq(50)
63
63
  end
64
64
  end
65
65
 
66
66
  describe '#stream_data' do
67
67
  it 'yields each item in small collections' do
68
- report = report_class.new(small_data)
68
+ report = report_class.new small_data
69
69
  yielded_items = []
70
70
 
71
71
  report.stream_data do |item|
@@ -76,7 +76,7 @@ RSpec.describe RailsExcelReporter::Streaming do
76
76
  end
77
77
 
78
78
  it 'uses find_each for large ActiveRecord-like collections' do
79
- mock_collection = double('ActiveRecord Collection')
79
+ mock_collection = double 'ActiveRecord Collection'
80
80
  allow(mock_collection).to receive(:respond_to?).with(:count).and_return(true)
81
81
  allow(mock_collection).to receive(:count).and_return(2000)
82
82
  allow(mock_collection).to receive(:respond_to?).with(:find_each).and_return(true)
@@ -86,7 +86,7 @@ RSpec.describe RailsExcelReporter::Streaming do
86
86
  large_data.each(&block)
87
87
  end
88
88
 
89
- report = report_class.new(mock_collection)
89
+ report = report_class.new mock_collection
90
90
 
91
91
  report.stream_data do |item|
92
92
  yielded_items << item
@@ -96,7 +96,7 @@ RSpec.describe RailsExcelReporter::Streaming do
96
96
  end
97
97
 
98
98
  it 'returns enumerator when no block given' do
99
- report = report_class.new(small_data)
99
+ report = report_class.new small_data
100
100
  enumerator = report.stream_data
101
101
 
102
102
  expect(enumerator).to be_a(Enumerator)
@@ -106,7 +106,7 @@ RSpec.describe RailsExcelReporter::Streaming do
106
106
 
107
107
  describe '#with_progress_tracking' do
108
108
  it 'tracks progress and yields items with progress info' do
109
- report = report_class.new(small_data)
109
+ report = report_class.new small_data
110
110
  progress_updates = []
111
111
 
112
112
  report.with_progress_tracking do |_item, progress|
@@ -1,8 +1,8 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  RSpec.describe RailsExcelReporter::Styling do
4
- let(:styled_class) do
5
- Class.new(RailsExcelReporter::Base) do
4
+ let :styled_class do
5
+ Class.new RailsExcelReporter::Base do
6
6
  attributes :id, :name, :email
7
7
 
8
8
  style :header, {
@@ -22,14 +22,14 @@ RSpec.describe RailsExcelReporter::Styling do
22
22
  end
23
23
  end
24
24
 
25
- let(:sample_data) do
25
+ let :sample_data do
26
26
  [
27
27
  OpenStruct.new(id: 1, name: 'John', email: 'john@example.com'),
28
28
  OpenStruct.new(id: 2, name: 'Jane', email: 'jane@example.com')
29
29
  ]
30
30
  end
31
31
 
32
- let(:report) { styled_class.new(sample_data) }
32
+ let(:report) { styled_class.new sample_data }
33
33
 
34
34
  describe 'class methods' do
35
35
  describe '.style' do
@@ -66,12 +66,12 @@ RSpec.describe RailsExcelReporter::Styling do
66
66
 
67
67
  describe '#get_column_style' do
68
68
  it 'returns merged column style with defaults' do
69
- id_style = report.get_column_style(:id)
69
+ id_style = report.get_column_style :id
70
70
  expect(id_style[:alignment]).to eq({ horizontal: :center })
71
71
  end
72
72
 
73
73
  it 'returns default style for undefined columns' do
74
- email_style = report.get_column_style(:email)
74
+ email_style = report.get_column_style :email
75
75
  expect(email_style).to eq(RailsExcelReporter.config.default_styles[:cell])
76
76
  end
77
77
  end
@@ -86,7 +86,7 @@ RSpec.describe RailsExcelReporter::Styling do
86
86
  alignment: { horizontal: :center }
87
87
  }
88
88
 
89
- caxlsx_style = report.build_caxlsx_style(style_options)
89
+ caxlsx_style = report.build_caxlsx_style style_options
90
90
 
91
91
  expect(caxlsx_style[:bg_color]).to eq('FF0000')
92
92
  expect(caxlsx_style[:fg_color]).to eq('FFFFFF')
@@ -101,7 +101,7 @@ RSpec.describe RailsExcelReporter::Styling do
101
101
  styled_class.style :base, { bold: true, font_size: 10 }
102
102
  styled_class.style :override, { font_size: 12, italic: true }
103
103
 
104
- merged = report.merge_styles(:base, :override)
104
+ merged = report.merge_styles :base, :override
105
105
 
106
106
  expect(merged[:bold]).to be true
107
107
  expect(merged[:font_size]).to eq(12)
@@ -112,7 +112,7 @@ RSpec.describe RailsExcelReporter::Styling do
112
112
 
113
113
  describe 'inheritance' do
114
114
  it 'inherits styles from parent class' do
115
- child_class = Class.new(styled_class) do
115
+ child_class = Class.new styled_class do
116
116
  style :email, { italic: true }
117
117
  end
118
118
 
data/spec/spec_helper.rb CHANGED
@@ -24,7 +24,7 @@ RSpec.configure do |config|
24
24
  config.order = :random
25
25
  Kernel.srand config.seed
26
26
 
27
- config.before(:each) do
28
- RailsExcelReporter.instance_variable_set(:@configuration, nil)
27
+ config.before :each do
28
+ RailsExcelReporter.instance_variable_set :@configuration, nil
29
29
  end
30
30
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails-excel-reporter
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Elí Sebastian Herrera Aguilar
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-07-14 00:00:00.000000000 Z
11
+ date: 2025-07-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '7.0'
19
+ version: '8.0'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: '7.0'
26
+ version: '8.0'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: caxlsx
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -44,14 +44,14 @@ dependencies:
44
44
  requirements:
45
45
  - - ">="
46
46
  - !ruby/object:Gem::Version
47
- version: '7.0'
47
+ version: '8.0'
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
- version: '7.0'
54
+ version: '8.0'
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: pry
57
57
  requirement: !ruby/object:Gem::Requirement
@@ -94,20 +94,6 @@ dependencies:
94
94
  - - "~>"
95
95
  - !ruby/object:Gem::Version
96
96
  version: '5.0'
97
- - !ruby/object:Gem::Dependency
98
- name: rubocop
99
- requirement: !ruby/object:Gem::Requirement
100
- requirements:
101
- - - "~>"
102
- - !ruby/object:Gem::Version
103
- version: '1.0'
104
- type: :development
105
- prerelease: false
106
- version_requirements: !ruby/object:Gem::Requirement
107
- requirements:
108
- - - "~>"
109
- - !ruby/object:Gem::Version
110
- version: '1.0'
111
97
  - !ruby/object:Gem::Dependency
112
98
  name: simplecov
113
99
  requirement: !ruby/object:Gem::Requirement
@@ -159,7 +145,6 @@ executables: []
159
145
  extensions: []
160
146
  extra_rdoc_files: []
161
147
  files:
162
- - CLAUDE.md
163
148
  - Gemfile
164
149
  - Gemfile.lock
165
150
  - README.md
@@ -197,14 +182,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
197
182
  requirements:
198
183
  - - ">="
199
184
  - !ruby/object:Gem::Version
200
- version: 2.7.0
185
+ version: 3.2.0
201
186
  required_rubygems_version: !ruby/object:Gem::Requirement
202
187
  requirements:
203
188
  - - ">="
204
189
  - !ruby/object:Gem::Version
205
190
  version: '0'
206
191
  requirements: []
207
- rubygems_version: 3.5.11
192
+ rubygems_version: 3.4.1
208
193
  signing_key:
209
194
  specification_version: 4
210
195
  summary: Generate Excel reports (.xlsx) in Rails with a simple DSL
data/CLAUDE.md DELETED
@@ -1,94 +0,0 @@
1
- # CLAUDE.md
2
-
3
- This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
-
5
- ## Project Overview
6
-
7
- This is a Ruby gem called `rails-excel-reporter` that provides a simple DSL for generating Excel reports (.xlsx format) in Ruby on Rails applications. The gem uses the `caxlsx` library (version 4.x) for Excel generation and includes features like streaming for large datasets, custom styling, callbacks, and Rails controller helpers.
8
-
9
- ## Development Commands
10
-
11
- ### Testing
12
- ```bash
13
- # Run all tests
14
- bundle exec rspec
15
-
16
- # Run specific test file
17
- bundle exec rspec spec/rails_excel_reporter/base_spec.rb
18
-
19
- # Run tests with documentation format
20
- bundle exec rspec --format documentation
21
- ```
22
-
23
- ### Code Quality
24
- ```bash
25
- # Run linter
26
- bundle exec rubocop
27
-
28
- # Generate documentation
29
- bundle exec yard
30
- ```
31
-
32
- ### Build and Install
33
- ```bash
34
- # Build the gem
35
- gem build rails_excel_reporter.gemspec
36
-
37
- # Install locally
38
- gem install rails-excel-reporter-0.1.0.gem
39
- ```
40
-
41
- ## Architecture
42
-
43
- ### Core Components
44
-
45
- 1. **`RailsExcelReporter::Base`** (`lib/rails_excel_reporter/base.rb`) - Main class that report classes inherit from. Contains the core DSL and rendering logic.
46
-
47
- 2. **Modules:**
48
- - **`Styling`** (`lib/rails_excel_reporter/styling.rb`) - Handles custom styling for headers, columns, and cells
49
- - **`Streaming`** (`lib/rails_excel_reporter/streaming.rb`) - Manages streaming for large datasets with configurable thresholds
50
- - **`ControllerHelpers`** (`lib/rails_excel_reporter/controller_helpers.rb`) - Provides Rails controller methods for sending Excel responses
51
-
52
- 3. **`Configuration`** (`lib/rails_excel_reporter/configuration.rb`) - Global configuration management with defaults for styling, streaming thresholds, and file paths
53
-
54
- 4. **`ReportGenerator`** (`lib/generators/report/report_generator.rb`) - Rails generator for scaffolding new report classes
55
-
56
- ### Key Patterns
57
-
58
- - **DSL Design**: Uses `attributes` class method to define report columns with support for custom headers and methods
59
- - **Streaming**: Automatically streams large datasets (>1000 records by default) using `find_each` for ActiveRecord or manual iteration
60
- - **Styling**: Supports custom styling through the `style` class method with conversion to caxlsx format
61
- - **Callbacks**: Provides `before_render`, `after_render`, `before_row`, and `after_row` hooks
62
- - **Flexible Data Sources**: Works with ActiveRecord collections, arrays, and any enumerable
63
-
64
- ### Report Class Structure
65
-
66
- ```ruby
67
- class MyReport < RailsExcelReporter::Base
68
- attributes :column1, :column2, { name: :column3, header: "Custom Header" }
69
-
70
- style :header, { bg_color: "4472C4", fg_color: "FFFFFF", bold: true }
71
- style :column1, { alignment: { horizontal: :center } }
72
-
73
- # Custom attribute methods override default behavior
74
- def column1
75
- # Access current record via `object`
76
- object.column1.upcase
77
- end
78
- end
79
- ```
80
-
81
- ### Error Handling
82
-
83
- The gem defines specific error classes:
84
- - `RailsExcelReporter::Error` - Base error class
85
- - `AttributeNotFoundError` - When an attribute doesn't exist on the data source
86
- - `InvalidConfigurationError` - For configuration issues
87
-
88
- ## File Structure
89
-
90
- - `lib/rails_excel_reporter.rb` - Main entry point and module definition
91
- - `lib/rails_excel_reporter/` - Core gem modules
92
- - `lib/generators/` - Rails generator for creating report classes
93
- - `spec/` - RSpec test suite
94
- - `rails_excel_reporter.gemspec` - Gem specification