From 0ffd1af992fc0da97a874ca1807089a7638e145f Mon Sep 17 00:00:00 2001 From: Tilo Sloboda Date: Fri, 5 Jul 2024 19:50:11 +0800 Subject: [PATCH] Adding SmarterCSV.generate; improve behavior of Writer class --- CHANGELOG.md | 4 +++ lib/smarter_csv.rb | 26 +++++++++++++- lib/smarter_csv/smarter_csv.rb | 9 ----- lib/smarter_csv/version.rb | 2 +- lib/smarter_csv/writer.rb | 21 ++++++++--- spec/smarter_csv/writer_spec.rb | 63 +++++++++++++++++++++------------ 6 files changed, 87 insertions(+), 38 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b28c5c71..b6babdb9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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)) diff --git a/lib/smarter_csv.rb b/lib/smarter_csv.rb index f0ef1b78..7f70923b 100644 --- a/lib/smarter_csv.rb +++ b/lib/smarter_csv.rb @@ -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" @@ -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 @@ -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 diff --git a/lib/smarter_csv/smarter_csv.rb b/lib/smarter_csv/smarter_csv.rb index f7420a62..51e6503c 100644 --- a/lib/smarter_csv/smarter_csv.rb +++ b/lib/smarter_csv/smarter_csv.rb @@ -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 diff --git a/lib/smarter_csv/version.rb b/lib/smarter_csv/version.rb index 255a0b91..a1f5b302 100644 --- a/lib/smarter_csv/version.rb +++ b/lib/smarter_csv/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module SmarterCSV - VERSION = "1.11.0" + VERSION = "1.11.1" end diff --git a/lib/smarter_csv/writer.rb b/lib/smarter_csv/writer.rb index a92a13e6..eef60c37 100644 --- a/lib/smarter_csv/writer.rb +++ b/lib/smarter_csv/writer.rb @@ -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 @@ -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 diff --git a/spec/smarter_csv/writer_spec.rb b/spec/smarter_csv/writer_spec.rb index 41f59b22..5e7dff06 100644 --- a/spec/smarter_csv/writer_spec.rb +++ b/spec/smarter_csv/writer_spec.rb @@ -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") @@ -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 @@ -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 @@ -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 }] @@ -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)