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 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