csv2hash 0.0.2 → 0.1

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