csv-utils 0.5.0 → 0.7.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 +4 -4
- data/CHANGELOG.md +55 -0
- data/README.md +115 -18
- data/bin/csv-change-eol +4 -8
- data/bin/csv-diff +19 -35
- data/bin/csv-duplicate-finder +17 -14
- data/bin/csv-find-error +7 -5
- data/bin/csv-readline +3 -14
- data/bin/csv-splitter +7 -5
- data/bin/csv-validator +10 -6
- data/lib/csv-utils.rb +5 -1
- data/lib/csv_utils/byte_order_mark.rb +34 -0
- data/lib/csv_utils/character_encoding.rb +23 -0
- data/lib/csv_utils/csv_compare.rb +89 -65
- data/lib/csv_utils/csv_extender.rb +26 -4
- data/lib/csv_utils/csv_iterator.rb +111 -44
- data/lib/csv_utils/csv_options.rb +117 -30
- data/lib/csv_utils/csv_report.rb +28 -4
- data/lib/csv_utils/csv_row.rb +30 -0
- data/lib/csv_utils/csv_row_matcher.rb +16 -2
- data/lib/csv_utils/csv_sort.rb +63 -24
- data/lib/csv_utils/csv_transformer.rb +82 -37
- data/lib/csv_utils/csv_wrapper.rb +24 -4
- data/lib/csv_utils/encoding_options.rb +40 -0
- data/lib/csv_utils/version.rb +6 -0
- metadata +29 -23
- data/.github/workflows/ci.yml +0 -53
- data/.gitignore +0 -52
- data/.rubocop.yml +0 -81
- data/.ruby-gemset +0 -1
- data/.ruby-version +0 -1
- data/ARCHITECTURE.md +0 -154
- data/CLAUDE.md +0 -63
- data/Gemfile +0 -14
- data/Gemfile.lock +0 -77
- data/csv-utils.gemspec +0 -19
- data/script/console +0 -7
|
@@ -1,63 +1,107 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# Transforms a CSV given a series of steps
|
|
4
3
|
module CSVUtils
|
|
4
|
+
# Copies a CSV file through a pipeline of steps that filter, change and add to its rows in batches.
|
|
5
|
+
# Each step method returns the transformer so they can be chained, and {#process} runs them.
|
|
6
|
+
#
|
|
7
|
+
# Step blocks receive the row, the headers as they were when the step was added, and the value
|
|
8
|
+
# computed by the last {#additional_data} step for the current batch.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# CSVUtils::CSVTransformer.new('input.csv', 'output.csv')
|
|
12
|
+
# .read_headers
|
|
13
|
+
# .select { |row, _headers, _data| row[0].to_i > 100 }
|
|
14
|
+
# .append(['Email']) { |row, _headers, _data| ["#{row[1].downcase}@example.com"] }
|
|
15
|
+
# .process
|
|
5
16
|
class CSVTransformer
|
|
17
|
+
# @return [Array<String>, nil] the headers the output will have after the steps added so far
|
|
6
18
|
attr_reader :headers
|
|
7
19
|
|
|
20
|
+
# @param src_csv [String, CSV, #shift] path or CSV to read; paths are opened and closed by the transformer
|
|
21
|
+
# @param dest_csv [String, CSV, #<<] path or CSV to write
|
|
22
|
+
# @param csv_options [Hash] options passed to CSV.open for paths; the source is read with its :encoding,
|
|
23
|
+
# 'bom|utf-8' by default, and the output written in the encoding values were decoded to (see {EncodingOptions})
|
|
8
24
|
def initialize(src_csv, dest_csv, csv_options = {})
|
|
9
|
-
@src_csv = CSVUtils::CSVWrapper.new(src_csv, 'rb', csv_options)
|
|
10
|
-
@dest_csv = CSVUtils::CSVWrapper.new(dest_csv, 'wb', csv_options)
|
|
25
|
+
@src_csv = CSVUtils::CSVWrapper.new(src_csv, 'rb', EncodingOptions.read(csv_options))
|
|
26
|
+
@dest_csv = CSVUtils::CSVWrapper.new(dest_csv, 'wb', EncodingOptions.write(csv_options))
|
|
11
27
|
end
|
|
12
28
|
|
|
29
|
+
# Reads the first row as the headers. Without it, every row is data and the output has no header row.
|
|
30
|
+
# @return [self]
|
|
13
31
|
def read_headers
|
|
14
32
|
@headers = @src_csv.shift
|
|
15
33
|
self
|
|
16
34
|
end
|
|
17
35
|
|
|
36
|
+
# Computes a value once per batch, passed to the steps that follow as their third argument.
|
|
37
|
+
# @yieldparam batch [Array<Array<String>>]
|
|
38
|
+
# @yieldparam headers [Array<String>, nil]
|
|
39
|
+
# @return [self]
|
|
18
40
|
def additional_data(&block)
|
|
19
41
|
steps << [:additional_data, @headers, block]
|
|
20
42
|
self
|
|
21
43
|
end
|
|
22
44
|
|
|
45
|
+
# Keeps the rows the block returns true for.
|
|
46
|
+
# @yieldparam row [Array<String>]
|
|
47
|
+
# @yieldparam headers [Array<String>, nil]
|
|
48
|
+
# @yieldparam additional_data [Object]
|
|
49
|
+
# @return [self]
|
|
23
50
|
def select(&block)
|
|
24
51
|
steps << [:select, @headers, block]
|
|
25
52
|
self
|
|
26
53
|
end
|
|
27
54
|
|
|
55
|
+
# Drops the rows the block returns true for.
|
|
56
|
+
# @yieldparam (see #select)
|
|
57
|
+
# @return [self]
|
|
28
58
|
def reject(&block)
|
|
29
59
|
steps << [:reject, @headers, block]
|
|
30
60
|
self
|
|
31
61
|
end
|
|
32
62
|
|
|
63
|
+
# Replaces each row with the block's result.
|
|
64
|
+
# @param new_headers [Array<String>, nil] the headers after this step
|
|
65
|
+
# @yieldparam (see #select)
|
|
66
|
+
# @yieldreturn [Array] the new row
|
|
67
|
+
# @return [self]
|
|
33
68
|
def map(new_headers, &block)
|
|
34
69
|
steps << [:map, @headers, block]
|
|
35
70
|
@headers = new_headers
|
|
36
71
|
self
|
|
37
72
|
end
|
|
38
73
|
|
|
74
|
+
# Adds the block's values to the end of each row.
|
|
75
|
+
# @param additional_headers [Array<String>, nil] headers for the new values; nil drops the header row
|
|
76
|
+
# @yieldparam (see #select)
|
|
77
|
+
# @yieldreturn [Array] values to append
|
|
78
|
+
# @return [self]
|
|
39
79
|
def append(additional_headers, &block)
|
|
40
80
|
steps << [:append, @headers, block]
|
|
41
|
-
|
|
42
|
-
if additional_headers
|
|
43
|
-
@headers += additional_headers
|
|
44
|
-
else
|
|
45
|
-
@headers = nil
|
|
46
|
-
end
|
|
47
|
-
|
|
81
|
+
# without headers (read_headers not called) the output has no header row to extend
|
|
82
|
+
@headers = (@headers + additional_headers if @headers && additional_headers)
|
|
48
83
|
self
|
|
49
84
|
end
|
|
50
85
|
|
|
86
|
+
# Calls the block for each row without changing it.
|
|
87
|
+
# @yieldparam (see #select)
|
|
88
|
+
# @return [self]
|
|
51
89
|
def each(&block)
|
|
52
90
|
steps << [:each, @headers, block]
|
|
53
91
|
self
|
|
54
92
|
end
|
|
55
93
|
|
|
94
|
+
# Replaces the headers written to the output.
|
|
95
|
+
# @param headers [Array<String>, nil]
|
|
96
|
+
# @return [self]
|
|
56
97
|
def set_headers(headers)
|
|
57
98
|
@headers = headers
|
|
58
99
|
self
|
|
59
100
|
end
|
|
60
101
|
|
|
102
|
+
# Runs the steps over every row, writes the output and closes the files it opened, even when a step raises.
|
|
103
|
+
# @param batch_size [Integer] rows read and run through the steps at a time
|
|
104
|
+
# @return [void]
|
|
61
105
|
def process(batch_size = 10_000)
|
|
62
106
|
batch = []
|
|
63
107
|
|
|
@@ -79,7 +123,7 @@ module CSVUtils
|
|
|
79
123
|
end
|
|
80
124
|
|
|
81
125
|
steps_proc.call if batch.size.positive?
|
|
82
|
-
|
|
126
|
+
ensure
|
|
83
127
|
@src_csv.close
|
|
84
128
|
@dest_csv.close
|
|
85
129
|
end
|
|
@@ -90,33 +134,34 @@ module CSVUtils
|
|
|
90
134
|
@steps ||= []
|
|
91
135
|
end
|
|
92
136
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
batch.select! do |row|
|
|
97
|
-
block.call row, current_headers, @additional_data
|
|
98
|
-
end
|
|
99
|
-
when :reject
|
|
100
|
-
batch.reject! do |row|
|
|
101
|
-
block.call row, current_headers, @additional_data
|
|
102
|
-
end
|
|
103
|
-
when :map
|
|
104
|
-
batch.map! do |row|
|
|
105
|
-
block.call row, current_headers, @additional_data
|
|
106
|
-
end
|
|
107
|
-
when :append
|
|
108
|
-
batch.map! do |row|
|
|
109
|
-
row + block.call(row, current_headers, @additional_data)
|
|
110
|
-
end
|
|
111
|
-
when :additional_data
|
|
112
|
-
@additional_data = block.call(batch, current_headers)
|
|
113
|
-
when :each
|
|
114
|
-
batch.each do |row|
|
|
115
|
-
block.call(row, current_headers, @additional_data)
|
|
116
|
-
end
|
|
117
|
-
end
|
|
118
|
-
|
|
137
|
+
# each step type has a process_<type>_step method that changes the batch in place
|
|
138
|
+
def process_step(step_type, current_headers, batch, &)
|
|
139
|
+
send(:"process_#{step_type}_step", current_headers, batch, &)
|
|
119
140
|
batch
|
|
120
141
|
end
|
|
142
|
+
|
|
143
|
+
def process_select_step(current_headers, batch)
|
|
144
|
+
batch.select! { |row| yield row, current_headers, @additional_data }
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def process_reject_step(current_headers, batch)
|
|
148
|
+
batch.reject! { |row| yield row, current_headers, @additional_data }
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def process_map_step(current_headers, batch)
|
|
152
|
+
batch.map! { |row| yield row, current_headers, @additional_data }
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def process_append_step(current_headers, batch)
|
|
156
|
+
batch.map! { |row| row + yield(row, current_headers, @additional_data) }
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def process_additional_data_step(current_headers, batch)
|
|
160
|
+
@additional_data = yield batch, current_headers
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def process_each_step(current_headers, batch)
|
|
164
|
+
batch.each { |row| yield row, current_headers, @additional_data }
|
|
165
|
+
end
|
|
121
166
|
end
|
|
122
167
|
end
|
|
@@ -1,25 +1,38 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# Wraps a CSV object, if wrapper opens the csv file it will close it
|
|
4
3
|
module CSVUtils
|
|
4
|
+
# Wraps a path or an open CSV so callers can read, write and close it the same way.
|
|
5
|
+
# It closes only the files it opened itself.
|
|
5
6
|
class CSVWrapper
|
|
7
|
+
# @return [CSV, #shift, #<<] the CSV being wrapped
|
|
6
8
|
attr_reader :csv
|
|
7
9
|
|
|
10
|
+
# @param csv [String, CSV, #shift, #<<] path to open, or a CSV (or array of rows) to use as is
|
|
11
|
+
# @param mode [String] file mode for a path
|
|
12
|
+
# @param csv_options [Hash] options passed to CSV.open for a path
|
|
8
13
|
def initialize(csv, mode, csv_options)
|
|
9
14
|
open(csv, mode, csv_options)
|
|
10
15
|
end
|
|
11
16
|
|
|
17
|
+
# Wraps a path or CSV; with a block, yields the wrapper and closes it afterwards, even when the block raises.
|
|
18
|
+
# @param file [String, CSV] path or CSV
|
|
19
|
+
# @param mode [String] file mode for a path
|
|
20
|
+
# @param csv_options [Hash] options passed to CSV.open for a path
|
|
21
|
+
# @yieldparam csv [CSVWrapper]
|
|
22
|
+
# @return [CSVWrapper, Object] the wrapper, or the block's value
|
|
12
23
|
def self.open(file, mode, csv_options = {})
|
|
13
24
|
csv = new(file, mode, csv_options)
|
|
25
|
+
return csv unless block_given?
|
|
14
26
|
|
|
15
|
-
|
|
27
|
+
begin
|
|
16
28
|
yield csv
|
|
29
|
+
ensure
|
|
17
30
|
csv.close
|
|
18
|
-
else
|
|
19
|
-
csv
|
|
20
31
|
end
|
|
21
32
|
end
|
|
22
33
|
|
|
34
|
+
# @api private
|
|
35
|
+
# @return [CSV, #shift, #<<]
|
|
23
36
|
def open(csv, mode, csv_options)
|
|
24
37
|
if csv.is_a?(String)
|
|
25
38
|
@close_when_done = true
|
|
@@ -30,18 +43,25 @@ module CSVUtils
|
|
|
30
43
|
end
|
|
31
44
|
end
|
|
32
45
|
|
|
46
|
+
# @param row [Array]
|
|
47
|
+
# @return [CSV, #<<]
|
|
33
48
|
def <<(row)
|
|
34
49
|
csv << row
|
|
35
50
|
end
|
|
36
51
|
|
|
52
|
+
# @return [Array<String>, nil] the next row, nil at the end
|
|
37
53
|
def shift
|
|
38
54
|
csv.shift
|
|
39
55
|
end
|
|
40
56
|
|
|
57
|
+
# Moves back to the first row.
|
|
58
|
+
# @return [void]
|
|
41
59
|
def rewind
|
|
42
60
|
csv.rewind
|
|
43
61
|
end
|
|
44
62
|
|
|
63
|
+
# Closes the CSV if the wrapper opened it.
|
|
64
|
+
# @return [void]
|
|
45
65
|
def close
|
|
46
66
|
csv.close if close_when_done?
|
|
47
67
|
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CSVUtils
|
|
4
|
+
# The one place the library decides how files are decoded and encoded. Every class reads with the
|
|
5
|
+
# :encoding CSV option, 'bom|utf-8' by default, so values are UTF-8 strings whatever the locale, and
|
|
6
|
+
# writes in the encoding the values were decoded to, so reading 'Windows-1252:UTF-8' writes UTF-8.
|
|
7
|
+
# File modes never carry an encoding: CSV raises when the mode and the :encoding option both have one.
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# CSV.open(path, 'rb', **CSVUtils::EncodingOptions.read(col_sep: "\t"))
|
|
11
|
+
module EncodingOptions
|
|
12
|
+
# @return [String] the :encoding option used for reading when none is given
|
|
13
|
+
DEFAULT_ENCODING = 'bom|utf-8'
|
|
14
|
+
|
|
15
|
+
# Options for reading: the given options with the default :encoding when they have none.
|
|
16
|
+
# @param csv_options [Hash]
|
|
17
|
+
# @return [Hash]
|
|
18
|
+
def self.read(csv_options)
|
|
19
|
+
{ encoding: DEFAULT_ENCODING }.merge(csv_options)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Options for writing values read with csv_options: the :encoding is the one values were decoded to.
|
|
23
|
+
# @param csv_options [Hash]
|
|
24
|
+
# @return [Hash]
|
|
25
|
+
def self.write(csv_options)
|
|
26
|
+
csv_options.merge(encoding: decoded_encoding(read(csv_options)[:encoding]))
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# The encoding of strings read with an :encoding option: the internal encoding of 'external:internal',
|
|
30
|
+
# otherwise the external one, without a 'bom|' prefix.
|
|
31
|
+
# @example
|
|
32
|
+
# CSVUtils::EncodingOptions.decoded_encoding('Windows-1252:UTF-8') # => 'UTF-8'
|
|
33
|
+
# CSVUtils::EncodingOptions.decoded_encoding('bom|utf-8') # => 'utf-8'
|
|
34
|
+
# @param encoding [String, Encoding]
|
|
35
|
+
# @return [String]
|
|
36
|
+
def self.decoded_encoding(encoding)
|
|
37
|
+
encoding.to_s.split(':').last.sub(/\Abom\|/i, '')
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: csv-utils
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Doug Youch
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: csv
|
|
@@ -15,31 +15,39 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - ">="
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: '0'
|
|
18
|
+
version: '3.0'
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - ">="
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: '0'
|
|
25
|
+
version: '3.0'
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: inheritance-helper
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
29
29
|
requirements:
|
|
30
30
|
- - ">="
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
|
-
version: '0'
|
|
32
|
+
version: '0.2'
|
|
33
|
+
- - "<"
|
|
34
|
+
- !ruby/object:Gem::Version
|
|
35
|
+
version: '2'
|
|
33
36
|
type: :runtime
|
|
34
37
|
prerelease: false
|
|
35
38
|
version_requirements: !ruby/object:Gem::Requirement
|
|
36
39
|
requirements:
|
|
37
40
|
- - ">="
|
|
38
41
|
- !ruby/object:Gem::Version
|
|
39
|
-
version: '0'
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
version: '0.2'
|
|
43
|
+
- - "<"
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: '2'
|
|
46
|
+
description: Streaming tools for CSV files too big or too broken to load into memory.
|
|
47
|
+
Compare two sorted files into create, update and delete actions, sort with an external
|
|
48
|
+
merge sort, transform and extend rows in batches, and build reports from Ruby objects.
|
|
49
|
+
Detects separators, byte order marks and encodings, and ships command line tools
|
|
50
|
+
that pinpoint malformed rows, diff, grep, split and validate CSV files.
|
|
43
51
|
email: dougyouch@gmail.com
|
|
44
52
|
executables:
|
|
45
53
|
- csv-change-eol
|
|
@@ -54,15 +62,7 @@ executables:
|
|
|
54
62
|
extensions: []
|
|
55
63
|
extra_rdoc_files: []
|
|
56
64
|
files:
|
|
57
|
-
-
|
|
58
|
-
- ".gitignore"
|
|
59
|
-
- ".rubocop.yml"
|
|
60
|
-
- ".ruby-gemset"
|
|
61
|
-
- ".ruby-version"
|
|
62
|
-
- ARCHITECTURE.md
|
|
63
|
-
- CLAUDE.md
|
|
64
|
-
- Gemfile
|
|
65
|
-
- Gemfile.lock
|
|
65
|
+
- CHANGELOG.md
|
|
66
66
|
- LICENSE
|
|
67
67
|
- README.md
|
|
68
68
|
- bin/csv-change-eol
|
|
@@ -74,8 +74,9 @@ files:
|
|
|
74
74
|
- bin/csv-readline
|
|
75
75
|
- bin/csv-splitter
|
|
76
76
|
- bin/csv-validator
|
|
77
|
-
- csv-utils.gemspec
|
|
78
77
|
- lib/csv-utils.rb
|
|
78
|
+
- lib/csv_utils/byte_order_mark.rb
|
|
79
|
+
- lib/csv_utils/character_encoding.rb
|
|
79
80
|
- lib/csv_utils/csv_compare.rb
|
|
80
81
|
- lib/csv_utils/csv_extender.rb
|
|
81
82
|
- lib/csv_utils/csv_iterator.rb
|
|
@@ -86,12 +87,17 @@ files:
|
|
|
86
87
|
- lib/csv_utils/csv_sort.rb
|
|
87
88
|
- lib/csv_utils/csv_transformer.rb
|
|
88
89
|
- lib/csv_utils/csv_wrapper.rb
|
|
89
|
-
-
|
|
90
|
+
- lib/csv_utils/encoding_options.rb
|
|
91
|
+
- lib/csv_utils/version.rb
|
|
90
92
|
homepage: https://github.com/dougyouch/csv-utils
|
|
91
93
|
licenses:
|
|
92
94
|
- MIT
|
|
93
95
|
metadata:
|
|
94
96
|
rubygems_mfa_required: 'true'
|
|
97
|
+
source_code_uri: https://github.com/dougyouch/csv-utils
|
|
98
|
+
changelog_uri: https://github.com/dougyouch/csv-utils/blob/master/CHANGELOG.md
|
|
99
|
+
bug_tracker_uri: https://github.com/dougyouch/csv-utils/issues
|
|
100
|
+
documentation_uri: https://rubydoc.info/gems/csv-utils
|
|
95
101
|
rdoc_options: []
|
|
96
102
|
require_paths:
|
|
97
103
|
- lib
|
|
@@ -99,14 +105,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
99
105
|
requirements:
|
|
100
106
|
- - ">="
|
|
101
107
|
- !ruby/object:Gem::Version
|
|
102
|
-
version: '
|
|
108
|
+
version: '3.3'
|
|
103
109
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
104
110
|
requirements:
|
|
105
111
|
- - ">="
|
|
106
112
|
- !ruby/object:Gem::Version
|
|
107
113
|
version: '0'
|
|
108
114
|
requirements: []
|
|
109
|
-
rubygems_version:
|
|
115
|
+
rubygems_version: 4.0.20
|
|
110
116
|
specification_version: 4
|
|
111
|
-
summary:
|
|
117
|
+
summary: Compare, sort, transform and debug large or malformed CSV files
|
|
112
118
|
test_files: []
|
data/.github/workflows/ci.yml
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
name: CI
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
branches: [master]
|
|
6
|
-
pull_request:
|
|
7
|
-
branches: [master]
|
|
8
|
-
|
|
9
|
-
jobs:
|
|
10
|
-
lint:
|
|
11
|
-
name: RuboCop
|
|
12
|
-
runs-on: ubuntu-latest
|
|
13
|
-
steps:
|
|
14
|
-
- uses: actions/checkout@v4
|
|
15
|
-
|
|
16
|
-
- name: Set up Ruby
|
|
17
|
-
uses: ruby/setup-ruby@v1
|
|
18
|
-
with:
|
|
19
|
-
ruby-version: '3.4'
|
|
20
|
-
bundler-cache: true
|
|
21
|
-
|
|
22
|
-
- name: Run RuboCop
|
|
23
|
-
run: bundle exec rubocop --format github
|
|
24
|
-
|
|
25
|
-
test:
|
|
26
|
-
name: Tests (Ruby ${{ matrix.ruby }})
|
|
27
|
-
runs-on: ubuntu-latest
|
|
28
|
-
strategy:
|
|
29
|
-
fail-fast: false
|
|
30
|
-
matrix:
|
|
31
|
-
ruby: ['3.2', '3.3', '3.4']
|
|
32
|
-
|
|
33
|
-
steps:
|
|
34
|
-
- uses: actions/checkout@v4
|
|
35
|
-
|
|
36
|
-
- name: Set up Ruby ${{ matrix.ruby }}
|
|
37
|
-
uses: ruby/setup-ruby@v1
|
|
38
|
-
with:
|
|
39
|
-
ruby-version: ${{ matrix.ruby }}
|
|
40
|
-
bundler-cache: true
|
|
41
|
-
|
|
42
|
-
- name: Run tests
|
|
43
|
-
run: bundle exec rspec
|
|
44
|
-
|
|
45
|
-
- name: Upload coverage to Codecov
|
|
46
|
-
if: matrix.ruby == '3.4'
|
|
47
|
-
uses: codecov/codecov-action@v4
|
|
48
|
-
with:
|
|
49
|
-
files: coverage/coverage.xml
|
|
50
|
-
fail_ci_if_error: false
|
|
51
|
-
verbose: true
|
|
52
|
-
env:
|
|
53
|
-
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
data/.gitignore
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
*.gem
|
|
2
|
-
*.rbc
|
|
3
|
-
/.config
|
|
4
|
-
/coverage/
|
|
5
|
-
/InstalledFiles
|
|
6
|
-
/pkg/
|
|
7
|
-
/spec/reports/
|
|
8
|
-
/spec/examples.txt
|
|
9
|
-
/test/tmp/
|
|
10
|
-
/test/version_tmp/
|
|
11
|
-
/tmp/
|
|
12
|
-
|
|
13
|
-
# Used by dotenv library to load environment variables.
|
|
14
|
-
# .env
|
|
15
|
-
|
|
16
|
-
## Specific to RubyMotion:
|
|
17
|
-
.dat*
|
|
18
|
-
.repl_history
|
|
19
|
-
build/
|
|
20
|
-
*.bridgesupport
|
|
21
|
-
build-iPhoneOS/
|
|
22
|
-
build-iPhoneSimulator/
|
|
23
|
-
|
|
24
|
-
## Specific to RubyMotion (use of CocoaPods):
|
|
25
|
-
#
|
|
26
|
-
# We recommend against adding the Pods directory to your .gitignore. However
|
|
27
|
-
# you should judge for yourself, the pros and cons are mentioned at:
|
|
28
|
-
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
|
|
29
|
-
#
|
|
30
|
-
# vendor/Pods/
|
|
31
|
-
|
|
32
|
-
## Documentation cache and generated files:
|
|
33
|
-
/.yardoc/
|
|
34
|
-
/_yardoc/
|
|
35
|
-
/doc/
|
|
36
|
-
/rdoc/
|
|
37
|
-
|
|
38
|
-
## Environment normalization:
|
|
39
|
-
/.bundle/
|
|
40
|
-
/vendor/bundle
|
|
41
|
-
/lib/bundler/man/
|
|
42
|
-
|
|
43
|
-
# for a library or gem, you might want to ignore these files since the code is
|
|
44
|
-
# intended to run in multiple environments; otherwise, check them in:
|
|
45
|
-
# Gemfile.lock
|
|
46
|
-
# .ruby-version
|
|
47
|
-
# .ruby-gemset
|
|
48
|
-
|
|
49
|
-
# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
|
|
50
|
-
.rvmrc
|
|
51
|
-
|
|
52
|
-
*~
|
data/.rubocop.yml
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
AllCops:
|
|
2
|
-
TargetRubyVersion: 3.2
|
|
3
|
-
NewCops: enable
|
|
4
|
-
SuggestExtensions: false
|
|
5
|
-
Exclude:
|
|
6
|
-
- 'bin/**/*'
|
|
7
|
-
- 'vendor/**/*'
|
|
8
|
-
- 'coverage/**/*'
|
|
9
|
-
|
|
10
|
-
# Relaxed metrics for existing codebase
|
|
11
|
-
Metrics/MethodLength:
|
|
12
|
-
Max: 35
|
|
13
|
-
|
|
14
|
-
Metrics/AbcSize:
|
|
15
|
-
Max: 40
|
|
16
|
-
|
|
17
|
-
Metrics/ClassLength:
|
|
18
|
-
Max: 200
|
|
19
|
-
|
|
20
|
-
Metrics/CyclomaticComplexity:
|
|
21
|
-
Max: 15
|
|
22
|
-
|
|
23
|
-
Metrics/PerceivedComplexity:
|
|
24
|
-
Max: 15
|
|
25
|
-
|
|
26
|
-
Metrics/BlockLength:
|
|
27
|
-
Exclude:
|
|
28
|
-
- 'spec/**/*'
|
|
29
|
-
- '*.gemspec'
|
|
30
|
-
|
|
31
|
-
# Style preferences
|
|
32
|
-
Style/Documentation:
|
|
33
|
-
Enabled: false
|
|
34
|
-
|
|
35
|
-
Style/FrozenStringLiteralComment:
|
|
36
|
-
EnforcedStyle: always
|
|
37
|
-
|
|
38
|
-
Layout/LineLength:
|
|
39
|
-
Max: 130
|
|
40
|
-
Exclude:
|
|
41
|
-
- 'spec/**/*'
|
|
42
|
-
|
|
43
|
-
# File naming - allow hyphenated gem name
|
|
44
|
-
Naming/FileName:
|
|
45
|
-
Exclude:
|
|
46
|
-
- 'lib/csv-utils.rb'
|
|
47
|
-
|
|
48
|
-
# Allow set_ prefix for transformer DSL methods
|
|
49
|
-
Naming/AccessorMethodName:
|
|
50
|
-
Exclude:
|
|
51
|
-
- 'lib/csv_utils/csv_transformer.rb'
|
|
52
|
-
|
|
53
|
-
# Allow Kernel#open for pipe support (intentional design)
|
|
54
|
-
Security/Open:
|
|
55
|
-
Exclude:
|
|
56
|
-
- 'lib/csv_utils/csv_wrapper.rb'
|
|
57
|
-
|
|
58
|
-
# Duplicate branches are intentional in comparison logic
|
|
59
|
-
Lint/DuplicateBranch:
|
|
60
|
-
Exclude:
|
|
61
|
-
- 'lib/csv_utils/csv_compare.rb'
|
|
62
|
-
- 'lib/csv_utils/csv_sort.rb'
|
|
63
|
-
|
|
64
|
-
# Allow empty blocks in specs (used for testing iteration behavior)
|
|
65
|
-
Lint/EmptyBlock:
|
|
66
|
-
Exclude:
|
|
67
|
-
- 'spec/**/*'
|
|
68
|
-
|
|
69
|
-
# Style relaxations for existing code patterns
|
|
70
|
-
Style/OptionalBooleanParameter:
|
|
71
|
-
Enabled: false
|
|
72
|
-
|
|
73
|
-
Style/StringConcatenation:
|
|
74
|
-
Enabled: false
|
|
75
|
-
|
|
76
|
-
Style/FormatStringToken:
|
|
77
|
-
Enabled: false
|
|
78
|
-
|
|
79
|
-
# Gemspec settings
|
|
80
|
-
Gemspec/RequiredRubyVersion:
|
|
81
|
-
Enabled: false
|
data/.ruby-gemset
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
csv-utils
|
data/.ruby-version
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
3.4.2
|