Skip to content

Commit f0722c3

Browse files
authored
Adding SmarterCSV.generate; improve behavior of Writer class (#280)
1 parent 1131c98 commit f0722c3

File tree

6 files changed

+87
-38
lines changed

6 files changed

+87
-38
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11

22
# SmarterCSV 1.x Change Log
33

4+
## 1.11.1 (2024-07-05)
5+
* improved behavior of Writer class
6+
* added SmarterCSV.generate shortcut for CSV writing
7+
48
## 1.11.0 (2024-07-02)
59
* added SmarterCSV::Writer to output CSV files ([issue #44](https://github.com/tilo/smarter_csv/issues/44))
610

lib/smarter_csv.rb

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# frozen_string_literal: true
22

33
require "smarter_csv/version"
4+
require "smarter_csv/errors"
5+
46
require "smarter_csv/file_io"
57
require "smarter_csv/options_processing"
68
require "smarter_csv/auto_detection"
@@ -9,8 +11,10 @@
911
require 'smarter_csv/header_validations'
1012
require "smarter_csv/headers"
1113
require "smarter_csv/hash_transformations"
14+
1215
require "smarter_csv/parse"
1316
require "smarter_csv/writer"
17+
require "smarter_csv/smarter_csv"
1418

1519
# load the C-extension:
1620
case RUBY_ENGINE
@@ -49,4 +53,24 @@
4953
BLOCK_COMMENT
5054
end
5155
# :nocov:
52-
require "smarter_csv/smarter_csv"
56+
57+
module SmarterCSV
58+
# SmarterCSV.generate(filename, options) do |csv_writer|
59+
# MyModel.find_in_batches(batch_size: 100) do |batch|
60+
# batch.pluck(:name, :description, :instructor).each do |record|
61+
# csv_writer << record
62+
# end
63+
# end
64+
# end
65+
#
66+
# rubocop:disable Lint/UnusedMethodArgument
67+
def self.generate(filename, options = {}, &block)
68+
raise unless block_given?
69+
70+
writer = Writer.new(filename, options)
71+
yield writer
72+
ensure
73+
writer.finalize
74+
end
75+
# rubocop:enable Lint/UnusedMethodArgument
76+
end

lib/smarter_csv/smarter_csv.rb

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,6 @@
11
# frozen_string_literal: true
22

33
module SmarterCSV
4-
class SmarterCSVException < StandardError; end
5-
class HeaderSizeMismatch < SmarterCSVException; end
6-
class IncorrectOption < SmarterCSVException; end
7-
class ValidationError < SmarterCSVException; end
8-
class DuplicateHeaders < SmarterCSVException; end
9-
class MissingKeys < SmarterCSVException; end # previously known as MissingHeaders
10-
class NoColSepDetected < SmarterCSVException; end
11-
class KeyMappingError < SmarterCSVException; end
12-
134
# first parameter: filename or input object which responds to readline method
145
def SmarterCSV.process(input, given_options = {}, &block) # rubocop:disable Lint/UnusedMethodArgument
156
initialize_variables

lib/smarter_csv/version.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# frozen_string_literal: true
22

33
module SmarterCSV
4-
VERSION = "1.11.0"
4+
VERSION = "1.11.1"
55
end

lib/smarter_csv/writer.rb

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,22 +35,35 @@ module SmarterCSV
3535
# Make sure to use the correct form when specifying headers manually,
3636
# in combination with the :discover_headers option
3737

38+
attr_reader :options, :row_sep, :col_sep, :quote_char, :force_quotes, :discover_headers, :headers, :map_headers, :output_file
39+
3840
class Writer
3941
def initialize(file_path, options = {})
4042
@options = options
41-
@discover_headers = options.has_key?(:discover_headers) ? (options[:discover_headers] == true) : true
42-
@headers = options[:headers] || []
4343
@row_sep = options[:row_sep] || "\n" # RFC4180 "\r\n"
4444
@col_sep = options[:col_sep] || ','
45-
@quote_char = '"'
45+
@quote_char = options[:quote_char] || '"'
4646
@force_quotes = options[:force_quotes] == true
47+
@discover_headers = true # defaults to true
48+
if options.has_key?(:discover_headers)
49+
# passing in the option overrides the default behavior
50+
@discover_headers = options[:discover_headers] == true
51+
else
52+
# disable discover_headers when headers are given explicitly
53+
@discover_headers = !(options.has_key?(:map_headers) || options.has_key?(:headers))
54+
end
55+
@headers = [] # start with empty headers
56+
@headers = options[:headers] if options.has_key?(:headers) # unless explicitly given
57+
@headers = options[:map_headers].keys if options.has_key?(:map_headers) && !options.has_key?(:headers)
4758
@map_headers = options[:map_headers] || {}
59+
4860
@output_file = File.open(file_path, 'w+')
4961
# hidden state:
5062
@temp_file = Tempfile.new('tempfile', '/tmp')
5163
@quote_regex = Regexp.union(@col_sep, @row_sep, @quote_char)
5264
end
5365

66+
# this can be called many times in order to append lines to the csv file
5467
def <<(data)
5568
case data
5669
when Hash
@@ -60,7 +73,7 @@ def <<(data)
6073
when NilClass
6174
# ignore
6275
else
63-
raise ArgumentError, "Invalid data type: #{data.class}. Must be a Hash or an Array."
76+
raise InvalidInputData, "Invalid data type: #{data.class}. Must be a Hash or an Array."
6477
end
6578
end
6679

spec/smarter_csv/writer_spec.rb

Lines changed: 40 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -90,14 +90,31 @@
9090
expect(output).to include("Alex,,,USA,\n")
9191
end
9292

93-
context "when discover_headers is turned off" do
94-
let(:options) { {discover_headers: false, headers: [:name, :country]} }
93+
context "when headers are given explicitly" do
94+
let(:options) { {headers: [:country, :name]} }
9595

9696
it 'writes the given headers and data correctly' do
9797
create_csv_file
98+
99+
output = File.read(file_path)
100+
101+
expect(output).to include("country,name\n")
102+
expect(output).to include(",John\n")
103+
expect(output).to include("USA,Jane\n")
104+
expect(output).to include(",Mike\n")
105+
expect(output).to include("USA,Alex\n")
106+
end
107+
end
108+
109+
context "when map_headers is given explicitly" do
110+
let(:options) { {map_headers: {name: "Person", country: "Country"}} }
111+
112+
it 'writes the given headers and data correctly' do
113+
create_csv_file
114+
98115
output = File.read(file_path)
99116

100-
expect(output).to include("name,country\n")
117+
expect(output).to include("Person,Country\n")
101118
expect(output).to include("John,\n")
102119
expect(output).to include("Jane,USA\n")
103120
expect(output).to include("Mike,\n")
@@ -106,18 +123,18 @@
106123
end
107124
end
108125

109-
context 'when headers are given in advance' do
126+
context 'when headers are given explicitly' do
110127
let(:options) { { headers: %i[name age city] } }
111128

112129
it 'writes the given headers and data correctly' do
113130
create_csv_file
114131
output = File.read(file_path)
115132

116-
expect(output).to include("name,age,city,country,state\n")
133+
expect(output).to include("name,age,city\n")
117134
expect(output).to include("John,30,New York\n")
118-
expect(output).to include("Jane,25,,USA\n")
119-
expect(output).to include("Mike,35,Chicago,,IL\n")
120-
expect(output).to include("Alex,,,USA,\n")
135+
expect(output).to include("Jane,25,\n")
136+
expect(output).to include("Mike,35,Chicago\n")
137+
expect(output).to include("Alex,,\n")
121138
end
122139
end
123140

@@ -142,20 +159,22 @@
142159
name: 'Full Name',
143160
age: 'Age',
144161
city: 'City',
145-
country: 'Country',
146162
state: 'State',
163+
country: 'Country',
147164
}
148165
}
149166
end
150167

151-
it 'writes the mapped headers and data correctly' do
168+
it 'writes the mapped headers and data in the correct order' do
152169
create_csv_file
170+
153171
output = File.read(file_path)
154172

155-
expect(output).to include("Full Name,Age,City,Country,State\n")
156-
expect(output).to include("John,30,New York\n")
157-
expect(output).to include("Jane,25,,USA\n")
158-
expect(output).to include("Mike,35,Chicago,,IL\n")
173+
expect(output).to include("Full Name,Age,City,State,Country\n")
174+
expect(output).to include("John,30,New York,,\n")
175+
expect(output).to include("Jane,25,,,USA\n")
176+
expect(output).to include("Mike,35,Chicago,IL,\n")
177+
expect(output).to include("Alex,,,,USA\n")
159178
end
160179
end
161180

@@ -194,15 +213,6 @@
194213
expect(output).to include("5,,,4\n")
195214
end
196215

197-
it 'appends with existing headers' do
198-
options = { headers: [:a] }
199-
writer = SmarterCSV::Writer.new(file_path, options)
200-
writer << [{ a: 1, b: 2 }]
201-
writer.finalize
202-
203-
expect(File.read(file_path)).to eq("a,b\n1,2\n")
204-
end
205-
206216
it 'appends with missing fields' do
207217
writer = SmarterCSV::Writer.new(file_path)
208218
writer << [{ a: 1, b: 2 }, { a: 3 }]
@@ -293,6 +303,13 @@
293303
end
294304

295305
context 'Error Handling' do
306+
it 'raises an error for invalid input data' do
307+
expect do
308+
writer = SmarterCSV::Writer.new(file_path)
309+
writer << "this is invalid"
310+
end.to raise_error SmarterCSV::InvalidInputData
311+
end
312+
296313
it 'handles file access issues' do
297314
allow(File).to receive(:open).and_raise(Errno::EACCES)
298315

0 commit comments

Comments
 (0)