csv-mapper 0.0.4 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,2 @@
1
+ pkg/*
2
+ doc/*
@@ -1,3 +1,12 @@
1
+ == 0.5.0 2010-05-12
2
+ * Parsing performance is now approximately 8x-10x faster when not specifying a custom map_to class.
3
+ * Default class mapping is now Struct instead of OpenStruct
4
+ * Recommended usage is now "CsvMapper.import(...)"
5
+ * Fixes recurring problem with using CsvMapper in rake tasks
6
+ * Keeps everything a little cleaner
7
+ * #map transformations now prefer blocks over lambdas or symbols.
8
+ * Converted from using newgem to jeweler for managing the library itself.
9
+
1
10
  == 0.0.4 2009-08-05
2
11
  * Merged contributions from Jeffrey Chupp - http://semanticart.com
3
12
  * Added support for "Automagical Attribute Discovery"
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ (The MIT License)
2
+
3
+ Copyright (c) 2009 Luke Pillow
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ 'Software'), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -1,4 +1,4 @@
1
- = README
1
+ = csv-mapper
2
2
 
3
3
  == DESCRIPTION:
4
4
 
@@ -6,7 +6,7 @@ CsvMapper is a small library intended to simplify the common steps involved with
6
6
 
7
7
  == EXAMPLES:
8
8
 
9
- The following example will import a CSV file to an Array of OpenStruct[http://ruby-doc.org/core/classes/OpenStruct.html] instances.
9
+ The following example will import a CSV file to an Array of Struct[http://www.ruby-doc.org/core/classes/Struct.html] instances.
10
10
 
11
11
  ==== Example CSV File Structure
12
12
 
@@ -17,9 +17,7 @@ The following example will import a CSV file to an Array of OpenStruct[http://ru
17
17
  ...etc...
18
18
 
19
19
  ==== Simple Usage Example
20
- include CsvMapper
21
-
22
- results = import('/path/to/file.csv') do
20
+ results = CsvMapper.import('/path/to/file.csv') do
23
21
  start_at_row 1
24
22
  [first_name, last_name, age]
25
23
  end
@@ -29,9 +27,7 @@ The following example will import a CSV file to an Array of OpenStruct[http://ru
29
27
  results.first.age # 27
30
28
 
31
29
  ==== Automagical Attribute Discovery Example
32
- include CsvMapper
33
-
34
- results = import('/path/to/file.csv') do
30
+ results = CsvMapper.import('/path/to/file.csv') do
35
31
  read_attributes_from_file
36
32
  end
37
33
 
@@ -45,10 +41,8 @@ Although CsvMapper has no dependency on ActiveRecord; it's easy to import a CSV
45
41
  # Define an ActiveRecord model
46
42
  class Person < ActiveRecord::Base; end
47
43
 
48
- include CsvMapper
49
-
50
- results = import('/path/to/file.csv') do
51
- map_to Person # Map to the Person ActiveRecord class (defined above) instead of the default OpenStruct.
44
+ results = CsvMapper.import('/path/to/file.csv') do
45
+ map_to Person # Map to the Person ActiveRecord class (defined above) instead of the default Struct.
52
46
  after_row lambda{|row, person| person.save } # Call this lambda and save each record after it's parsed.
53
47
 
54
48
  start_at_row 1
@@ -65,27 +59,16 @@ FasterCSV[http://fastercsv.rubyforge.org/] on pre 1.9 versions of Ruby
65
59
 
66
60
  * sudo gem install csv-mapper
67
61
 
68
- == LICENSE:
69
-
70
- (The MIT License)
71
-
72
- Copyright (c) 2008 Luke Pillow
73
-
74
- Permission is hereby granted, free of charge, to any person obtaining
75
- a copy of this software and associated documentation files (the
76
- 'Software'), to deal in the Software without restriction, including
77
- without limitation the rights to use, copy, modify, merge, publish,
78
- distribute, sublicense, and/or sell copies of the Software, and to
79
- permit persons to whom the Software is furnished to do so, subject to
80
- the following conditions:
62
+ == Note on Patches/Pull Requests
63
+
64
+ * Fork the project.
65
+ * Make your feature addition or bug fix.
66
+ * Add tests for it. This is important so I don't break it in a
67
+ future version unintentionally.
68
+ * Commit, do not mess with rakefile, version, or history.
69
+ (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
70
+ * Send me a pull request. Bonus points for topic branches.
81
71
 
82
- The above copyright notice and this permission notice shall be
83
- included in all copies or substantial portions of the Software.
72
+ == Copyright
84
73
 
85
- THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
86
- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
87
- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
88
- IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
89
- CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
90
- TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
91
- SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
74
+ Copyright (c) 2009 Luke Pillow. See LICENSE for details.
data/Rakefile CHANGED
@@ -1,28 +1,47 @@
1
- %w[rubygems rake rake/clean fileutils newgem rubigen].each { |f| require f }
2
- require File.dirname(__FILE__) + '/lib/csv-mapper'
3
-
4
- # Generate all the Rake tasks
5
- # Run 'rake -T' to see list of generated tasks (from gem root directory)
6
- $hoe = Hoe.new('csv-mapper', CsvMapper::VERSION) do |p|
7
- p.developer('Luke Pillow', 'lpillow@gmail.com')
8
- p.changes = p.paragraphs_of("History.txt", 0..1).join("\n\n")
9
- # p.post_install_message = 'PostInstall.txt' # TODO remove if post-install message not required
10
- p.rubyforge_name = p.name # TODO this is default value
11
- p.extra_deps = [
12
- ['fastercsv','>= 1.4.0'],
13
- ]
14
- p.extra_dev_deps = [
15
- ['newgem', ">= #{::Newgem::VERSION}"]
16
- ]
17
-
18
- p.clean_globs |= %w[**/.DS_Store tmp *.log]
19
- path = (p.rubyforge_name == p.name) ? p.rubyforge_name : "\#{p.rubyforge_name}/\#{p.name}"
20
- p.remote_rdoc_dir = File.join(path.gsub(/^#{p.rubyforge_name}\/?/,''), 'rdoc')
21
- p.rsync_args = '-av --delete --ignore-errors'
22
- end
23
-
24
- require 'newgem/tasks' # load /tasks/*.rake
25
- Dir['tasks/**/*.rake'].each { |t| load t }
26
-
27
- # TODO - want other tests/tasks run by default? Add them to the list
28
- # task :default => [:spec, :features]
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "csv-mapper"
8
+ gem.summary = %Q{CsvMapper is a small library intended to simplify the common steps involved with importing CSV files to a usable form in Ruby.}
9
+ gem.description = %Q{CSV Mapper makes it easy to import data from CSV files directly to a collection of any type of Ruby object. The simplest way to create mappings is declare the names of the attributes in the order corresponding to the CSV file column order.}
10
+ gem.email = "lpillow@gmail.com"
11
+ gem.homepage = "http://github.com/pillowfactory/csv-mapper"
12
+ gem.authors = ["Luke Pillow"]
13
+ gem.add_development_dependency "rspec", ">= 1.2.9"
14
+ gem.add_dependency "fastercsv"
15
+ gem.extra_rdoc_files << "History.txt"
16
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
17
+ end
18
+ Jeweler::GemcutterTasks.new
19
+ rescue LoadError
20
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
21
+ end
22
+
23
+ require 'spec/rake/spectask'
24
+ Spec::Rake::SpecTask.new(:spec) do |spec|
25
+ spec.libs << 'lib' << 'spec'
26
+ spec.spec_files = FileList['spec/**/*_spec.rb']
27
+ end
28
+
29
+ Spec::Rake::SpecTask.new(:rcov) do |spec|
30
+ spec.libs << 'lib' << 'spec'
31
+ spec.pattern = 'spec/**/*_spec.rb'
32
+ spec.rcov = true
33
+ end
34
+
35
+ task :spec => :check_dependencies
36
+
37
+ task :default => :spec
38
+
39
+ require 'rake/rdoctask'
40
+ Rake::RDocTask.new do |rdoc|
41
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
42
+
43
+ rdoc.rdoc_dir = 'rdoc'
44
+ rdoc.title = "csv-mapper #{version}"
45
+ rdoc.rdoc_files.include('README*')
46
+ rdoc.rdoc_files.include('lib/**/*.rb')
47
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.5.0
@@ -1,7 +1,7 @@
1
- $:.unshift(File.dirname(__FILE__)) unless
2
- $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
1
+ dir = File.dirname(__FILE__)
2
+ $LOAD_PATH.unshift dir unless $LOAD_PATH.include?(dir)
3
3
 
4
- require 'ostruct'
4
+ require 'rubygems'
5
5
 
6
6
  # the following is slightly modified from Gregory Brown's
7
7
  # solution on the Ruport Blaag:
@@ -44,7 +44,7 @@ end
44
44
  # know the corresponding CSV column index to associate with the attribute.
45
45
  #
46
46
  # ===== The Basics
47
- # * +map_to+ - Override the default OpenStruct target. Accepts a class and an optional hash of default attribute names and values.
47
+ # * +map_to+ - Override the default Struct target. Accepts a class and an optional hash of default attribute names and values.
48
48
  # * +start_at_row+ - Specify what row to begin parsing at. Use this to skip headers.
49
49
  # * +before_row+ - Accepts an Array of method name symbols or lambdas to be invoked before parsing each row.
50
50
  # * +after_row+ - Accepts an Array of method name symbols or lambdas to be invoked after parsing each row.
@@ -81,7 +81,6 @@ end
81
81
  # other_results = import('/path/to/file.csv', :map => a_row_map)
82
82
  #
83
83
  module CsvMapper
84
- VERSION = '0.0.4'
85
84
 
86
85
  # Create a new RowMap instance from the definition in the given block.
87
86
  def map_csv(&map_block)
@@ -111,234 +110,13 @@ module CsvMapper
111
110
  results
112
111
  end
113
112
 
114
- # CsvMapper::RowMap provides a simple, DSL-like interface for constructing mappings.
115
- # A CsvMapper::RowMap provides the main functionality of the library. It will mostly be used indirectly through the CsvMapper API,
116
- # but may be useful to use directly for the dynamic CSV mappings.
117
- class RowMap
118
- #Start with a 'blank slate'
119
- instance_methods.each { |m| undef_method m unless m =~ /^__||instance_eval/ }
120
-
121
- Infinity = 1.0/0
122
- attr_reader :mapped_attributes
123
-
124
- # Create a new instance with access to an evaluation context
125
- def initialize(context, csv_data = nil, &map_block)
126
- @context = context
127
- @csv_data = csv_data
128
- @before_filters = []
129
- @after_filters = []
130
- @parser_options = {}
131
- @start_at_row = 0
132
- @stop_at_row = Infinity
133
- @delimited_by = FasterCSV::DEFAULT_OPTIONS[:col_sep]
134
- @mapped_attributes = []
135
-
136
- self.instance_eval(&map_block) if block_given?
137
- end
138
-
139
- # Each row of a CSV is parsed and mapped to a new instance of a Ruby class; OpenStruct by default.
140
- # Use this method to change the what class each row is mapped to.
141
- # The given class must respond to a parameter-less #new and all attribute mappings defined.
142
- # Providing a hash of defaults will ensure that each resulting object will have the providing name and attribute values
143
- # unless overridden by a mapping
144
- def map_to(klass, defaults={})
145
- @map_to_klass = klass
146
-
147
- defaults.each do |name, value|
148
- self.add_attribute(name, -99).map lambda{|row| value}
149
- end
150
- end
151
-
152
- # Allow us to read the first line of a csv file to automatically generate the attribute names.
153
- # Spaces are replaced with underscores and non-word characters are removed.
154
- #
155
- # Keep in mind that there is potential for overlap in using this (i.e. you have a field named
156
- # files+ and one named files- and they both get named 'files').
157
- #
158
- # You can specify aliases to rename fields to prevent conflicts and/or improve readability and compatibility.
159
- #
160
- # i.e. read_attributes_from_file('files+' => 'files_plus', 'files-' => 'files_minus)
161
- def read_attributes_from_file aliases = {}
162
- attributes = FasterCSV.new(@csv_data, @parser_options).readline
163
- @start_at_row = [ @start_at_row, 1 ].max
164
- @csv_data.rewind
165
- attributes.each_with_index do |name, index|
166
- name.strip!
167
- use_name = aliases[name] || name.gsub(/\s+/, '_').gsub(/[\W]+/, '').downcase
168
- add_attribute use_name, index
169
- end
170
- end
171
-
172
- # Specify a hash of FasterCSV options to be used for CSV parsing
173
- #
174
- # Can be anything FasterCSV::new()[http://fastercsv.rubyforge.org/classes/FasterCSV.html#M000018] accepts
175
- def parser_options(opts=nil)
176
- @parser_options = opts if opts
177
- @parser_options.merge :col_sep => @delimited_by
178
- end
179
-
180
- # Convenience method to 'move' the cursor skipping the current index.
181
- def _SKIP_
182
- self.move_cursor
183
- end
184
-
185
- # Specify the CSV column delimiter. Defaults to comma.
186
- def delimited_by(delimiter=nil)
187
- @delimited_by = delimiter if delimiter
188
- @delimited_by
189
- end
190
-
191
- # Declare what row to begin parsing the CSV.
192
- # This is useful for skipping headers and such.
193
- def start_at_row(row_number=nil)
194
- @start_at_row = row_number if row_number
195
- @start_at_row
196
- end
197
-
198
- # Declare the last row to be parsed in a CSV.
199
- def stop_at_row(row_number=nil)
200
- @stop_at_row = row_number if row_number
201
- @stop_at_row
202
- end
203
-
204
- # Declare method name symbols and/or lambdas to be executed before each row.
205
- # Each method or lambda must accept to parameters: +csv_row+, +target_object+
206
- # Methods names should refer to methods available within the RowMap's provided context
207
- def before_row(*befores)
208
- self.add_filters(@before_filters, *befores)
209
- end
210
-
211
- # Declare method name symbols and/or lambdas to be executed before each row.
212
- # Each method or lambda must accept to parameters: +csv_row+, +target_object+
213
- # Methods names should refer to methods available within the RowMap's provided context
214
- def after_row(*afters)
215
- self.add_filters(@after_filters, *afters)
216
- end
217
-
218
- # Add a new attribute to this map. Mostly used internally, but is useful for dynamic map creation.
219
- # returns the newly created CsvMapper::AttributeMap
220
- def add_attribute(name, index=nil)
221
- attr_mapping = CsvMapper::AttributeMap.new(name.to_sym, index, @context)
222
- self.mapped_attributes << attr_mapping
223
- attr_mapping
224
- end
225
-
226
- # The current cursor location
227
- def cursor # :nodoc:
228
- @cursor ||= 0
229
- end
230
-
231
- # Move the cursor relative to it's current position
232
- def move_cursor(positions=1) # :nodoc:
233
- self.cursor += positions
234
- end
235
-
236
- # Given a CSV row return an instance of an object defined by this mapping
237
- def parse(csv_row)
238
- target = self.map_to_class.new
239
- @before_filters.each {|filter| filter.call(csv_row, target) }
240
-
241
- self.mapped_attributes.inject(target) do |result, attr_map|
242
- result.send("#{attr_map.name}=".to_sym, attr_map.parse(csv_row))
243
- result
244
- end
245
-
246
- @after_filters.each {|filter| filter.call(csv_row, target) }
247
-
248
- return target
249
- end
250
-
251
- protected # :nodoc:
252
-
253
- # The Hacktastic "magic"
254
- # Used to dynamically create CsvMapper::AttributeMaps based on unknown method calls that
255
- # should represent the names of mapped attributes.
256
- #
257
- # An optional first argument is used to move this maps cursor position and as the index of the
258
- # new AttributeMap
259
- def method_missing(name, *args) # :nodoc:
260
-
261
- if index = args[0]
262
- self.move_cursor(index - self.cursor)
263
- else
264
- index = self.cursor
265
- self.move_cursor
266
- end
267
-
268
- add_attribute(name, index)
269
- end
270
-
271
- def add_filters(to_hook, *filters) # :nodoc:
272
- (to_hook << filters.collect do |filter|
273
- filter.is_a?(Symbol) ? lambda{|row, target| @context.send(filter, row, target)} : filter
274
- end).flatten!
275
- end
276
-
277
- def map_to_class # :nodoc:
278
- @map_to_klass || OpenStruct
279
- end
280
-
281
- def cursor=(value) # :nodoc:
282
- @cursor=value
283
- end
284
-
285
-
286
- end
287
-
288
- # A CsvMapper::AttributeMap contains the instructions to parse a value from a CSV row and to know the
289
- # name of the attribute it is targeting.
290
- class AttributeMap
291
- attr_reader :name, :index
292
-
293
- # Creates a new instance using the provided attribute +name+, CSV row +index+, and evaluation +map_context+
294
- def initialize(name, index, map_context)
295
- @name, @index, @map_context = name, index, map_context
296
- end
297
-
298
- # Set the index that this map is targeting.
299
- #
300
- # Returns this AttributeMap for chainability
301
- def at(index)
302
- @index = index
303
- self
304
- end
305
-
306
- # Provide a lambda or the symbol name of a method on this map's evaluation context to be used when parsing
307
- # the value from a CSV row.
308
- # Both the lambda or the method provided should accept a single +row+ parameter
309
- #
310
- # Returns this AttributeMap for chainability
311
- def map(transform)
312
- @transformer = transform
313
- self
314
- end
315
-
316
- # Given a CSV row, return the value at this AttributeMap's index using any provided map transforms (see map)
317
- def parse(csv_row)
318
- @transformer ? parse_transform(csv_row) : csv_row[self.index]
319
- end
320
-
321
- # Access the raw value of the CSV row without any map transforms applied.
322
- def raw_value(csv_row)
323
- csv_row[self.index]
324
- end
325
-
326
- private
327
-
328
- def parse_transform(csv_row)
329
- if @transformer.is_a? Symbol
330
- transform_name = @transformer
331
- @transformer = lambda{|row| @map_context.send(transform_name, row) }
332
- end
333
-
334
- @transformer.call(csv_row)
335
- end
336
-
337
- end
338
-
339
113
  protected
340
114
  # Create a new RowMap instance from the definition in the given block and pass the csv_data.
341
115
  def map_csv_with_data(csv_data, &map_block) # :nodoc:
342
116
  CsvMapper::RowMap.new(self, csv_data, &map_block)
343
117
  end
344
- end
118
+
119
+ extend self
120
+ end
121
+
122
+ require 'csv-mapper/row_map'