csv2hash 0.0.2 → 0.1

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
  SHA1:
3
- metadata.gz: d4d2f7ba34ef01f65b28e952dcc72dc479ed2891
4
- data.tar.gz: 77bfe0698393fedb815d54f631d8e6e7af05f401
3
+ metadata.gz: a139a39be73ed85cce068c4b1b34c564a7c5a7a7
4
+ data.tar.gz: 26ad3fe5d1ce458dfcd8dd4269d519f419f79592
5
5
  SHA512:
6
- metadata.gz: d246e42bd2202619bc8e4ad3ca581c9c5b3fa15be34bcee30817d31030b782d33d1f1e2d89cf95df17059c21e63c185bc4a682ec9a08fbc5c1242f18ed0e8800
7
- data.tar.gz: b863db362851632a77e7a157d5a112f67e0e8d3c6ae31dd7a2d610fdc92defb1c59f720656a84e54f2ac207b79b2e695924e91925c99dbc808322cb4c2ceb101
6
+ metadata.gz: 9270e2cf190351a68be08114049fb16392769da65b3fb8eb03206f699a898bf8a99e21d3cf6f6fd28196dfbd3197cb9f0918f3563205d2c1b0d1ddf3064a6b5c
7
+ data.tar.gz: 55f28e1b266ca939c901d47d61bb6bd58b91d25506e764da2bab3ae62b9022ff190f630b29cb199a7a09f33714d367b7506e51c9d3a5c696095d5b58073de82c
data/.coveralls.yml CHANGED
@@ -1,2 +1 @@
1
- service_name: travis-pro
2
1
  repo_token: N3Ql9kjuVUDMaof2gKqn1TiNXt4bYfRUw
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- csv2hash (0.0.2)
4
+ csv2hash (0.1)
5
5
 
6
6
  GEM
7
7
  remote: http://rubygems.org/
data/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
  [![Coverage Status](https://coveralls.io/repos/joel/csv2hash/badge.png)](https://coveralls.io/r/joel/csv2hash)
10
10
 
11
11
 
12
- It's DSL for valided and map CSV to Ruby Hash
12
+ It is a DSL to validate and map a CSV to a Ruby Hash.
13
13
 
14
14
  ## Installation
15
15
 
@@ -27,21 +27,23 @@ Or install it yourself as:
27
27
 
28
28
  ## Usage
29
29
 
30
- #### Rules
30
+ Parsing is based on rules, you must defined rules of parsing
31
31
 
32
- You should be declare an definition for you CSV, for each cells you should define what you expect.
32
+ ### Rules
33
+
34
+ You should declare a definition for you CSV, and then define for each cell what you would expect.
33
35
 
34
36
  Example :
35
37
 
36
- You want first cell parsed should be string with values are 'yes' or 'no' you must fill follow rule :
38
+ You want the cell located on first line, first column to be a string with its values to be 'yes' or 'no'. Then you can right the following validation rule :
37
39
 
38
40
  { name: 'aswering', type: 'string', values: ['yes', 'no'], position: [0,0] }
39
41
 
40
- All keys as default values, so you can just define this rule :
42
+ The type is 'string' by default, so can just write:
41
43
 
42
44
  { name: 'aswering', values: ['yes', 'no'], position: [0,0] }
43
45
 
44
- You can define message, default is 'undefined :key on :position'
46
+ You can define a message, default is 'undefined :key on :position'
45
47
 
46
48
  { name: 'aswering', values: ['yes', 'no'], position: [0,0], message: 'this value is not supported' }
47
49
 
@@ -49,37 +51,52 @@ You can also define Range
49
51
 
50
52
  { name: 'score', values: 0..5, position: [0,0] }
51
53
 
52
- if you insert key on you message they will be substituted
54
+ The message is parsed:
53
55
 
54
56
  { ..., message: 'value of :name is not supported, please you one of :values' }
55
57
 
56
- produce :
57
-
58
+ It produces :
58
59
 
59
60
  value of aswering is not supported, please you one of [yes, no]
60
61
 
61
- ##### Position
62
+ ### Default values
63
+
64
+ Only position is required:
65
+
66
+ * :position
67
+
68
+ All remaining keys are optionals:
69
+
70
+ * message: 'undefined :key on :position'
71
+ * mappable: true
72
+ * type: 'string'
73
+ * values: nil
74
+ * nested: nil
75
+ * allow_blank: false
76
+ * extra_validator: nil
62
77
 
63
- Position mean [Y, X], where Y is rows, X columns
78
+ ## Define where your data is expected
64
79
 
65
- #### Definition
80
+ **IMPORTANT!** Position mean [Y, X], where Y is rows, X columns
66
81
 
67
- You should provide a definition, you have 2 types of definitions, mapping definition for search on x,y in your data or collection definition for rules apply for all lines in x, so you position rules should be only x value
82
+ A definition should be provided. There are 2 types of definitions:
83
+ * search for data at a precise position in the table: `y,x`
84
+ * or search for data in a column of rows, where all the rows are the same: `x` (column index)
68
85
 
69
- ### Sample
86
+ ## Samples
70
87
 
71
- #### Mapping
88
+ ### Validation of a cell at a precise position
72
89
 
73
- Consider csv data like that
90
+ Consider the following CSV:
74
91
 
75
92
  | Fields | Person Informations | Optional |
76
93
  |-------------|----------------------|----------|
77
- | Nickname | john | no |
94
+ | Nickname | jo | no |
78
95
  | First Name | John | yes |
79
96
  | Last Name | Doe | yes |
80
97
 
81
98
 
82
- Mapping sample definition
99
+ Precise position validation sample:
83
100
 
84
101
  class MyParser
85
102
 
@@ -89,6 +106,12 @@ Mapping sample definition
89
106
  @file_path = file_path
90
107
  end
91
108
 
109
+ def data
110
+ @data_wrapper ||= Csv2hash.new(definition, file_path).parse
111
+ end
112
+
113
+ private
114
+
92
115
  def rules
93
116
  [].tap do |mapping|
94
117
  mapping << { position: [2,1], key: 'first_name' }
@@ -97,27 +120,21 @@ Mapping sample definition
97
120
  end
98
121
 
99
122
  def definition
100
- Definition.new(rules, type = Definition::MAPPING)
101
- end
102
-
103
- def data
104
- Csv2hash.new(definition, file_path).tap do |csv2hash|
105
- csv2hash.parse
106
- end.data
123
+ Csv2Hash::Definition.new(rules, type = Csv2Hash::Definition::MAPPING, 1)
107
124
  end
108
125
 
109
126
  end
110
127
 
111
- #### Collection
128
+ ### Validation of a collection
112
129
 
113
- Consider csv data like that
130
+ Consider the following CSV:
114
131
 
115
132
  | Nickname | First Name | Last Name |
116
133
  |----------|------------|-----------|
117
- | john | John | Doe |
118
- | jane | Jane | Doe |
134
+ | jo | John | Doe |
135
+ | ja | Jane | Doe |
119
136
 
120
- Mapping sample definition
137
+ Collection validation sample:
121
138
 
122
139
  class MyParser
123
140
 
@@ -127,6 +144,12 @@ Mapping sample definition
127
144
  @file_path = file_path
128
145
  end
129
146
 
147
+ def data
148
+ @data_wrapper ||= Csv2hash.new(definition, file_path).parse
149
+ end
150
+
151
+ private
152
+
130
153
  def rules
131
154
  [].tap do |mapping|
132
155
  mapping << { position: 0, key: 'nickname' }
@@ -136,60 +159,125 @@ Mapping sample definition
136
159
  end
137
160
 
138
161
  def definition
139
- Definition.new(rules, type = Definition::COLLECTION)
140
- end
141
-
142
- def data
143
- Csv2hash.new(definition, file_path).tap do |csv2hash|
144
- csv2hash.parse
145
- end.data
162
+ Csv2Hash::Definition.new(rules, type = Csv2Hash::Definition::MAPPING, 1)
146
163
  end
147
164
 
148
165
  end
149
166
 
150
- #### Headers
167
+ ### CSV Headers
151
168
 
152
- You should be define header size
169
+ You can define the number of rows to skip in the header of the CSV.
153
170
 
154
171
  Definition.new(rules, type, header_size=0)
155
172
 
156
- #### Exception or CSV mode
173
+ ### Parser and configuration
157
174
 
158
- You can choice 2 mode of parsing, either exception mode for raise exception in first breaking rules or csv mode for get csv original data + errors throwing into added columns.
175
+ Pasrer take severale parameters like that :
159
176
 
177
+ initialize definition, file_path, exception_mode=true, data_source=nil, ignore_blank_line=false
160
178
 
161
- parse return data or csv_with_errors if parse is invalid, you can plug this like that :
179
+ you can pass directly Array of data (Array at 2 dimensions) really useful for testing, if you don't care about line blank in your CSV you can ignore them.
162
180
 
163
- csv2hash = Csv2hash.new(definition, 'file_path').new
164
- result = csv2hash.parse
165
- return result if csv2hash.valid?
181
+ ### Response
166
182
 
167
- filename = 'issues_errors.csv'
168
- tempfile = Tempfile.new [filename, File.extname(filename)]
169
- File.open(tempfile.path, 'wb') { |file| file.write result }
183
+ The parser return values wrapper into DataWrapper Object, you can call .valid? method on this Object and grab either data or errors like that :
170
184
 
171
- # Send mail with csv file + errors and free resource
185
+ response = parser.parse
186
+ if response.valid?
187
+ response.data
188
+ else
189
+ response.errors
190
+ end
172
191
 
173
- tempfile.unlink
192
+ data or errors are Array, but errors can be formatted on csv format with .to_csv call
174
193
 
194
+ response.errors.to_csv
175
195
 
176
- #### Default values
196
+ ## Exception or Not !
177
197
 
178
- only position is require
198
+ You can choice 2 modes of parsing, either **exception mode** for raise exception in first breaking rules or **csv mode** for get csv original data + errors throwing into added columns.
179
199
 
180
- * :position
200
+ ### On **CSV MODE** you can choose different way for manage errors
181
201
 
182
- all remaind keys are optionals
202
+ `.parse()` return `data_wrapper` if `.parse()` is invalid, you can code your own behavior like that :
203
+
204
+ in your code
205
+
206
+ parser = Csv2hash.new(definition, 'file_path').new
207
+ response = parser.parse
208
+ return response if response.valid?
209
+ # Whatever
210
+
211
+ In the same time Csv2hash call **notify(response)** method when CSV parsing fail, you can add your own Notifier like that
212
+
213
+ module Csv2hash
214
+ module Plugins
215
+ class Notifier
216
+ def initialize csv2hash
217
+ csv2hash.notifier.extend NotifierWithEmail
218
+ end
219
+
220
+ module NotifierWithEmail
221
+ def notify response
222
+ filename = 'issues_errors.csv'
223
+ tempfile = Tempfile.new [filename, File.extname(filename)]
224
+ File.open(tempfile.path, 'wb') { |file| file.write response.errors.to_csv }
225
+ # Send mail with csv file + errors and free resource
226
+ tempfile.unlink
227
+ end
228
+ end
229
+ end
230
+ end
231
+ end
232
+
233
+ Or other implementation
234
+
235
+ ### Errors Format
236
+
237
+ errors is a Array of Hash
238
+
239
+ { y: 1, x: 0, message: 'message', key: 'key', value: '' }
240
+
241
+ ## Sample
242
+
243
+ ### Csv data
244
+
245
+ | Fields | Person Informations |
246
+ |-------------|----------------------|
247
+ | Nickname | nil |
248
+
249
+ ### Rule
250
+
251
+ { position: [1,1], key: 'nickname', allow_blank: false }
252
+
253
+ ### Error
254
+
255
+ { y: 1, x: 1, message: 'undefined nikcname on [0, 0]', key: 'nickname', value: nil }
256
+ ## Personal Validator Rule
257
+
258
+ You can define your own Validator
259
+
260
+ For downcase validation
261
+
262
+ class DowncaseValidator < Csv2hash::ExtraValidator
263
+ def valid? value
264
+ !!(value.match /^[a-z]+$/)
265
+ end
266
+ end
267
+
268
+ in your rule
269
+
270
+ { position: [0,0], key: 'name', extra_validator: DowncaseValidator.new,
271
+ message: 'your data should be writting in downcase only' }
272
+
273
+ Csv data
274
+
275
+ [ [ 'Foo' ] ]
183
276
 
184
- * message: 'undefined :key on :position'
185
- * mappable: true
186
- * type: 'string'
187
- * values: nil
188
- * nested: nil
189
- * allow_blank: false
190
277
 
191
278
  ### Limitations
192
279
 
280
+ * Don't manage number of column you expected on collection mode
193
281
 
194
282
  ## Contributing
195
283
 
@@ -197,4 +285,4 @@ all remaind keys are optionals
197
285
  2. Create your feature branch (`git checkout -b my-new-feature`)
198
286
  3. Commit your changes (`git commit -am 'Added some feature'`)
199
287
  4. Push to the branch (`git push origin my-new-feature`)
200
- 5. Create new Pull Request
288
+ 5. Create new Pull Request
data/ReleaseNotes.md ADDED
@@ -0,0 +1,16 @@
1
+ VERSION 0.1
2
+
3
+ * Add csv errors mode
4
+ * Add Extra Validator
5
+ * Add Notifier Plugin
6
+ * Add Data Wrapper Response
7
+ * Add possibility to pass directly data in constructor, for more testing
8
+ * You can ignore blank line
9
+
10
+ VERSION 0.0.2
11
+
12
+ * Liitle enhancement
13
+
14
+ VERSION 0.0.1
15
+
16
+ * Skeleton and first parsing
data/bin/publish CHANGED
@@ -15,7 +15,7 @@ class Publish
15
15
 
16
16
  end
17
17
 
18
- if ARGV.length != 1 or !ARGV[0].match(/\d{1,3}.\d{1,3}.\d{1,3}/)
18
+ if ARGV.length != 1 # or !ARGV[0].match(/\d{1,3}.\d{1,3}.\d{1,3}/)
19
19
  puts 'HELP: '
20
20
  puts '$ bin/publish 0.0.1'
21
21
  exit 0
data/csv2hash.gemspec CHANGED
@@ -1,10 +1,10 @@
1
1
  Gem::Specification.new do |spec|
2
2
 
3
3
  spec.name = 'csv2hash'
4
- spec.version = '0.0.2'
4
+ spec.version = '0.1'
5
5
  spec.date = '2013-11-26'
6
- spec.summary = %q{Mapping CSV to Ruby Hash}
7
- spec.description = %q{DSL for CSV Ruby Hash mapping}
6
+ spec.summary = %q{Mapping a CSV to a Ruby Hash}
7
+ spec.description = %q{DSL to map a CSV to a Ruby Hash}
8
8
  spec.authors = ['Joel AZEMAR']
9
9
  spec.email = 'joel.azemar@gmail.com'
10
10
  spec.files = ['lib/csv2hash.rb']
@@ -21,4 +21,4 @@ Gem::Specification.new do |spec|
21
21
  spec.add_development_dependency 'rspec'
22
22
 
23
23
  spec.required_ruby_version = '~> 2.0'
24
- end
24
+ end
@@ -4,7 +4,7 @@ class CsvArray < Array
4
4
  def to_csv options = {}
5
5
  CSV.generate(options) do |csv|
6
6
  self.each do |element|
7
- csv << element
7
+ csv << [element[:value], element[:message]]
8
8
  end
9
9
  end
10
10
  end
@@ -0,0 +1,13 @@
1
+ class Csv2hash
2
+ class DataWrapper
3
+
4
+ attr_accessor :data, :errors, :valid
5
+
6
+ def initialize
7
+ self.valid = true
8
+ self.data, self.errors = [], []
9
+ end
10
+
11
+ def valid?() valid ; end
12
+ end
13
+ end
@@ -1,52 +1,55 @@
1
- class Definition
1
+ class Csv2hash
2
+ class Definition
2
3
 
3
- MAPPING = 'mapping'
4
- COLLECTION = 'collection'
4
+ MAPPING = 'mapping'
5
+ COLLECTION = 'collection'
5
6
 
6
- TYPES = [Definition::MAPPING, Definition::COLLECTION]
7
+ TYPES = [MAPPING, COLLECTION]
7
8
 
8
- attr_accessor :rules, :type, :header_size
9
+ attr_accessor :rules, :type, :header_size
9
10
 
10
- def initialize rules, type, header_size=0
11
- @rules, @type, @header_size = rules, type, header_size
12
- end
11
+ def initialize rules, type, header_size=0
12
+ self.rules, self.type, self.header_size = rules, type, header_size
13
+ end
13
14
 
14
- def validate!
15
- unless TYPES.include?(type)
16
- raise "not suitable type, please use '#{Definition::MAPPING}' or '#{Definition::COLLECTION}'"
15
+ def validate!
16
+ unless TYPES.include?(type)
17
+ raise "not suitable type, please use '#{MAPPING}' or '#{COLLECTION}'"
18
+ end
19
+ raise 'rules must be an Array of rules' unless rules.class == Array
17
20
  end
18
- raise 'rules must be an Array of rules' unless rules.class == Array
19
- end
20
21
 
21
- def default!
22
- rules.each do |rule|
23
- default_position rule
24
- unless rule.has_key? :message
25
- if rule.has_key? :values
26
- rule.merge! message: ':key not supported, please use one of :values'
27
- else
28
- rule.merge! message: 'undefined :key on :position'
22
+ def default!
23
+ rules.each do |rule|
24
+ default_position rule
25
+ unless rule.has_key? :message
26
+ if rule.has_key? :values
27
+ rule.merge! message: ':key not supported, please use one of :values'
28
+ else
29
+ rule.merge! message: 'undefined :key on :position'
30
+ end
29
31
  end
32
+ rule.merge! mappable: true unless rule.has_key? :mappable
33
+ rule.merge! type: 'string' unless rule.has_key? :type
34
+ rule.merge! values: nil unless rule.has_key? :values
35
+ rule.merge! nested: nil unless rule.has_key? :nested
36
+ rule.merge! allow_blank: false unless rule.has_key? :allow_blank
37
+ rule.merge! extra_validator: nil unless rule.has_key? :extra_validator
30
38
  end
31
- rule.merge! mappable: true unless rule.has_key? :mappable
32
- rule.merge! type: 'string' unless rule.has_key? :type
33
- rule.merge! values: nil unless rule.has_key? :values
34
- rule.merge! nested: nil unless rule.has_key? :nested
35
- rule.merge! allow_blank: false unless rule.has_key? :allow_blank
36
39
  end
37
- end
38
40
 
39
- private
41
+ private
40
42
 
41
- def default_position rule
42
- case type
43
- when Definition::MAPPING
44
- y, x = rule.fetch(:position, ['undefined', 'undefined'])
45
- rule.merge! key: "key_#{y}_#{x}" unless rule.has_key? :key
46
- when Definition::COLLECTION
47
- x = rule.fetch :position
48
- rule.merge! key: "key_undefined_#{x}" unless rule.has_key? :key
43
+ def default_position rule
44
+ case type
45
+ when MAPPING
46
+ y, x = rule.fetch(:position, ['undefined', 'undefined'])
47
+ rule.merge! key: "key_#{y}_#{x}" unless rule.has_key? :key
48
+ when COLLECTION
49
+ x = rule.fetch :position
50
+ rule.merge! key: "key_undefined_#{x}" unless rule.has_key? :key
51
+ end
49
52
  end
50
- end
51
53
 
52
- end
54
+ end
55
+ end
@@ -0,0 +1,5 @@
1
+ class Csv2hash
2
+ class ExtraValidator
3
+ def valid?(rule, value) raise 'method parse!() should be implemented in sub class'; end
4
+ end
5
+ end
@@ -0,0 +1,7 @@
1
+ class Csv2hash
2
+ class Notifier
3
+ def notify response
4
+ # puts 'Csv with errors'
5
+ end
6
+ end
7
+ end
@@ -1,10 +1,12 @@
1
- module Parser::Collection extend Parser
1
+ module Csv2hash::Parser::Collection
2
+ include Csv2hash::Parser
2
3
 
3
4
  def fill!
4
- @data = {}.tap do |data_computed|
5
+ self.data = {}.tap do |data_computed|
5
6
  data_computed[:data] ||= []
6
- @data_source.each_with_index do |line, y|
7
+ self.data_source.each_with_index do |line, y|
7
8
  next if y < definition.header_size
9
+ next if self.ignore_blank_line and line.compact.empty?
8
10
  data_computed[:data] << {}.tap do |data_parsed|
9
11
  fill_it data_parsed, line
10
12
  end
@@ -26,4 +28,4 @@ module Parser::Collection extend Parser
26
28
  end
27
29
  end
28
30
 
29
- end
31
+ end
@@ -1,7 +1,8 @@
1
- module Parser::Mapping extend Parser
1
+ module Csv2hash::Parser::Mapping
2
+ include Csv2hash::Parser
2
3
 
3
4
  def fill!
4
- @data = {}.tap do |data_computed|
5
+ self.data = {}.tap do |data_computed|
5
6
  data_computed[:data] ||= []
6
7
  data_computed[:data] << {}.tap do |data_parsed|
7
8
  fill_it data_parsed, data_source
@@ -23,4 +24,4 @@ module Parser::Mapping extend Parser
23
24
  end
24
25
  end
25
26
 
26
- end
27
+ end
@@ -1,2 +1,4 @@
1
- module Parser
2
- end
1
+ class Csv2hash
2
+ module Parser
3
+ end
4
+ end
@@ -1,9 +1,10 @@
1
- module Validator::Collection
2
- include Validator
1
+ module Csv2hash::Validator::Collection
2
+ include Csv2hash::Validator
3
3
 
4
4
  def validate_data!
5
- @data_source.each_with_index do |line, y|
5
+ self.data_source.each_with_index do |line, y|
6
6
  next if y < definition.header_size
7
+ next if self.ignore_blank_line and line.compact.empty?
7
8
  validate_rules y
8
9
  end
9
10
  end
@@ -14,4 +15,4 @@ module Validator::Collection
14
15
  [nil, _position]
15
16
  end
16
17
 
17
- end
18
+ end
@@ -1,5 +1,5 @@
1
- module Validator::Mapping
2
- include Validator
1
+ module Csv2hash::Validator::Mapping
2
+ include Csv2hash::Validator
3
3
 
4
4
  def validate_data!
5
5
  validate_rules data_source
@@ -11,4 +11,4 @@ module Validator::Mapping
11
11
  _position
12
12
  end
13
13
 
14
- end
14
+ end
@@ -1,42 +1,44 @@
1
- module Validator
1
+ class Csv2hash
2
+ module Validator
3
+
4
+ def validate_rules y=nil
5
+ definition.rules.each do |rule|
6
+ _y, x = position rule.fetch(:position)
7
+ begin
8
+ validate_cell (_y||y), x, rule
9
+ rescue => e
10
+ self.errors << { y: (_y||y), x: x, message: e.message, key: rule.fetch(:key) }
11
+ raise if exception_mode
12
+ end
13
+ end
14
+ end
15
+
16
+ def valid?() self.errors.empty?; end
2
17
 
3
- attr_accessor :errors, :exception
18
+ protected
4
19
 
5
- def validate_rules y=nil
6
- definition.rules.each do |rule|
7
- _y, x = position rule.fetch(:position)
20
+ def validate_cell y, x, rule
21
+ value = data_source[y][x] rescue nil
8
22
  begin
9
- validate_cell (_y||y), x, rule
23
+ raise unless value unless rule.fetch :allow_blank
24
+ if (extra_validator = rule.fetch :extra_validator) && extra_validator.kind_of?(Csv2hash::ExtraValidator)
25
+ raise unless extra_validator.valid? rule, value
26
+ else
27
+ if value && (values = rule.fetch :values)
28
+ raise unless values.include?(value)
29
+ end
30
+ end
10
31
  rescue => e
11
- errors << { y: (_y||y), x: x, message: e.message, key: rule.fetch(:key) }
12
- raise if exception
32
+ raise message(rule, y, x)
13
33
  end
14
34
  end
15
- end
16
-
17
- def valid?() errors.empty?; end
18
-
19
- protected
20
35
 
21
- def validate_cell y, x, rule
22
-
23
- value = data_source[y][x] rescue nil
24
-
25
- begin
26
- raise unless value unless rule.fetch :allow_blank
27
- if value && (values = rule.fetch :values)
28
- raise unless values.include?(value)
36
+ def message rule, y, x
37
+ msg = rule.fetch(:message).tap do |msg|
38
+ rule.each { |key, value| msg.gsub!(":#{key.to_s}", value.to_s) unless key == :position }
29
39
  end
30
- rescue => e
31
- raise message(rule, y, x)
40
+ msg.gsub ':position', "[#{y}, #{x}]"
32
41
  end
33
- end
34
42
 
35
- def message rule, y, x
36
- msg = rule.fetch(:message).tap do |msg|
37
- rule.each { |key, value| msg.gsub!(":#{key.to_s}", value.to_s) unless key == :position }
38
- end
39
- msg.gsub ':position', "[#{y}, #{x}]"
40
43
  end
41
-
42
- end
44
+ end
@@ -1,3 +1,3 @@
1
1
  module Cvs2hash
2
- VERSION = '0.0.2'
2
+ VERSION = '0.1'
3
3
  end
data/lib/csv2hash.rb CHANGED
@@ -7,29 +7,52 @@ require 'csv2hash/parser'
7
7
  require 'csv2hash/parser/mapping'
8
8
  require 'csv2hash/parser/collection'
9
9
  require 'csv2hash/csv_array'
10
+ require 'csv2hash/data_wrapper'
11
+ require 'csv2hash/notifier'
12
+ require 'csv2hash/extra_validator'
13
+
10
14
  require 'csv'
11
15
 
12
16
  class Csv2hash
13
17
 
14
- attr_accessor :definition, :file_path, :data, :data_source
18
+ attr_accessor :definition, :file_path, :data, :notifier, :exception_mode, :errors, :ignore_blank_line
19
+
20
+ def initialize definition, file_path, exception_mode=true, data_source=nil, ignore_blank_line=false
21
+ @data_source = data_source
22
+ self.definition, self.file_path = definition, file_path
23
+ dynamic_lib_loading 'Parser'
24
+ self.exception_mode, self.errors = exception_mode, []
25
+ dynamic_lib_loading 'Validator'
26
+ self.notifier = Notifier.new
27
+ self.ignore_blank_line = ignore_blank_line
28
+ init_plugins
29
+ end
15
30
 
16
- def initialize definition, file_path, exception=true
17
- @definition, @file_path = definition, file_path
18
- dynamic_parser_loading
19
- @exception, @errors = exception, []
20
- dynamic_validator_loading
31
+ def init_plugins
32
+ begin
33
+ @plugins = []
34
+ ::Csv2hash::Plugins.constants.each do |name|
35
+ @plugins << ::Csv2hash::Plugins.const_get(name).new(self)
36
+ end
37
+ rescue; end
21
38
  end
22
39
 
23
40
  def parse
24
41
  load_data_source
42
+
25
43
  definition.validate!
26
44
  definition.default!
27
45
  validate_data!
28
- if valid?
29
- fill!
30
- data
31
- else
32
- csv_with_errors
46
+
47
+ Csv2hash::DataWrapper.new.tap do |response|
48
+ if valid?
49
+ fill!
50
+ response.data = data[:data]
51
+ else
52
+ response.valid = false
53
+ response.errors = csv_with_errors
54
+ notifier.notify response
55
+ end
33
56
  end
34
57
  end
35
58
 
@@ -37,36 +60,27 @@ class Csv2hash
37
60
  @csv_with_errors ||= begin
38
61
  CsvArray.new.tap do |rows|
39
62
  errors.each do |error|
40
- rows << (([data_source[error[:x]][error[:y]]]||[nil]) + [error[:message]])
63
+ rows << error.merge({ value: (data_source[error[:y]][error[:x]] rescue nil) })
41
64
  end
42
- end.to_csv
65
+ end #.to_csv
43
66
  end
44
67
  end
45
68
 
46
69
  # protected
47
70
 
48
71
  def data_source
49
- @data_source ||= CSV.read @file_path
72
+ @data_source ||= CSV.read self.file_path
50
73
  end
51
74
  alias_method :load_data_source, :data_source
52
75
 
53
76
  private
54
77
 
55
- def dynamic_validator_loading
56
- case definition.type
57
- when Definition::MAPPING
58
- self.extend Validator::Mapping
59
- when Definition::COLLECTION
60
- self.extend Validator::Collection
61
- end
62
- end
63
-
64
- def dynamic_parser_loading
78
+ def dynamic_lib_loading type
65
79
  case definition.type
66
- when Definition::MAPPING
67
- self.extend Parser::Mapping
68
- when Definition::COLLECTION
69
- self.extend Parser::Collection
80
+ when Csv2hash::Definition::MAPPING
81
+ self.extend Module.module_eval("Csv2hash::#{type}::Mapping")
82
+ when Csv2hash::Definition::COLLECTION
83
+ self.extend Module.module_eval("Csv2hash::#{type}::Collection")
70
84
  end
71
85
  end
72
86
 
@@ -1,17 +1,17 @@
1
1
  require 'spec_helper'
2
2
 
3
- describe Definition do
3
+ describe Csv2hash::Definition do
4
4
 
5
5
  context 'regular context' do
6
6
  subject do
7
- Definition.new(
7
+ Csv2hash::Definition.new(
8
8
  [ { position: [0,0], key: 'name' } ],
9
- Definition::MAPPING
9
+ Csv2hash::Definition::MAPPING
10
10
  )
11
11
  end
12
12
 
13
13
  it 'variable should be assigned' do
14
- subject.type.should eql Definition::MAPPING
14
+ subject.type.should eql Csv2hash::Definition::MAPPING
15
15
  subject.rules.should eql [ { position: [0,0], key: 'name' } ]
16
16
  end
17
17
  end
@@ -19,17 +19,18 @@ describe Definition do
19
19
  describe '#validate!' do
20
20
  context 'rules failling validation' do
21
21
  subject do
22
- Definition.new nil, 'unsuitable_type'
22
+ Csv2hash::Definition.new nil, 'unsuitable_type'
23
23
  end
24
24
  it 'should throw exception' do
25
25
  expect {
26
26
  subject.validate!
27
- }.to raise_error "not suitable type, please use '#{Definition::MAPPING}' or '#{Definition::COLLECTION}'"
27
+ }.to raise_error("not suitable type, please use '#{Csv2hash::Definition::MAPPING}' " \
28
+ "or '#{Csv2hash::Definition::COLLECTION}'")
28
29
  end
29
30
  end
30
31
  context 'rules failling validation' do
31
32
  subject do
32
- Definition.new 'rules',Definition::MAPPING
33
+ Csv2hash::Definition.new 'rules',Csv2hash::Definition::MAPPING
33
34
  end
34
35
  it 'should throw exception' do
35
36
  expect { subject.validate! }.to raise_error 'rules must be an Array of rules'
@@ -39,7 +40,7 @@ describe Definition do
39
40
 
40
41
  describe '#default!' do
41
42
  subject do
42
- Definition.new [ { position: [0,0], key: 'name' } ], Definition::MAPPING
43
+ Csv2hash::Definition.new [ { position: [0,0], key: 'name' } ], Csv2hash::Definition::MAPPING
43
44
  end
44
45
 
45
46
  before { subject.default! }
@@ -52,7 +53,8 @@ describe Definition do
52
53
  type: 'string',
53
54
  values: nil,
54
55
  nested: nil,
55
- allow_blank: false }])
56
+ allow_blank: false,
57
+ extra_validator: nil }])
56
58
  end
57
59
  end
58
60
  end
@@ -1,29 +1,27 @@
1
1
  require 'spec_helper'
2
2
 
3
- describe Parser::Collection do
3
+ describe Csv2hash::Parser::Collection do
4
4
 
5
5
  let(:definition) do
6
- Definition.new [ { position: 0, key: 'name' } ], Definition::COLLECTION
6
+ Csv2hash::Definition.new [ { position: 0, key: 'name' } ], Csv2hash::Definition::COLLECTION
7
7
  end
8
8
 
9
9
  let(:data_source) { [ [ 'John Doe' ], [ 'Jane Doe' ] ] }
10
10
 
11
11
  subject do
12
- Csv2hash.new(definition, 'file_path').tap do |csv2hash|
13
- csv2hash.data_source = data_source
14
- end
12
+ Csv2hash.new(definition, 'file_path', false, data_source)
15
13
  end
16
14
 
17
15
  context 'regular way' do
18
16
  it { expect { subject.parse }.to_not raise_error }
19
17
  it {
20
- subject.tap do |csv2hash|
21
- csv2hash.parse
18
+ subject.tap do |parser|
19
+ parser.parse
22
20
  end.data.should eql({ data: [ { 'name' => 'John Doe' }, { 'name' => 'Jane Doe' } ] })
23
21
  }
24
22
  context 'with header' do
25
23
  before { subject.definition.header_size = 1 }
26
- let(:data_source) { [ [ 'Name' ], [ 'John Doe' ], [ 'Jane Doe' ] ]}
24
+ let(:data_source) { [ [ 'Name' ], [ 'John Doe' ], [ 'Jane Doe' ] ] }
27
25
  it {
28
26
  subject.tap { |c| c.parse }.data.should eql({ data: [ { 'name' => 'John Doe' }, { 'name' => 'Jane Doe' } ] })
29
27
  }
@@ -46,4 +44,13 @@ describe Parser::Collection do
46
44
  }
47
45
  end
48
46
 
47
+ context '#ignore_blank_line' do
48
+ let(:data_source) { [ [ 'John Doe' ], [ 'Jane Doe' ], [ nil ] ] }
49
+ it {
50
+ subject.tap do |parser|
51
+ parser.ignore_blank_line = true
52
+ parser.parse
53
+ end.data.should eql({ data: [ { 'name' => 'John Doe' }, { 'name' => 'Jane Doe' } ] })
54
+ }
55
+ end
49
56
  end
@@ -1,17 +1,15 @@
1
1
  require 'spec_helper'
2
2
 
3
- describe Parser::Mapping do
3
+ describe Csv2hash::Parser::Mapping do
4
4
 
5
5
  let(:definition) do
6
- Definition.new [ { position: [0,0], key: 'name' } ], Definition::MAPPING
6
+ Csv2hash::Definition.new [ { position: [0,0], key: 'name' } ], Csv2hash::Definition::MAPPING
7
7
  end
8
8
 
9
9
  let(:data_source) { [ [ 'John Doe' ] ] }
10
10
 
11
11
  subject do
12
- Csv2hash.new(definition, 'file_path').tap do |csv2hash|
13
- csv2hash.data_source = data_source
14
- end
12
+ Csv2hash.new(definition, 'file_path', false, data_source)
15
13
  end
16
14
 
17
15
  context 'regular way' do
@@ -1,3 +1,3 @@
1
1
  require 'spec_helper'
2
2
 
3
- describe Parser
3
+ describe Csv2hash::Parser
@@ -1,18 +1,16 @@
1
1
  require 'spec_helper'
2
2
 
3
- describe Validator::Collection do
3
+ describe Csv2hash::Validator::Collection do
4
4
 
5
5
  let(:definition) do
6
- Definition.new([ { position: 0, key: 'name' } ], Definition::COLLECTION).tap do |definition|
6
+ Csv2hash::Definition.new([ { position: 0, key: 'name' } ], Csv2hash::Definition::COLLECTION).tap do |definition|
7
7
  definition.validate!
8
8
  definition.default!
9
9
  end
10
10
  end
11
11
 
12
12
  subject do
13
- Csv2hash.new(definition, 'file_path').tap do |csv2hash|
14
- csv2hash.data_source = data_source
15
- end
13
+ Csv2hash.new(definition, 'file_path', true, data_source)
16
14
  end
17
15
 
18
16
  context 'with valid data' do
@@ -25,8 +23,18 @@ describe Validator::Collection do
25
23
  end
26
24
  end
27
25
 
26
+ context '#ignore_blank_line' do
27
+ let(:data_source) { [ [ ] ] }
28
+ before { subject.ignore_blank_line = true }
29
+ it { expect { subject.validate_data! }.to_not raise_error }
30
+ context 'csv mode' do
31
+ before { subject.exception_mode = false }
32
+ its(:errors) { should be_empty }
33
+ end
34
+ end
35
+
28
36
  context 'with invalid data' do
29
- let(:data_source) { [ [ ] ]}
37
+ let(:data_source) { [ [ ] ] }
30
38
  it { expect { subject.validate_data! }.to raise_error('undefined name on [0, 0]') }
31
39
  context 'with header' do
32
40
  before { subject.definition.header_size = 1 }
@@ -37,8 +45,8 @@ describe Validator::Collection do
37
45
 
38
46
  context 'wihtout exception' do
39
47
  let(:data_source) { [ [ ] ]}
40
- before { subject.exception = false }
41
- it { subject.parse.should eql ",\"undefined name on [0, 0]\"\n" }
48
+ before { subject.exception_mode = false }
49
+ it { subject.parse.errors.to_csv.should eql ",\"undefined name on [0, 0]\"\n" }
42
50
 
43
51
  context 'errors should be filled' do
44
52
  before { subject.parse }
@@ -47,13 +55,14 @@ describe Validator::Collection do
47
55
 
48
56
  context 'original csv + errors should returned' do
49
57
  let(:definition) do
50
- Definition.new([ { position: 0, key: 'agree', values: ['yes', 'no'] } ], Definition::COLLECTION).tap do |d|
58
+ Csv2hash::Definition.new(
59
+ [{ position: 0, key: 'agree', values: ['yes', 'no'] }], Csv2hash::Definition::COLLECTION).tap do |d|
51
60
  d.validate!; d.default!
52
61
  end
53
62
  end
54
63
  let(:data_source) { [ [ 'what?' ], [ 'yes' ], [ 'no' ] ] }
55
- it { subject.parse.should eql "what?,\"agree not supported, please use one of [\"\"yes\"\", \"\"no\"\"]\"\n" }
64
+ it { subject.parse.errors.to_csv.should eql "what?,\"agree not supported, please use one of [\"\"yes\"\", \"\"no\"\"]\"\n" }
56
65
  end
57
66
  end
58
67
 
59
- end
68
+ end
@@ -1,18 +1,16 @@
1
1
  require 'spec_helper'
2
2
 
3
- describe Validator::Mapping do
3
+ describe Csv2hash::Validator::Mapping do
4
4
 
5
5
  let(:definition) do
6
- Definition.new([ { position: [0,0], key: 'name' } ], Definition::MAPPING).tap do |definition|
6
+ Csv2hash::Definition.new([ { position: [0,0], key: 'name' } ], Csv2hash::Definition::MAPPING).tap do |definition|
7
7
  definition.validate!
8
8
  definition.default!
9
9
  end
10
10
  end
11
11
 
12
12
  subject do
13
- Csv2hash.new(definition, 'file_path').tap do |csv2hash|
14
- csv2hash.data_source = data_source
15
- end
13
+ Csv2hash.new(definition, 'file_path', true, data_source)
16
14
  end
17
15
 
18
16
  context 'with valid data' do
@@ -26,9 +24,9 @@ describe Validator::Mapping do
26
24
  end
27
25
 
28
26
  context 'wihtout exception' do
29
- let(:data_source) { [ [ ] ]}
30
- before { subject.exception = false }
31
- it { subject.parse.should eql ",\"undefined name on [0, 0]\"\n" }
27
+ let(:data_source) { [ [ ] ] }
28
+ before { subject.exception_mode = false }
29
+ it { subject.parse.errors.to_csv.should eql ",\"undefined name on [0, 0]\"\n" }
32
30
 
33
31
  context 'errors should be filled' do
34
32
  before { subject.parse }
@@ -37,7 +35,7 @@ describe Validator::Mapping do
37
35
 
38
36
  context 'original csv + errors should be returned' do
39
37
  let(:definition) do
40
- Definition.new(rules, Definition::MAPPING).tap do |d|
38
+ Csv2hash::Definition.new(rules, Csv2hash::Definition::MAPPING).tap do |d|
41
39
  d.validate!; d.default!
42
40
  end
43
41
  end
@@ -50,7 +48,8 @@ describe Validator::Mapping do
50
48
  ]
51
49
  end
52
50
  let(:data_source) { [ [ 'what?' ], [ 'yes', 'what?' ], [ 'yes', 'what?', 'no' ] ] }
53
- it { subject.parse.should eql "what?,\"agree not supported, please use one of [\"\"yes\"\", \"\"no\"\"]\"\n" }
51
+ it { subject.parse.errors.to_csv.should eql(
52
+ "what?,\"agree not supported, please use one of [\"\"yes\"\", \"\"no\"\"]\"\n") }
54
53
  end
55
54
  context 'range values' do
56
55
  let(:rules) do
@@ -61,8 +60,37 @@ describe Validator::Mapping do
61
60
  ]
62
61
  end
63
62
  let(:data_source) { [ [ 12 ], [ 2, 12 ], [ 3, 12, 1 ] ] }
64
- it { subject.parse.should eql "12,\"score not supported, please use one of 1..10\"\n" }
63
+ it { subject.parse.errors.to_csv.should eql("12,\"score not supported, please use one of 1..10\"\n") }
64
+ end
65
+ end
66
+
67
+ context 'with extra_validator' do
68
+ let(:definition) do
69
+ Csv2hash::Definition.new([
70
+ { position: [0,0], key: 'name', extra_validator: DowncaseValidator.new,
71
+ message: 'your data should be writting in downcase only'
72
+ }
73
+ ],
74
+ Csv2hash::Definition::MAPPING).tap do |definition|
75
+ definition.validate!; definition.default!
76
+ end
77
+ end
78
+ before { subject.parse }
79
+ context 'not valid data' do
80
+ let(:data_source) { [ [ 'Foo' ] ] }
81
+ its(:errors) { should eql [{x: 0, y: 0, message: 'your data should be writting in downcase only', key: 'name'}]}
82
+ end
83
+ context 'valid data' do
84
+ let(:data_source) { [ [ 'foo' ] ] }
85
+ its(:errors) { should be_empty }
65
86
  end
66
87
  end
67
88
  end
68
- end
89
+ end
90
+
91
+ class DowncaseValidator < Csv2hash::ExtraValidator
92
+ def valid? rule, value
93
+ !!(value.match /^[a-z]+$/)
94
+ end
95
+ end
96
+
@@ -1,22 +1,22 @@
1
1
  require 'spec_helper'
2
2
 
3
- describe Validator do
3
+ describe Csv2hash::Validator do
4
4
 
5
5
  let(:definition) do
6
- Definition.new([ { position: [0,0], key: 'name' } ], Definition::MAPPING).tap do |definition|
6
+ Csv2hash::Definition.new([ { position: [0,0], key: 'name' } ], Csv2hash::Definition::MAPPING).tap do |definition|
7
7
  definition.validate!
8
8
  definition.default!
9
9
  end
10
10
  end
11
11
 
12
12
  subject do
13
- Csv2hash.new(definition, 'file_path').tap do |csv2hash|
14
- csv2hash.data_source = data_source
13
+ Csv2hash.new(definition, 'file_path').tap do |parser|
14
+ parser.instance_variable_set :@data_source, data_source
15
15
  end
16
16
  end
17
17
 
18
18
  describe '#message' do
19
- subject { Csv2hash.new double('definition', type: Definition::COLLECTION), nil }
19
+ subject { Csv2hash.new double('definition', type: Csv2hash::Definition::COLLECTION), nil }
20
20
 
21
21
  context 'string value' do
22
22
  let(:rule) { { foo: 'bar', message: ':foo are value of foo key' } }
@@ -43,4 +43,4 @@ describe Validator do
43
43
  end
44
44
  end
45
45
 
46
- end
46
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: csv2hash
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: '0.1'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joel AZEMAR
@@ -52,7 +52,7 @@ dependencies:
52
52
  - - '>='
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
- description: DSL for CSV Ruby Hash mapping
55
+ description: DSL to map a CSV to a Ruby Hash
56
56
  email: joel.azemar@gmail.com
57
57
  executables:
58
58
  - load_rvm
@@ -70,12 +70,16 @@ files:
70
70
  - LICENSE
71
71
  - README.md
72
72
  - Rakefile
73
+ - ReleaseNotes.md
73
74
  - bin/load_rvm
74
75
  - bin/publish
75
76
  - csv2hash.gemspec
76
77
  - lib/csv2hash.rb
77
78
  - lib/csv2hash/csv_array.rb
79
+ - lib/csv2hash/data_wrapper.rb
78
80
  - lib/csv2hash/definition.rb
81
+ - lib/csv2hash/extra_validator.rb
82
+ - lib/csv2hash/notifier.rb
79
83
  - lib/csv2hash/parser.rb
80
84
  - lib/csv2hash/parser/collection.rb
81
85
  - lib/csv2hash/parser/mapping.rb
@@ -115,7 +119,7 @@ rubyforge_project:
115
119
  rubygems_version: 2.1.11
116
120
  signing_key:
117
121
  specification_version: 4
118
- summary: Mapping CSV to Ruby Hash
122
+ summary: Mapping a CSV to a Ruby Hash
119
123
  test_files:
120
124
  - spec/csv2hash/definition_spec.rb
121
125
  - spec/csv2hash/parser/collection_spec.rb