csv-mapper 0.0.4 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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'