rails-excel-reporter 0.1.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 +7 -0
- data/CLAUDE.md +94 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +306 -0
- data/README.md +506 -0
- data/lib/generators/report/report_generator.rb +32 -0
- data/lib/generators/report/templates/report.rb.erb +43 -0
- data/lib/rails_excel_reporter/base.rb +210 -0
- data/lib/rails_excel_reporter/configuration.rb +40 -0
- data/lib/rails_excel_reporter/controller_helpers.rb +34 -0
- data/lib/rails_excel_reporter/railtie.rb +35 -0
- data/lib/rails_excel_reporter/streaming.rb +81 -0
- data/lib/rails_excel_reporter/styling.rb +87 -0
- data/lib/rails_excel_reporter/version.rb +3 -0
- data/lib/rails_excel_reporter.rb +19 -0
- data/rails_excel_reporter.gemspec +36 -0
- data/spec/rails_excel_reporter/base_spec.rb +190 -0
- data/spec/rails_excel_reporter/configuration_spec.rb +75 -0
- data/spec/rails_excel_reporter/controller_helpers_spec.rb +121 -0
- data/spec/rails_excel_reporter/streaming_spec.rb +139 -0
- data/spec/rails_excel_reporter/styling_spec.rb +124 -0
- data/spec/rails_excel_reporter_spec.rb +25 -0
- data/spec/spec_helper.rb +30 -0
- metadata +211 -0
@@ -0,0 +1,210 @@
|
|
1
|
+
require 'caxlsx'
|
2
|
+
require 'tempfile'
|
3
|
+
require 'ostruct'
|
4
|
+
require 'stringio'
|
5
|
+
require_relative 'styling'
|
6
|
+
require_relative 'streaming'
|
7
|
+
|
8
|
+
module RailsExcelReporter
|
9
|
+
class Base
|
10
|
+
include Styling
|
11
|
+
include Streaming
|
12
|
+
|
13
|
+
attr_reader :collection, :worksheet_name, :progress_callback
|
14
|
+
|
15
|
+
def initialize(collection, worksheet_name: nil, &progress_callback)
|
16
|
+
@collection = collection
|
17
|
+
@worksheet_name = worksheet_name || default_worksheet_name
|
18
|
+
@progress_callback = progress_callback
|
19
|
+
@rendered = false
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.attributes(*attrs)
|
23
|
+
if attrs.empty?
|
24
|
+
@attributes ||= []
|
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
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.symbolize_hash_keys(hash)
|
40
|
+
result = {}
|
41
|
+
hash.each do |key, value|
|
42
|
+
result[key.to_sym] = value
|
43
|
+
end
|
44
|
+
result
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.attribute(name, options = {})
|
48
|
+
@attributes ||= []
|
49
|
+
@attributes << { name: name.to_sym, header: options[:header] || name.to_s.humanize }
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.inherited(subclass)
|
53
|
+
super
|
54
|
+
subclass.instance_variable_set(:@attributes, @attributes.dup) if @attributes
|
55
|
+
end
|
56
|
+
|
57
|
+
def attributes
|
58
|
+
self.class.attributes
|
59
|
+
end
|
60
|
+
|
61
|
+
def file
|
62
|
+
render unless @rendered
|
63
|
+
@tempfile
|
64
|
+
end
|
65
|
+
|
66
|
+
def to_xlsx
|
67
|
+
render unless @rendered
|
68
|
+
@tempfile.rewind
|
69
|
+
@tempfile.read
|
70
|
+
end
|
71
|
+
|
72
|
+
def save_to(path)
|
73
|
+
File.open(path, 'wb') do |file|
|
74
|
+
file.write(to_xlsx)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def filename
|
79
|
+
timestamp = Time.now.strftime(RailsExcelReporter.config.date_format).gsub('-', '_')
|
80
|
+
"#{worksheet_name.parameterize}_report_#{timestamp}.xlsx"
|
81
|
+
end
|
82
|
+
|
83
|
+
def stream
|
84
|
+
StringIO.new(to_xlsx)
|
85
|
+
end
|
86
|
+
|
87
|
+
def to_h
|
88
|
+
{
|
89
|
+
worksheet_name: worksheet_name,
|
90
|
+
attributes: attributes,
|
91
|
+
data: collection_data,
|
92
|
+
collection_size: collection_size,
|
93
|
+
streaming: should_stream?
|
94
|
+
}
|
95
|
+
end
|
96
|
+
|
97
|
+
def before_render; end
|
98
|
+
|
99
|
+
def after_render; end
|
100
|
+
|
101
|
+
def before_row(object); end
|
102
|
+
|
103
|
+
def after_row(object); end
|
104
|
+
|
105
|
+
private
|
106
|
+
|
107
|
+
def default_worksheet_name
|
108
|
+
if self.class.name
|
109
|
+
self.class.name.demodulize.underscore.humanize
|
110
|
+
else
|
111
|
+
'Report'
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def render
|
116
|
+
validate_attributes!
|
117
|
+
|
118
|
+
before_render
|
119
|
+
|
120
|
+
@tempfile = Tempfile.new([filename.gsub('.xlsx', ''), '.xlsx'],
|
121
|
+
RailsExcelReporter.config.temp_directory)
|
122
|
+
|
123
|
+
package = ::Axlsx::Package.new
|
124
|
+
workbook = package.workbook
|
125
|
+
|
126
|
+
worksheet = workbook.add_worksheet(name: worksheet_name)
|
127
|
+
|
128
|
+
add_headers(worksheet)
|
129
|
+
add_data_rows(worksheet)
|
130
|
+
|
131
|
+
package.serialize(@tempfile.path)
|
132
|
+
|
133
|
+
@rendered = true
|
134
|
+
|
135
|
+
after_render
|
136
|
+
end
|
137
|
+
|
138
|
+
def validate_attributes!
|
139
|
+
raise 'No attributes defined. Use `attributes` class method to define columns.' if attributes.empty?
|
140
|
+
end
|
141
|
+
|
142
|
+
def add_headers(worksheet)
|
143
|
+
header_values = attributes.map { |attr| attr[:header] }
|
144
|
+
header_style = build_caxlsx_style(get_header_style)
|
145
|
+
|
146
|
+
if header_style.any?
|
147
|
+
style_id = worksheet.workbook.styles.add_style(header_style)
|
148
|
+
worksheet.add_row(header_values, style: style_id)
|
149
|
+
else
|
150
|
+
worksheet.add_row(header_values)
|
151
|
+
end
|
152
|
+
|
153
|
+
worksheet.auto_filter = "A1:#{column_letter(attributes.size)}1"
|
154
|
+
end
|
155
|
+
|
156
|
+
def add_data_rows(worksheet)
|
157
|
+
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
|
163
|
+
|
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
|
168
|
+
|
169
|
+
worksheet.add_row(row_values, style: row_styles)
|
170
|
+
|
171
|
+
after_row(object)
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
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]
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
attr_reader :object
|
189
|
+
|
190
|
+
def collection_data
|
191
|
+
@collection_data ||= stream_data.map do |item|
|
192
|
+
attributes.map do |attr|
|
193
|
+
get_attribute_value(item, attr[:name])
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
def column_letter(column_number)
|
199
|
+
result = ''
|
200
|
+
while column_number > 0
|
201
|
+
column_number -= 1
|
202
|
+
result = ((column_number % 26) + 65).chr + result
|
203
|
+
column_number /= 26
|
204
|
+
end
|
205
|
+
result
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
ReportBase = Base
|
210
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module RailsExcelReporter
|
2
|
+
class Configuration
|
3
|
+
attr_accessor :default_styles, :date_format, :streaming_threshold, :temp_directory
|
4
|
+
|
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
|
+
}
|
17
|
+
@date_format = '%Y-%m-%d'
|
18
|
+
@streaming_threshold = 1000
|
19
|
+
@temp_directory = nil
|
20
|
+
end
|
21
|
+
|
22
|
+
def temp_directory
|
23
|
+
@temp_directory || Dir.tmpdir
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class << self
|
28
|
+
def configuration
|
29
|
+
@configuration ||= Configuration.new
|
30
|
+
end
|
31
|
+
|
32
|
+
def configure
|
33
|
+
yield(configuration)
|
34
|
+
end
|
35
|
+
|
36
|
+
def config
|
37
|
+
configuration
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module RailsExcelReporter
|
2
|
+
module ControllerHelpers
|
3
|
+
def send_excel_report(report, options = {})
|
4
|
+
filename = options[:filename] || report.filename
|
5
|
+
disposition = options[:disposition] || 'attachment'
|
6
|
+
|
7
|
+
send_data(
|
8
|
+
report.to_xlsx,
|
9
|
+
filename: filename,
|
10
|
+
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
11
|
+
disposition: disposition
|
12
|
+
)
|
13
|
+
end
|
14
|
+
|
15
|
+
def stream_excel_report(report, options = {})
|
16
|
+
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
|
+
|
23
|
+
self.response_body = report.stream
|
24
|
+
end
|
25
|
+
|
26
|
+
def excel_report_response(report, options = {})
|
27
|
+
if report.should_stream?
|
28
|
+
stream_excel_report(report, options)
|
29
|
+
else
|
30
|
+
send_excel_report(report, options)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'rails/railtie'
|
2
|
+
|
3
|
+
module RailsExcelReporter
|
4
|
+
class Railtie < Rails::Railtie
|
5
|
+
initializer 'rails_excel_reporter.configure_rails_initialization' do |app|
|
6
|
+
app.config.paths.add 'app/reports', eager_load: true
|
7
|
+
end
|
8
|
+
|
9
|
+
initializer 'rails_excel_reporter.include_controller_helpers' do
|
10
|
+
ActiveSupport.on_load(:action_controller) do
|
11
|
+
include RailsExcelReporter::ControllerHelpers
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
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
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module RailsExcelReporter
|
2
|
+
module Streaming
|
3
|
+
def self.included(base)
|
4
|
+
base.extend(ClassMethods)
|
5
|
+
end
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
def streaming_threshold
|
9
|
+
@streaming_threshold || RailsExcelReporter.config.streaming_threshold
|
10
|
+
end
|
11
|
+
|
12
|
+
def streaming_threshold=(value)
|
13
|
+
@streaming_threshold = value
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def should_stream?
|
18
|
+
collection_size >= self.class.streaming_threshold
|
19
|
+
end
|
20
|
+
|
21
|
+
def collection_size
|
22
|
+
return @collection_size if defined?(@collection_size)
|
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
|
33
|
+
end
|
34
|
+
|
35
|
+
def stream_data(&block)
|
36
|
+
return enum_for(:stream_data) unless block_given?
|
37
|
+
|
38
|
+
if should_stream?
|
39
|
+
stream_large_dataset(&block)
|
40
|
+
else
|
41
|
+
stream_small_dataset(&block)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def with_progress_tracking
|
46
|
+
return enum_for(:with_progress_tracking) unless block_given?
|
47
|
+
|
48
|
+
total = collection_size
|
49
|
+
current = 0
|
50
|
+
|
51
|
+
stream_data do |item|
|
52
|
+
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
|
+
|
57
|
+
yield item, progress
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def stream_large_dataset(&block)
|
64
|
+
if @collection.respond_to?(:find_each)
|
65
|
+
@collection.find_each(batch_size: 1000, &block)
|
66
|
+
elsif @collection.respond_to?(:each)
|
67
|
+
@collection.each(&block)
|
68
|
+
else
|
69
|
+
@collection.to_a.each(&block)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def stream_small_dataset(&block)
|
74
|
+
if @collection.respond_to?(:each)
|
75
|
+
@collection.each(&block)
|
76
|
+
else
|
77
|
+
@collection.to_a.each(&block)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
module RailsExcelReporter
|
2
|
+
module Styling
|
3
|
+
def self.included(base)
|
4
|
+
base.extend(ClassMethods)
|
5
|
+
end
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
def style(target, options = {})
|
9
|
+
@styles ||= {}
|
10
|
+
@styles[target.to_sym] = options
|
11
|
+
end
|
12
|
+
|
13
|
+
def styles
|
14
|
+
@styles ||= {}
|
15
|
+
end
|
16
|
+
|
17
|
+
def inherited(subclass)
|
18
|
+
super
|
19
|
+
subclass.instance_variable_set(:@styles, @styles.dup) if @styles
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def apply_style(worksheet, cell_range, style_name)
|
24
|
+
style_options = self.class.styles[style_name.to_sym] || {}
|
25
|
+
return unless style_options.any?
|
26
|
+
|
27
|
+
worksheet.add_style(cell_range, style_options)
|
28
|
+
end
|
29
|
+
|
30
|
+
def build_caxlsx_style(style_options)
|
31
|
+
caxlsx_options = {}
|
32
|
+
|
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]
|
46
|
+
|
47
|
+
caxlsx_options[:font_name] = style_options[:font_name] if style_options[:font_name]
|
48
|
+
|
49
|
+
caxlsx_options
|
50
|
+
end
|
51
|
+
|
52
|
+
def merge_styles(*style_names)
|
53
|
+
merged = {}
|
54
|
+
style_names.each do |style_name|
|
55
|
+
style_options = self.class.styles[style_name.to_sym] || {}
|
56
|
+
merged = deep_merge_hashes(merged, style_options)
|
57
|
+
end
|
58
|
+
merged
|
59
|
+
end
|
60
|
+
|
61
|
+
def get_column_style(column_name)
|
62
|
+
column_style = self.class.styles[column_name.to_sym] || {}
|
63
|
+
default_style = RailsExcelReporter.config.default_styles[:cell] || {}
|
64
|
+
deep_merge_hashes(default_style, column_style)
|
65
|
+
end
|
66
|
+
|
67
|
+
def get_header_style
|
68
|
+
header_style = self.class.styles[:header] || {}
|
69
|
+
default_style = RailsExcelReporter.config.default_styles[:header] || {}
|
70
|
+
deep_merge_hashes(default_style, header_style)
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
def deep_merge_hashes(hash1, hash2)
|
76
|
+
result = hash1.dup
|
77
|
+
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
|
83
|
+
end
|
84
|
+
result
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'active_support/core_ext/string'
|
2
|
+
require 'active_support/core_ext/hash'
|
3
|
+
require 'active_support/core_ext/time'
|
4
|
+
require 'tmpdir'
|
5
|
+
|
6
|
+
module RailsExcelReporter
|
7
|
+
class Error < StandardError; end
|
8
|
+
class AttributeNotFoundError < Error; end
|
9
|
+
class InvalidConfigurationError < Error; end
|
10
|
+
end
|
11
|
+
|
12
|
+
require 'rails_excel_reporter/version'
|
13
|
+
require 'rails_excel_reporter/configuration'
|
14
|
+
require 'rails_excel_reporter/styling'
|
15
|
+
require 'rails_excel_reporter/streaming'
|
16
|
+
require 'rails_excel_reporter/base'
|
17
|
+
require 'rails_excel_reporter/controller_helpers'
|
18
|
+
|
19
|
+
require 'rails_excel_reporter/railtie' if defined?(Rails)
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require_relative 'lib/rails_excel_reporter/version'
|
2
|
+
|
3
|
+
Gem::Specification.new do |spec|
|
4
|
+
spec.name = 'rails-excel-reporter'
|
5
|
+
spec.version = RailsExcelReporter::VERSION
|
6
|
+
spec.authors = ['Elí Sebastian Herrera Aguilar']
|
7
|
+
spec.email = ['esrbastianherrera@gmail.com']
|
8
|
+
|
9
|
+
spec.summary = 'Generate Excel reports (.xlsx) in Rails with a simple DSL'
|
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
|
+
spec.homepage = 'https://github.com/EliSebastian/rails-excel-reporter.git'
|
12
|
+
spec.license = 'MIT'
|
13
|
+
spec.required_ruby_version = Gem::Requirement.new('>= 2.7.0')
|
14
|
+
|
15
|
+
spec.metadata['allowed_push_host'] = 'https://rubygems.org'
|
16
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
17
|
+
spec.metadata['source_code_uri'] = 'https://github.com/EliSebastian/rails-excel-reporter.git'
|
18
|
+
spec.metadata['changelog_uri'] = 'https://github.com/EliSebastian/rails-excel-reporter.git/blob/main/CHANGELOG.md'
|
19
|
+
|
20
|
+
spec.files = Dir['{lib,spec}/**/*', '*.md', '*.gemspec', 'Gemfile*']
|
21
|
+
spec.bindir = 'exe'
|
22
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
23
|
+
spec.require_paths = ['lib']
|
24
|
+
|
25
|
+
spec.add_dependency 'activesupport', '~> 8.0'
|
26
|
+
spec.add_dependency 'caxlsx', '~> 4.0'
|
27
|
+
spec.add_dependency 'rails', '~> 8.0'
|
28
|
+
|
29
|
+
spec.add_development_dependency 'pry', '~> 0.14'
|
30
|
+
spec.add_development_dependency 'rspec', '~> 3.0'
|
31
|
+
spec.add_development_dependency 'rspec-rails', '~> 5.0'
|
32
|
+
spec.add_development_dependency 'rubocop', '~> 1.0'
|
33
|
+
spec.add_development_dependency 'simplecov', '~> 0.21'
|
34
|
+
spec.add_development_dependency 'sqlite3', '~> 1.4'
|
35
|
+
spec.add_development_dependency 'yard', '~> 0.9'
|
36
|
+
end
|