honey_format 0.8.2 → 0.9.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 +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
|