Skip to content

Adding SmarterCSV.generate; improve behavior of Writer class #280

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@

# SmarterCSV 1.x Change Log

## 1.11.1 (2024-07-05)
* improved behavior of Writer class
* added SmarterCSV.generate shortcut for CSV writing

## 1.11.0 (2024-07-02)
* added SmarterCSV::Writer to output CSV files ([issue #44](https://github.com/tilo/smarter_csv/issues/44))

Expand Down
26 changes: 25 additions & 1 deletion lib/smarter_csv.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# frozen_string_literal: true

require "smarter_csv/version"
require "smarter_csv/errors"

require "smarter_csv/file_io"
require "smarter_csv/options_processing"
require "smarter_csv/auto_detection"
Expand All @@ -9,8 +11,10 @@
require 'smarter_csv/header_validations'
require "smarter_csv/headers"
require "smarter_csv/hash_transformations"

require "smarter_csv/parse"
require "smarter_csv/writer"
require "smarter_csv/smarter_csv"

# load the C-extension:
case RUBY_ENGINE
Expand Down Expand Up @@ -49,4 +53,24 @@
BLOCK_COMMENT
end
# :nocov:
require "smarter_csv/smarter_csv"

module SmarterCSV
# SmarterCSV.generate(filename, options) do |csv_writer|
# MyModel.find_in_batches(batch_size: 100) do |batch|
# batch.pluck(:name, :description, :instructor).each do |record|
# csv_writer << record
# end
# end
# end
#
# rubocop:disable Lint/UnusedMethodArgument
def self.generate(filename, options = {}, &block)
raise unless block_given?

writer = Writer.new(filename, options)
yield writer
ensure
writer.finalize
end
# rubocop:enable Lint/UnusedMethodArgument
end
9 changes: 0 additions & 9 deletions lib/smarter_csv/smarter_csv.rb
Original file line number Diff line number Diff line change
@@ -1,15 +1,6 @@
# frozen_string_literal: true

module SmarterCSV
class SmarterCSVException < StandardError; end
class HeaderSizeMismatch < SmarterCSVException; end
class IncorrectOption < SmarterCSVException; end
class ValidationError < SmarterCSVException; end
class DuplicateHeaders < SmarterCSVException; end
class MissingKeys < SmarterCSVException; end # previously known as MissingHeaders
class NoColSepDetected < SmarterCSVException; end
class KeyMappingError < SmarterCSVException; end

# first parameter: filename or input object which responds to readline method
def SmarterCSV.process(input, given_options = {}, &block) # rubocop:disable Lint/UnusedMethodArgument
initialize_variables
Expand Down
2 changes: 1 addition & 1 deletion lib/smarter_csv/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true

module SmarterCSV
VERSION = "1.11.0"
VERSION = "1.11.1"
end
21 changes: 17 additions & 4 deletions lib/smarter_csv/writer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,22 +35,35 @@ module SmarterCSV
# Make sure to use the correct form when specifying headers manually,
# in combination with the :discover_headers option

attr_reader :options, :row_sep, :col_sep, :quote_char, :force_quotes, :discover_headers, :headers, :map_headers, :output_file

class Writer
def initialize(file_path, options = {})
@options = options
@discover_headers = options.has_key?(:discover_headers) ? (options[:discover_headers] == true) : true
@headers = options[:headers] || []
@row_sep = options[:row_sep] || "\n" # RFC4180 "\r\n"
@col_sep = options[:col_sep] || ','
@quote_char = '"'
@quote_char = options[:quote_char] || '"'
@force_quotes = options[:force_quotes] == true
@discover_headers = true # defaults to true
if options.has_key?(:discover_headers)
# passing in the option overrides the default behavior
@discover_headers = options[:discover_headers] == true
else
# disable discover_headers when headers are given explicitly
@discover_headers = !(options.has_key?(:map_headers) || options.has_key?(:headers))
end
@headers = [] # start with empty headers
@headers = options[:headers] if options.has_key?(:headers) # unless explicitly given
@headers = options[:map_headers].keys if options.has_key?(:map_headers) && !options.has_key?(:headers)
@map_headers = options[:map_headers] || {}

@output_file = File.open(file_path, 'w+')
# hidden state:
@temp_file = Tempfile.new('tempfile', '/tmp')
@quote_regex = Regexp.union(@col_sep, @row_sep, @quote_char)
end

# this can be called many times in order to append lines to the csv file
def <<(data)
case data
when Hash
Expand All @@ -60,7 +73,7 @@ def <<(data)
when NilClass
# ignore
else
raise ArgumentError, "Invalid data type: #{data.class}. Must be a Hash or an Array."
raise InvalidInputData, "Invalid data type: #{data.class}. Must be a Hash or an Array."
end
end

Expand Down
63 changes: 40 additions & 23 deletions spec/smarter_csv/writer_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -90,14 +90,31 @@
expect(output).to include("Alex,,,USA,\n")
end

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

it 'writes the given headers and data correctly' do
create_csv_file

output = File.read(file_path)

expect(output).to include("country,name\n")
expect(output).to include(",John\n")
expect(output).to include("USA,Jane\n")
expect(output).to include(",Mike\n")
expect(output).to include("USA,Alex\n")
end
end

context "when map_headers is given explicitly" do
let(:options) { {map_headers: {name: "Person", country: "Country"}} }

it 'writes the given headers and data correctly' do
create_csv_file

output = File.read(file_path)

expect(output).to include("name,country\n")
expect(output).to include("Person,Country\n")
expect(output).to include("John,\n")
expect(output).to include("Jane,USA\n")
expect(output).to include("Mike,\n")
Expand All @@ -106,18 +123,18 @@
end
end

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

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

expect(output).to include("name,age,city,country,state\n")
expect(output).to include("name,age,city\n")
expect(output).to include("John,30,New York\n")
expect(output).to include("Jane,25,,USA\n")
expect(output).to include("Mike,35,Chicago,,IL\n")
expect(output).to include("Alex,,,USA,\n")
expect(output).to include("Jane,25,\n")
expect(output).to include("Mike,35,Chicago\n")
expect(output).to include("Alex,,\n")
end
end

Expand All @@ -142,20 +159,22 @@
name: 'Full Name',
age: 'Age',
city: 'City',
country: 'Country',
state: 'State',
country: 'Country',
}
}
end

it 'writes the mapped headers and data correctly' do
it 'writes the mapped headers and data in the correct order' do
create_csv_file

output = File.read(file_path)

expect(output).to include("Full Name,Age,City,Country,State\n")
expect(output).to include("John,30,New York\n")
expect(output).to include("Jane,25,,USA\n")
expect(output).to include("Mike,35,Chicago,,IL\n")
expect(output).to include("Full Name,Age,City,State,Country\n")
expect(output).to include("John,30,New York,,\n")
expect(output).to include("Jane,25,,,USA\n")
expect(output).to include("Mike,35,Chicago,IL,\n")
expect(output).to include("Alex,,,,USA\n")
end
end

Expand Down Expand Up @@ -194,15 +213,6 @@
expect(output).to include("5,,,4\n")
end

it 'appends with existing headers' do
options = { headers: [:a] }
writer = SmarterCSV::Writer.new(file_path, options)
writer << [{ a: 1, b: 2 }]
writer.finalize

expect(File.read(file_path)).to eq("a,b\n1,2\n")
end

it 'appends with missing fields' do
writer = SmarterCSV::Writer.new(file_path)
writer << [{ a: 1, b: 2 }, { a: 3 }]
Expand Down Expand Up @@ -293,6 +303,13 @@
end

context 'Error Handling' do
it 'raises an error for invalid input data' do
expect do
writer = SmarterCSV::Writer.new(file_path)
writer << "this is invalid"
end.to raise_error SmarterCSV::InvalidInputData
end

it 'handles file access issues' do
allow(File).to receive(:open).and_raise(Errno::EACCES)

Expand Down
Loading