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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 50238c752453380d738cfc60c2b0db25ea5f692887c39776905bed5e91aedf6b
4
- data.tar.gz: 8cbdde37fbb98d69fa2d2f0bee3bf4eca78976fc7ff0ee93a17af630b557728f
3
+ metadata.gz: dd242cb77ccf8589eb2df059d887e13596f9c33a14fa587a886bcd376a6d728e
4
+ data.tar.gz: 72842684af98946cd50f7756818a4e9ce94b6c35d6c23a1e14661f70a93d3bac
5
5
  SHA512:
6
- metadata.gz: 9c69b356e531fc429be88738e96669be8acca70a224cd80a1de87bc8caf489ac6b993eb0eac79cb93deb419a27f9e725486c1856b0698ed47c5653c902156758
7
- data.tar.gz: ec12724d42d2bfcdf2c0fbfebd472671ea5d91250e8713cc03b6a9406943afceea89b05c5b62e3d3da76f3edd0d9b6d48ec6e134df5627dc13de25d042e02497
6
+ metadata.gz: ed5f6fa7f275d7445ca2962aa1787f8c10e25125253c3c65e3a5732cb0ef40e91f7c7d6bf73fa2d49d8d684c43bfec48058369f38df2e9ed5a52522f36f59d8c
7
+ data.tar.gz: 987843e3c4a987112bfbd46d00fa61a885ef12b71f19005e578e49d3f6f1bbe3af9022e3666af90437e015d522dbc7557a9f3065c45e4d0dd797d48670a9667f
@@ -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, Username\n 1, buren"
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, Username\n 1, buren"
41
+ csv_string = "Id,Username\n1,buren"
42
42
  csv = HoneyFormat::CSV.new(csv_string)
43
43
  csv.header # => ["Id", "Username"]
44
- csv.column # => [:id, :username]
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, Username\n 1, buren"
55
- upcaser = ->(row) { row.username.upcase!; row }
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, Username\n 1, buren"
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, Username\n 1, buren"
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 # => "Id, Username\n,buren\n"
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, Username\n 1, buren"
101
+ csv_string = "Id,Username\n1,buren"
95
102
  # Invalid
96
103
  HoneyFormat::CSV.new(csv_string, valid_columns: [:something, :username])
97
- # => #<HoneyFormat::MissingCSVHeaderColumnError: key :id ("Id") not in [:something, :username]>
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, buren"
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
@@ -1,5 +1,5 @@
1
1
  require 'honey_format/version'
2
- require 'honey_format/exceptions'
2
+ require 'honey_format/errors'
3
3
  require 'honey_format/csv'
4
4
 
5
5
  # Main module for HoneyFormat
@@ -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|
@@ -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 [MissingCSVHeaderError] raised when header is missing (empty or nil).
15
- # @raise [MissingCSVHeaderColumnError] raised when header column is missing.
16
- # @raise [UnknownCSVHeaderColumnError] raised when column is not in valid list.
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
@@ -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 of symbols representing valid columns if empty all columns will be considered valid.
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 [MissingCSVHeaderColumnError] raised when header is missing
14
- # @raise [UnknownCSVHeaderColumnError] raised when column is not in valid list.
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(MissingCSVHeaderError, "CSV header can't be empty.")
21
+ raise(Errors::MissingHeaderError, "CSV header can't be empty.")
22
22
  end
23
23
 
24
- @original_header = header.map { |col| col ? col.strip : nil }
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
- @columns.each(&block)
39
+ columns.each(&block)
40
40
  end
41
41
 
42
42
  # Returns columns as array.
43
43
  # @return [Array<Symbol>] of columns.
44
- def to_a
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
- @columns.length
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
- ::CSV.generate_line(@columns)
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 |column|
71
- column = @converter.call(column.dup)
72
-
73
- if column.nil? || column.empty?
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
- if valid.any? && !valid.include?(column)
78
- err_msg = "column :#{column} not in #{valid.inspect}"
79
- raise(UnknownCSVHeaderColumnError, err_msg)
80
- end
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
- column
83
- end
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
@@ -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 [EmptyColumnsError] raised when there are no columns.
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(EmptyColumnsError, err_msg)
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
- row = members.map do |column_name|
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?
@@ -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
- to_a.map(&:to_csv).join
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
@@ -1,4 +1,4 @@
1
1
  module HoneyFormat
2
2
  # Gem version
3
- VERSION = '0.8.2'
3
+ VERSION = '0.9.0'
4
4
  end
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.8.2
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-05-16 00:00:00.000000000 Z
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/exceptions.rb
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