honey_format 0.8.2 → 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +11 -0
- data/README.md +44 -11
- data/lib/honey_format.rb +1 -1
- data/lib/honey_format/convert_header_value.rb +4 -1
- data/lib/honey_format/csv.rb +12 -8
- data/lib/honey_format/errors.rb +24 -0
- data/lib/honey_format/header.rb +55 -22
- data/lib/honey_format/row.rb +3 -3
- data/lib/honey_format/row_builder.rb +5 -2
- data/lib/honey_format/rows.rb +5 -2
- data/lib/honey_format/version.rb +1 -1
- metadata +3 -3
- data/lib/honey_format/exceptions.rb +0 -14
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: dd242cb77ccf8589eb2df059d887e13596f9c33a14fa587a886bcd376a6d728e
|
4
|
+
data.tar.gz: 72842684af98946cd50f7756818a4e9ce94b6c35d6c23a1e14661f70a93d3bac
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ed5f6fa7f275d7445ca2962aa1787f8c10e25125253c3c65e3a5732cb0ef40e91f7c7d6bf73fa2d49d8d684c43bfec48058369f38df2e9ed5a52522f36f59d8c
|
7
|
+
data.tar.gz: 987843e3c4a987112bfbd46d00fa61a885ef12b71f19005e578e49d3f6f1bbe3af9022e3666af90437e015d522dbc7557a9f3065c45e4d0dd797d48670a9667f
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,14 @@
|
|
1
|
+
# v0.9.2
|
2
|
+
|
3
|
+
:warning: This release contains some backwards compatible changes.
|
4
|
+
|
5
|
+
* Add support for missing header values [[#PR10](https://github.com/buren/honey_format/pull/10)]
|
6
|
+
* Don't mutate original CSV header [[#PR10](https://github.com/buren/honey_format/pull/10)]
|
7
|
+
* Output converted columns, instead of original, when `#to_csv` is called [[#PR10](https://github.com/buren/honey_format/pull/10)]
|
8
|
+
* Update error class names [[#PR10](https://github.com/buren/honey_format/pull/10)]
|
9
|
+
- There are now two super classes for errors `HeaderError` and `RowError`
|
10
|
+
- All errors are under an `Errors` namespace, which `HoneyFormat` includes
|
11
|
+
|
1
12
|
# v0.8.2
|
2
13
|
|
3
14
|
* _[Bugfix]_ `#to_csv` now outputs nil values as empty string instead of `""`
|
data/README.md
CHANGED
@@ -5,7 +5,7 @@ Convert CSV to an array of objects with with ease.
|
|
5
5
|
Perfect for small files of test data or small import scripts.
|
6
6
|
|
7
7
|
```ruby
|
8
|
-
csv_string = "Id,
|
8
|
+
csv_string = "Id,Username\n1,buren"
|
9
9
|
csv = HoneyFormat::CSV.new(csv_string)
|
10
10
|
csv.header # => ["Id", "Username"]
|
11
11
|
user = csv.rows # => [#<struct id="1", username="buren">]
|
@@ -38,10 +38,10 @@ $ gem install honey_format
|
|
38
38
|
By default assumes a header in the CSV file.
|
39
39
|
|
40
40
|
```ruby
|
41
|
-
csv_string = "Id,
|
41
|
+
csv_string = "Id,Username\n1,buren"
|
42
42
|
csv = HoneyFormat::CSV.new(csv_string)
|
43
43
|
csv.header # => ["Id", "Username"]
|
44
|
-
csv.
|
44
|
+
csv.columns # => [:id, :username]
|
45
45
|
|
46
46
|
rows = csv.rows # => [#<struct id="1", username="buren">]
|
47
47
|
user = rows.first
|
@@ -51,8 +51,8 @@ user.username # => "buren"
|
|
51
51
|
|
52
52
|
Minimal custom row builder
|
53
53
|
```ruby
|
54
|
-
csv_string = "Id,
|
55
|
-
upcaser = ->(row) { row.username.upcase
|
54
|
+
csv_string = "Id,Username\n1,buren"
|
55
|
+
upcaser = ->(row) { row.tap { |r| r.username.upcase! } }
|
56
56
|
csv = HoneyFormat::CSV.new(csv_string, row_builder: upcaser)
|
57
57
|
csv.rows # => [#<struct id="1", username="BUREN">]
|
58
58
|
```
|
@@ -71,17 +71,24 @@ class Anonymizer
|
|
71
71
|
end
|
72
72
|
end
|
73
73
|
|
74
|
-
csv_string = "Id,
|
74
|
+
csv_string = "Id,Username\n1,buren"
|
75
75
|
csv = HoneyFormat::CSV.new(csv_string, row_builder: Anonymizer)
|
76
76
|
csv.rows # => [#<struct id="1", username="BUREN">]
|
77
77
|
```
|
78
78
|
|
79
79
|
Output CSV
|
80
80
|
```ruby
|
81
|
-
csv_string = "Id,
|
81
|
+
csv_string = "Id,Username\n1,buren"
|
82
82
|
csv = HoneyFormat::CSV.new(csv_string)
|
83
83
|
csv.rows.each { |row| row.id = nil }
|
84
|
-
csv.to_csv # => "
|
84
|
+
csv.to_csv # => "id,username\n,buren\n"
|
85
|
+
```
|
86
|
+
|
87
|
+
Output a subset of columns to CSV
|
88
|
+
```ruby
|
89
|
+
csv_string = "Id, Username, Country\n1,buren,Sweden"
|
90
|
+
csv = HoneyFormat::CSV.new(csv_string)
|
91
|
+
csv.to_csv(columns: [:id, :country]) # => "id,country\nburen,Sweden\n"
|
85
92
|
```
|
86
93
|
|
87
94
|
You can of course set the delimiter
|
@@ -91,10 +98,10 @@ HoneyFormat::CSV.new(csv_string, delimiter: ';')
|
|
91
98
|
|
92
99
|
Validate CSV header
|
93
100
|
```ruby
|
94
|
-
csv_string = "Id,
|
101
|
+
csv_string = "Id,Username\n1,buren"
|
95
102
|
# Invalid
|
96
103
|
HoneyFormat::CSV.new(csv_string, valid_columns: [:something, :username])
|
97
|
-
# =>
|
104
|
+
# => HoneyFormat::UnknownHeaderColumnError (column :id not in [:something, :username])
|
98
105
|
|
99
106
|
# Valid
|
100
107
|
csv = HoneyFormat::CSV.new(csv_string, valid_columns: [:id, :username])
|
@@ -103,7 +110,7 @@ csv.rows.first.username # => "buren"
|
|
103
110
|
|
104
111
|
Define header
|
105
112
|
```ruby
|
106
|
-
csv_string = "1,
|
113
|
+
csv_string = "1,buren"
|
107
114
|
csv = HoneyFormat::CSV.new(csv_string, header: ['Id', 'Username'])
|
108
115
|
csv.rows.first.username # => "buren"
|
109
116
|
```
|
@@ -122,6 +129,8 @@ user.åäö
|
|
122
129
|
csv_string = "First^Name\nJacob"
|
123
130
|
user = HoneyFormat::CSV.new(csv_string).rows.first
|
124
131
|
user.public_send(:"first^name") # => "Jacob"
|
132
|
+
# or
|
133
|
+
user['first^name'] # => "Jacob"
|
125
134
|
```
|
126
135
|
|
127
136
|
Pass your own header converter
|
@@ -134,6 +143,30 @@ user = HoneyFormat::CSV.new(csv_string, header_converter: converter).rows.first
|
|
134
143
|
user.first_name # => "Jacob"
|
135
144
|
```
|
136
145
|
|
146
|
+
Missing header values
|
147
|
+
```ruby
|
148
|
+
csv_string = "first,,third\nval0,val1,val2"
|
149
|
+
csv = HoneyFormat::CSV.new(csv_string)
|
150
|
+
user = csv.rows.first
|
151
|
+
user.column1 # => "val1"
|
152
|
+
```
|
153
|
+
|
154
|
+
Errors
|
155
|
+
```ruby
|
156
|
+
# there are two error super classes
|
157
|
+
begin
|
158
|
+
HoneyFormat::CSV.new(csv_string)
|
159
|
+
rescue HoneyFormat::HeaderError => e
|
160
|
+
puts 'there is a problem with the header'
|
161
|
+
raise(e)
|
162
|
+
rescue HoneyFormat::RowError => e
|
163
|
+
puts 'there is a problem with a row'
|
164
|
+
raise(e)
|
165
|
+
end
|
166
|
+
```
|
167
|
+
|
168
|
+
You can see all [available errors here](https://www.rubydoc.info/gems/honey_format/HoneyFormat/Errors).
|
169
|
+
|
137
170
|
If you want to see more usage examples check out the `spec/` directory.
|
138
171
|
|
139
172
|
## Benchmark
|
data/lib/honey_format.rb
CHANGED
@@ -20,7 +20,10 @@ module HoneyFormat
|
|
20
20
|
# ConvertHeaderValue.call(" User name ") #=> "user_name"
|
21
21
|
# @example Convert complex header
|
22
22
|
# ConvertHeaderValue.call(" First name (user)") #=> :'first_name(user)'
|
23
|
-
def self.call(column)
|
23
|
+
def self.call(column, index)
|
24
|
+
return :"column#{index}" if column.nil? || column.empty?
|
25
|
+
|
26
|
+
column = column.dup
|
24
27
|
column.strip!
|
25
28
|
column.downcase!
|
26
29
|
REPLACE_MAP.each do |data|
|
data/lib/honey_format/csv.rb
CHANGED
@@ -9,11 +9,15 @@ module HoneyFormat
|
|
9
9
|
# @return [CSV] a new instance of CSV.
|
10
10
|
# @param [String] csv string.
|
11
11
|
# @param [Array<Symbol>] valid_columns valid array of symbols representing valid columns if empty all will be considered valid.
|
12
|
-
# @param [Array<String>] header optional argument for CSV header
|
13
|
-
# @param [#call] row_builder will be called for each parsed row
|
14
|
-
# @raise [
|
15
|
-
# @raise [
|
16
|
-
# @raise [
|
12
|
+
# @param [Array<String>] header optional argument for CSV header.
|
13
|
+
# @param [#call] row_builder will be called for each parsed row.
|
14
|
+
# @raise [HeaderError] super class of errors raised when there is a CSV header error.
|
15
|
+
# @raise [MissingHeaderError] raised when header is missing (empty or nil).
|
16
|
+
# @raise [MissingHeaderColumnError] raised when header column is missing.
|
17
|
+
# @raise [UnknownHeaderColumnError] raised when column is not in valid list.
|
18
|
+
# @raise [RowError] super class of errors raised when there is a row error.
|
19
|
+
# @raise [EmptyRowColumnsError] raised when row columns are empty.
|
20
|
+
# @raise [InvalidRowLengthError] raised when row has more columns than header columns.
|
17
21
|
def initialize(csv, delimiter: ',', header: nil, valid_columns: [], header_converter: ConvertHeaderValue, row_builder: nil)
|
18
22
|
csv = ::CSV.parse(csv, col_sep: delimiter)
|
19
23
|
header_row = header || csv.shift
|
@@ -27,7 +31,7 @@ module HoneyFormat
|
|
27
31
|
@header.original
|
28
32
|
end
|
29
33
|
|
30
|
-
# CSV columns
|
34
|
+
# CSV columns converted from the original CSV header
|
31
35
|
# @return [Array<Symbol>] of column identifiers.
|
32
36
|
def columns
|
33
37
|
@header.to_a
|
@@ -50,8 +54,8 @@ module HoneyFormat
|
|
50
54
|
|
51
55
|
# Convert CSV object as CSV-string.
|
52
56
|
# @return [String] CSV-string representation.
|
53
|
-
def to_csv
|
54
|
-
header.to_csv + @rows.to_csv
|
57
|
+
def to_csv(columns: nil)
|
58
|
+
@header.to_csv(columns: columns) + @rows.to_csv(columns: columns)
|
55
59
|
end
|
56
60
|
end
|
57
61
|
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module HoneyFormat
|
2
|
+
# Errors
|
3
|
+
module Errors
|
4
|
+
# Header errors
|
5
|
+
# Super class of errors raised when there is a header error
|
6
|
+
class HeaderError < StandardError; end
|
7
|
+
# Raised when header is missing
|
8
|
+
class MissingHeaderError < HeaderError; end
|
9
|
+
# Raised when header column is missing
|
10
|
+
class MissingHeaderColumnError < HeaderError; end
|
11
|
+
# Raised when a column is not in passed valid columns
|
12
|
+
class UnknownHeaderColumnError < HeaderError; end
|
13
|
+
|
14
|
+
# Row errors
|
15
|
+
# Super class of errors raised when there is a row error
|
16
|
+
class RowError < StandardError; end
|
17
|
+
# Raised when row columns are empty
|
18
|
+
class EmptyRowColumnsError < RowError; end
|
19
|
+
# Raised when row has more columns than header columns
|
20
|
+
class InvalidRowLengthError < RowError; end
|
21
|
+
end
|
22
|
+
|
23
|
+
include Errors
|
24
|
+
end
|
data/lib/honey_format/header.rb
CHANGED
@@ -8,20 +8,20 @@ module HoneyFormat
|
|
8
8
|
# Instantiate a Header
|
9
9
|
# @return [Header] a new instance of Header.
|
10
10
|
# @param [Array<String>] header array of strings.
|
11
|
-
# @param [Array<Symbol>] valid array
|
11
|
+
# @param [Array<Symbol, String>] valid array representing the valid columns, if empty all columns will be considered valid.
|
12
12
|
# @param converter [#call] header converter that implements a #call method that takes one column (string) argument.
|
13
|
-
# @raise [
|
14
|
-
# @raise [
|
13
|
+
# @raise [MissingHeaderColumnError] raised when header is missing
|
14
|
+
# @raise [UnknownHeaderColumnError] raised when column is not in valid list.
|
15
15
|
# @example Instantiate a header with a customer converter
|
16
16
|
# converter = ->(col) { col == 'username' ? 'handle' : col }
|
17
17
|
# header = HoneyFormat::Header.new(['name', 'username'], converter: converter)
|
18
18
|
# header.to_a # => ['name', 'handle']
|
19
19
|
def initialize(header, valid: [], converter: ConvertHeaderValue)
|
20
20
|
if header.nil? || header.empty?
|
21
|
-
raise(
|
21
|
+
raise(Errors::MissingHeaderError, "CSV header can't be empty.")
|
22
22
|
end
|
23
23
|
|
24
|
-
@original_header = header
|
24
|
+
@original_header = header
|
25
25
|
@converter = converter
|
26
26
|
@columns = build_columns(@original_header, valid)
|
27
27
|
end
|
@@ -36,51 +36,84 @@ module HoneyFormat
|
|
36
36
|
# @return [Enumerator]
|
37
37
|
# If no block is given, an enumerator object will be returned.
|
38
38
|
def each(&block)
|
39
|
-
|
39
|
+
columns.each(&block)
|
40
40
|
end
|
41
41
|
|
42
42
|
# Returns columns as array.
|
43
43
|
# @return [Array<Symbol>] of columns.
|
44
|
-
def
|
44
|
+
def columns
|
45
45
|
@columns
|
46
46
|
end
|
47
47
|
|
48
|
+
# Returns columns as array.
|
49
|
+
# @return [Array<Symbol>] of columns.
|
50
|
+
def to_a
|
51
|
+
columns
|
52
|
+
end
|
53
|
+
|
48
54
|
# Return the number of header columns
|
49
55
|
# @return [Integer] the number of header columns
|
50
56
|
def length
|
51
|
-
|
57
|
+
columns.length
|
52
58
|
end
|
53
59
|
alias_method :size, :length
|
54
60
|
|
55
61
|
# Header as CSV-string
|
56
62
|
# @return [String] CSV-string representation.
|
57
|
-
def to_csv
|
58
|
-
|
63
|
+
def to_csv(columns: nil)
|
64
|
+
attributes = if columns
|
65
|
+
self.columns & columns
|
66
|
+
else
|
67
|
+
self.columns
|
68
|
+
end
|
69
|
+
|
70
|
+
::CSV.generate_line(attributes)
|
59
71
|
end
|
60
72
|
|
61
73
|
private
|
62
74
|
|
63
75
|
# Convert original header
|
64
76
|
# @param [Array<String>] header the original header
|
65
|
-
# @param [Array<Symbol>] valid list of valid column names if empty all are considered valid.
|
66
77
|
# @return [Array<String>] converted columns
|
67
78
|
def build_columns(header, valid)
|
68
79
|
valid = valid.map(&:to_sym)
|
69
80
|
|
70
|
-
header.map do |
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
raise(MissingCSVHeaderColumnError, "CSV header column can't be empty.")
|
81
|
+
header.each_with_index.map do |header_column, index|
|
82
|
+
convert_column(header_column, index).tap do |column|
|
83
|
+
maybe_raise_missing_column!(column)
|
84
|
+
maybe_raise_unknown_column!(column, valid)
|
75
85
|
end
|
86
|
+
end
|
87
|
+
end
|
76
88
|
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
89
|
+
def convert_column(column, index)
|
90
|
+
return @converter.call(column) if converter_arity == 1
|
91
|
+
@converter.call(column, index)
|
92
|
+
end
|
81
93
|
|
82
|
-
|
83
|
-
|
94
|
+
def converter_arity
|
95
|
+
# procs and lambdas respond to #arity
|
96
|
+
return @converter.arity if @converter.respond_to?(:arity)
|
97
|
+
@converter.method(:call).arity
|
98
|
+
end
|
99
|
+
|
100
|
+
def maybe_raise_unknown_column!(column, valid)
|
101
|
+
return if valid.empty?
|
102
|
+
return if valid.include?(column)
|
103
|
+
|
104
|
+
err_msg = "column :#{column} not in #{valid.inspect}"
|
105
|
+
raise(Errors::UnknownHeaderColumnError, err_msg)
|
106
|
+
end
|
107
|
+
|
108
|
+
def maybe_raise_missing_column!(column)
|
109
|
+
return if column && !column.empty?
|
110
|
+
|
111
|
+
parts = [
|
112
|
+
"CSV header column can't be nil or empty!",
|
113
|
+
"When you pass your own converter make sure that it never returns nil or an empty string.",
|
114
|
+
'Instead generate unique columns names.'
|
115
|
+
]
|
116
|
+
raise(Errors::MissingHeaderColumnError, parts.join(' '))
|
84
117
|
end
|
85
118
|
end
|
86
119
|
end
|
data/lib/honey_format/row.rb
CHANGED
@@ -7,13 +7,13 @@ module HoneyFormat
|
|
7
7
|
# @return [Row] a new instance of Row.
|
8
8
|
# @param [Array] columns an array of symbols.
|
9
9
|
# @param builder [#call, #to_csv] optional row builder
|
10
|
-
# @raise [
|
10
|
+
# @raise [EmptyRowColumnsError] raised when there are no columns.
|
11
11
|
# @example Create new row
|
12
12
|
# Row.new!([:id])
|
13
13
|
def initialize(columns, builder: nil)
|
14
14
|
if columns.empty?
|
15
15
|
err_msg = 'Expected array with at least one element, but was empty.'
|
16
|
-
raise(
|
16
|
+
raise(Errors::EmptyRowColumnsError, err_msg)
|
17
17
|
end
|
18
18
|
|
19
19
|
@row_builder = RowBuilder.new(*columns)
|
@@ -41,7 +41,7 @@ module HoneyFormat
|
|
41
41
|
"row: #{row.inspect}",
|
42
42
|
"orignal message: '#{e.message}'"
|
43
43
|
].join(', ')
|
44
|
-
raise(InvalidRowLengthError, err_msg)
|
44
|
+
raise(Errors::InvalidRowLengthError, err_msg)
|
45
45
|
end
|
46
46
|
end
|
47
47
|
end
|
@@ -9,8 +9,11 @@ module HoneyFormat
|
|
9
9
|
|
10
10
|
# Represent row as CSV
|
11
11
|
# @return [String] CSV-string representation.
|
12
|
-
def to_csv
|
13
|
-
|
12
|
+
def to_csv(columns: nil)
|
13
|
+
attributes = members
|
14
|
+
attributes = columns & attributes if columns
|
15
|
+
|
16
|
+
row = attributes.map do |column_name|
|
14
17
|
column = public_send(column_name)
|
15
18
|
next column.to_csv if column.respond_to?(:to_csv)
|
16
19
|
next if column.nil?
|
data/lib/honey_format/rows.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
require 'set'
|
1
2
|
require 'honey_format/row'
|
2
3
|
|
3
4
|
module HoneyFormat
|
@@ -35,8 +36,10 @@ module HoneyFormat
|
|
35
36
|
alias_method :size, :length
|
36
37
|
|
37
38
|
# @return [String] CSV-string representation.
|
38
|
-
def to_csv
|
39
|
-
|
39
|
+
def to_csv(columns: nil)
|
40
|
+
# Convert columns to Set for performance
|
41
|
+
columns = Set.new(columns) if columns
|
42
|
+
to_a.map { |row| row.to_csv(columns: columns) }.join
|
40
43
|
end
|
41
44
|
|
42
45
|
private
|
data/lib/honey_format/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: honey_format
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.9.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jacob Burenstam
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-
|
11
|
+
date: 2018-06-09 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -119,7 +119,7 @@ files:
|
|
119
119
|
- lib/honey_format.rb
|
120
120
|
- lib/honey_format/convert_header_value.rb
|
121
121
|
- lib/honey_format/csv.rb
|
122
|
-
- lib/honey_format/
|
122
|
+
- lib/honey_format/errors.rb
|
123
123
|
- lib/honey_format/header.rb
|
124
124
|
- lib/honey_format/row.rb
|
125
125
|
- lib/honey_format/row_builder.rb
|
@@ -1,14 +0,0 @@
|
|
1
|
-
module HoneyFormat
|
2
|
-
# Raised when header is missing
|
3
|
-
class MissingCSVHeaderError < StandardError; end
|
4
|
-
# Raised when there is a CSV header column error
|
5
|
-
class CSVHeaderColumnError < StandardError; end
|
6
|
-
# Raised when header column is missing
|
7
|
-
class MissingCSVHeaderColumnError < CSVHeaderColumnError; end
|
8
|
-
# Raised when a column is not in passed valid columns
|
9
|
-
class UnknownCSVHeaderColumnError < CSVHeaderColumnError; end
|
10
|
-
# Raised when columns are empty
|
11
|
-
class EmptyColumnsError < ArgumentError; end
|
12
|
-
# Raised when row has more columns than columns
|
13
|
-
class InvalidRowLengthError < ArgumentError; end
|
14
|
-
end
|