artforge-csv-mapper 1.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.
- data/History.txt +25 -0
- data/LICENSE +22 -0
- data/README.rdoc +90 -0
- data/Rakefile +47 -0
- data/VERSION +1 -0
- data/lib/csv-mapper.rb +131 -0
- data/lib/csv-mapper/attribute_map.rb +59 -0
- data/lib/csv-mapper/row_map.rb +224 -0
- data/spec/csv-mapper/attribute_map_spec.rb +65 -0
- data/spec/csv-mapper/row_map_spec.rb +139 -0
- data/spec/csv-mapper_spec.rb +238 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +3 -0
- data/spec/test.csv +4 -0
- data/spec/test_with_empty_column_names.csv +4 -0
- data/spec/test_with_pushed_down_header.csv +8 -0
- metadata +102 -0
data/History.txt
ADDED
@@ -0,0 +1,25 @@
|
|
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
|
+
|
10
|
+
== 0.0.4 2009-08-05
|
11
|
+
* Merged contributions from Jeffrey Chupp - http://semanticart.com
|
12
|
+
* Added support for "Automagical Attribute Discovery"
|
13
|
+
* Added Ruby 1.9 compatibility
|
14
|
+
|
15
|
+
== 0.0.3 2008-12-22
|
16
|
+
* Fixed specs to work with RSpec 1.1.9 and later where Modules aren't auto included
|
17
|
+
|
18
|
+
== 0.0.2 2008-12-15
|
19
|
+
|
20
|
+
* Added #stop_at_row method to RowMap
|
21
|
+
|
22
|
+
== 0.0.1 2008-12-05
|
23
|
+
|
24
|
+
* 1 major enhancement:
|
25
|
+
* Initial release
|
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.
|
data/README.rdoc
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
= csv-mapper
|
2
|
+
|
3
|
+
== DESCRIPTION:
|
4
|
+
|
5
|
+
CsvMapper is a small library intended to simplify the common steps involved with importing CSV files to a usable form in Ruby. CsvMapper is compatible with recent 1.8 versions of Ruby as well as Ruby 1.9+
|
6
|
+
|
7
|
+
== EXAMPLES:
|
8
|
+
|
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
|
+
|
11
|
+
==== Example CSV File Structure
|
12
|
+
|
13
|
+
First Name,Last Name,Age
|
14
|
+
John,Doe,27
|
15
|
+
Jane,Doe,26
|
16
|
+
Bat,Man,52
|
17
|
+
...etc...
|
18
|
+
|
19
|
+
==== Simple Usage Example
|
20
|
+
results = CsvMapper.import('/path/to/file.csv') do
|
21
|
+
start_at_row 1
|
22
|
+
[first_name, last_name, age]
|
23
|
+
end
|
24
|
+
|
25
|
+
results.first.first_name # John
|
26
|
+
results.first.last_name # Doe
|
27
|
+
results.first.age # 27
|
28
|
+
|
29
|
+
==== Automagical Attribute Discovery Example
|
30
|
+
results = CsvMapper.import('/path/to/file.csv') do
|
31
|
+
read_attributes_from_file
|
32
|
+
end
|
33
|
+
|
34
|
+
results.first.first_name # John
|
35
|
+
results.first.last_name # Doe
|
36
|
+
results.first.age # 27
|
37
|
+
|
38
|
+
==== Named Columns Example
|
39
|
+
Columns which aren't mentioned won't appear in the results.
|
40
|
+
|
41
|
+
# Don't mention first name
|
42
|
+
results = CsvMapper.import('/path/to/file_with_header_row.csv') do
|
43
|
+
named_columns
|
44
|
+
|
45
|
+
surname('last_name')
|
46
|
+
age
|
47
|
+
end
|
48
|
+
|
49
|
+
results.first.surname # Doe
|
50
|
+
results.first.age # 27
|
51
|
+
results.first.first_name # nil
|
52
|
+
results.first.last_name # nil
|
53
|
+
|
54
|
+
==== Import to ActiveRecord Example
|
55
|
+
Although CsvMapper has no dependency on ActiveRecord; it's easy to import a CSV file to ActiveRecord models and save them.
|
56
|
+
|
57
|
+
# Define an ActiveRecord model
|
58
|
+
class Person < ActiveRecord::Base; end
|
59
|
+
|
60
|
+
results = CsvMapper.import('/path/to/file.csv') do
|
61
|
+
map_to Person # Map to the Person ActiveRecord class (defined above) instead of the default Struct.
|
62
|
+
after_row lambda{|row, person| person.save } # Call this lambda and save each record after it's parsed.
|
63
|
+
|
64
|
+
start_at_row 1
|
65
|
+
[first_name, last_name, age]
|
66
|
+
end
|
67
|
+
|
68
|
+
See CsvMapper for a more detailed description
|
69
|
+
|
70
|
+
== REQUIREMENTS:
|
71
|
+
|
72
|
+
FasterCSV[http://fastercsv.rubyforge.org/] on pre 1.9 versions of Ruby
|
73
|
+
|
74
|
+
== INSTALL:
|
75
|
+
|
76
|
+
* sudo gem install csv-mapper
|
77
|
+
|
78
|
+
== Note on Patches/Pull Requests
|
79
|
+
|
80
|
+
* Fork the project.
|
81
|
+
* Make your feature addition or bug fix.
|
82
|
+
* Add tests for it. This is important so I don't break it in a
|
83
|
+
future version unintentionally.
|
84
|
+
* Commit, do not mess with rakefile, version, or history.
|
85
|
+
(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)
|
86
|
+
* Send me a pull request. Bonus points for topic branches.
|
87
|
+
|
88
|
+
== Copyright
|
89
|
+
|
90
|
+
Copyright (c) 2009 Luke Pillow. See LICENSE for details.
|
data/Rakefile
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "artforge-csv-mapper"
|
8
|
+
gem.summary = %Q{artforge-CsvMapper is a fork of a small library intended to simplify the common steps involved with importing CSV files to a usable form in Ruby. It has support for null column names. When this is merged, this gem will be removed.}
|
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 = "adam@artforge.com"
|
11
|
+
gem.homepage = "http://github.com/Artforge/csv-mapper"
|
12
|
+
gem.authors = ["Luke Pillow", "Russell Garner", "Adam Singer"]
|
13
|
+
gem.add_development_dependency "rspec", ">= 2.0.0"
|
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 'rspec/core/rake_task'
|
24
|
+
RSpec::Core::RakeTask.new(:spec) do |spec|
|
25
|
+
spec.libs << 'lib' << 'spec'
|
26
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
27
|
+
end
|
28
|
+
|
29
|
+
RSpec::Core::RakeTask.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 'rdoc/task'
|
40
|
+
RDoc::Task.new do |rdoc|
|
41
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
42
|
+
|
43
|
+
rdoc.rdoc_dir = 'rdoc'
|
44
|
+
rdoc.title = "artforge-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
|
+
1.0.1
|
data/lib/csv-mapper.rb
ADDED
@@ -0,0 +1,131 @@
|
|
1
|
+
dir = File.dirname(__FILE__)
|
2
|
+
$LOAD_PATH.unshift dir unless $LOAD_PATH.include?(dir)
|
3
|
+
|
4
|
+
require 'rubygems'
|
5
|
+
|
6
|
+
# the following is slightly modified from Gregory Brown's
|
7
|
+
# solution on the Ruport Blaag:
|
8
|
+
# http://ruport.blogspot.com/2008/03/fastercsv-api-shim-for-19.html
|
9
|
+
|
10
|
+
if RUBY_VERSION > "1.9"
|
11
|
+
require "csv"
|
12
|
+
unless defined? FCSV
|
13
|
+
class Object
|
14
|
+
FasterCSV = CSV
|
15
|
+
alias_method :FasterCSV, :CSV
|
16
|
+
end
|
17
|
+
end
|
18
|
+
else
|
19
|
+
require "fastercsv"
|
20
|
+
end
|
21
|
+
|
22
|
+
# This module provides the main interface for importing CSV files & data to mapped Ruby objects.
|
23
|
+
# = Usage
|
24
|
+
# Including CsvMapper will provide two methods:
|
25
|
+
# - +import+
|
26
|
+
# - +map_csv+
|
27
|
+
#
|
28
|
+
# See csv-mapper.rb[link:files/lib/csv-mapper_rb.html] for method docs.
|
29
|
+
#
|
30
|
+
# === Import From File
|
31
|
+
# results = import('/path/to/file.csv') do
|
32
|
+
# # declare mapping here
|
33
|
+
# end
|
34
|
+
#
|
35
|
+
# === Import From String or IO
|
36
|
+
# results = import(csv_data, :type => :io) do
|
37
|
+
# # declare mapping here
|
38
|
+
# end
|
39
|
+
#
|
40
|
+
# === Mapping
|
41
|
+
# Mappings are built inside blocks. All three of CsvMapper's main API methods accept a block containing a mapping.
|
42
|
+
# Maps are defined by using +map_to+, +start_at_row+, +before_row+, and +after_row+ (methods on CsvMapper::RowMap) and
|
43
|
+
# by defining your own mapping attributes.
|
44
|
+
# A mapping block uses an internal cursor to keep track of the order the mapping attributes are declared and use that order to
|
45
|
+
# know the corresponding CSV column index to associate with the attribute.
|
46
|
+
#
|
47
|
+
# ===== The Basics
|
48
|
+
# * +map_to+ - Override the default Struct target. Accepts a class and an optional hash of default attribute names and values.
|
49
|
+
# * +named_columns+ - Enables named columns mode. Headers are required. See "Named Column Mappings" below.
|
50
|
+
# * +start_at_row+ - Specify what row to begin parsing at. Use this to skip headers. When +named_columns+ is used, headers are assumed to be one line above this row.
|
51
|
+
# * +before_row+ - Accepts an Array of method name symbols or lambdas to be invoked before parsing each row.
|
52
|
+
# * +after_row+ - Accepts an Array of method name symbols or lambdas to be invoked after parsing each row.
|
53
|
+
# * +delimited_by+ - Accepts a character to be used to delimit columns. Use this to specify pipe-delimited files.
|
54
|
+
# * <tt>\_SKIP_</tt> - Use as a placehold to skip a CSV column index.
|
55
|
+
# * +parser_options+ - Accepts a hash of FasterCSV options. Can be anything FasterCSV::new()[http://fastercsv.rubyforge.org/classes/FasterCSV.html#M000018] understands
|
56
|
+
#
|
57
|
+
# ===== Attribute Mappings
|
58
|
+
# Attribute mappings are created by using the name of the attribute to be mapped to.
|
59
|
+
# The order in which attribute mappings are declared determines the index of the corresponding CSV row.
|
60
|
+
# All mappings begin at the 0th index of the CSV row.
|
61
|
+
# foo # maps the 0th CSV row position value to the value of the 'foo' attribute on the target object.
|
62
|
+
# bar # maps the 1st row position to 'bar'
|
63
|
+
# This could also be a nice one liner for easy CSV format conversion
|
64
|
+
# [foo, bar] # creates the same attribute maps as above.
|
65
|
+
# The mapping index may be specifically declared in two additional ways:
|
66
|
+
# foo(2) # maps the 2nd CSV row position value to 'foo' and moves the cursor to 3
|
67
|
+
# bar # maps the 3rd CSV row position to 'bar' due to the current cursor position
|
68
|
+
# baz.at(0) # maps the 0th CSV row position to 'baz' but only increments the cursor 1 position to 4
|
69
|
+
# Each attribute mapping may be configured to parse the record using a lambda or a method name
|
70
|
+
# foo.map lambda{|row| row[2].strip } # maps the 2nd row position value with leading and trailing whitespace removed to 'foo'.
|
71
|
+
# bar.map :clean_bar # maps the result of the clean_bar method to 'bar'. clean_bar must accept the row as a parameter.
|
72
|
+
# Attribute mapping declarations and "modifiers" may be chained
|
73
|
+
# foo.at(4).map :some_transform
|
74
|
+
#
|
75
|
+
# === Named Columns Mappings
|
76
|
+
# When +named_columns+ is called, column names will be read from one row above +start_at_row+. This allows
|
77
|
+
# you to map cell properties to named columns in the CSV. For example:
|
78
|
+
# surname('Last Name') # Where 'Last Name' is the name of a column in the CSV
|
79
|
+
#
|
80
|
+
# Columns which go unmentioned will be omitted from the results.
|
81
|
+
#
|
82
|
+
# === Create Reusable Mappings
|
83
|
+
# The +import+ method accepts an instance of RowMap as an optional mapping parameter.
|
84
|
+
# The easiest way to create an instance of a RowMap is by using +map_csv+.
|
85
|
+
# a_row_map = map_csv do
|
86
|
+
# # declare mapping here
|
87
|
+
# end
|
88
|
+
# Then you can reuse the mapping
|
89
|
+
# results = import(some_string, :type => :io, :map => a_row_map)
|
90
|
+
# other_results = import('/path/to/file.csv', :map => a_row_map)
|
91
|
+
#
|
92
|
+
module CsvMapper
|
93
|
+
|
94
|
+
# Create a new RowMap instance from the definition in the given block.
|
95
|
+
def map_csv(&map_block)
|
96
|
+
CsvMapper::RowMap.new(self, &map_block)
|
97
|
+
end
|
98
|
+
|
99
|
+
# Load CSV data and map the values according to the definition in the given block.
|
100
|
+
# Accepts either a file path, String, or IO as +data+. Defaults to file path.
|
101
|
+
#
|
102
|
+
# The following +options+ may be used:
|
103
|
+
# <tt>:type</tt>:: defaults to <tt>:file_path</tt>. Use <tt>:io</tt> to specify data as String or IO.
|
104
|
+
# <tt>:map</tt>:: Specify an instance of a RowMap to take presidence over a given block defintion.
|
105
|
+
#
|
106
|
+
def import(data, options={}, &map_block)
|
107
|
+
csv_data = options[:type] == :io ? data : File.new(data, 'r')
|
108
|
+
|
109
|
+
config = { :type => :file_path,
|
110
|
+
:map => map_csv_with_data(csv_data, &map_block) }.merge!(options)
|
111
|
+
|
112
|
+
map = config[:map]
|
113
|
+
|
114
|
+
results = []
|
115
|
+
FasterCSV.new(csv_data, map.parser_options ).each_with_index do |row, i|
|
116
|
+
results << map.parse(row) if i >= map.start_at_row && i <= map.stop_at_row
|
117
|
+
end
|
118
|
+
|
119
|
+
results
|
120
|
+
end
|
121
|
+
|
122
|
+
protected
|
123
|
+
# Create a new RowMap instance from the definition in the given block and pass the csv_data.
|
124
|
+
def map_csv_with_data(csv_data, &map_block) # :nodoc:
|
125
|
+
CsvMapper::RowMap.new(self, csv_data, &map_block)
|
126
|
+
end
|
127
|
+
|
128
|
+
extend self
|
129
|
+
end
|
130
|
+
|
131
|
+
require 'csv-mapper/row_map'
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module CsvMapper
|
2
|
+
# A CsvMapper::AttributeMap contains the instructions to parse a value from a CSV row and to know the
|
3
|
+
# name of the attribute it is targeting.
|
4
|
+
class AttributeMap
|
5
|
+
attr_reader :name, :index
|
6
|
+
|
7
|
+
# Creates a new instance using the provided attribute +name+, CSV row +index+, and evaluation +map_context+
|
8
|
+
def initialize(name, index, map_context)
|
9
|
+
@name, @index, @map_context = name, index, map_context
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_s
|
13
|
+
"#{@index}: #{@name}"
|
14
|
+
end
|
15
|
+
|
16
|
+
# Set the index that this map is targeting.
|
17
|
+
#
|
18
|
+
# Returns this AttributeMap for chainability
|
19
|
+
def at(index)
|
20
|
+
@index = index
|
21
|
+
self
|
22
|
+
end
|
23
|
+
|
24
|
+
# Provide a lambda or the symbol name of a method on this map's evaluation context to be used when parsing
|
25
|
+
# the value from a CSV row.
|
26
|
+
# Both the lambda or the method provided should accept a single +row+ parameter
|
27
|
+
#
|
28
|
+
# Returns this AttributeMap for chainability
|
29
|
+
def map(transform=nil, &block_transform)
|
30
|
+
@transformer = block_transform || transform
|
31
|
+
self
|
32
|
+
end
|
33
|
+
|
34
|
+
# Given a CSV row, return the value at this AttributeMap's index using any provided map transforms (see map)
|
35
|
+
def parse(csv_row)
|
36
|
+
@transformer ? parse_transform(csv_row) : raw_value(csv_row)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Access the raw value of the CSV row without any map transforms applied.
|
40
|
+
def raw_value(csv_row)
|
41
|
+
csv_row[self.index]
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def parse_transform(csv_row)
|
47
|
+
if @transformer.is_a? Symbol
|
48
|
+
transform_name = @transformer
|
49
|
+
@transformer = lambda{|row, index| @map_context.send(transform_name, row, index) }
|
50
|
+
end
|
51
|
+
|
52
|
+
if @transformer.arity == 1
|
53
|
+
@transformer.call(csv_row)
|
54
|
+
else
|
55
|
+
@transformer.call(csv_row, @index)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,224 @@
|
|
1
|
+
require 'csv-mapper/attribute_map'
|
2
|
+
|
3
|
+
module CsvMapper
|
4
|
+
# CsvMapper::RowMap provides a simple, DSL-like interface for constructing mappings.
|
5
|
+
# A CsvMapper::RowMap provides the main functionality of the library. It will mostly be used indirectly through the CsvMapper API,
|
6
|
+
# but may be useful to use directly for the dynamic CSV mappings.
|
7
|
+
class RowMap
|
8
|
+
#Start with a 'blank slate'
|
9
|
+
instance_methods.each { |m| undef_method m unless m =~ /^__||instance_eval/ }
|
10
|
+
|
11
|
+
Infinity = 1.0/0
|
12
|
+
attr_reader :context
|
13
|
+
attr_reader :mapped_attributes
|
14
|
+
|
15
|
+
# Create a new instance with access to an evaluation context
|
16
|
+
def initialize(context, csv_data = nil, &map_block)
|
17
|
+
@context = context
|
18
|
+
@csv_data = csv_data
|
19
|
+
@before_filters = []
|
20
|
+
@after_filters = []
|
21
|
+
@named_columns = false
|
22
|
+
@parser_options = {}
|
23
|
+
@start_at_row = 0
|
24
|
+
@stop_at_row = Infinity
|
25
|
+
@delimited_by = FasterCSV::DEFAULT_OPTIONS[:col_sep]
|
26
|
+
@mapped_attributes = []
|
27
|
+
|
28
|
+
self.instance_eval(&map_block) if block_given?
|
29
|
+
end
|
30
|
+
|
31
|
+
# Each row of a CSV is parsed and mapped to a new instance of a Ruby class; Struct by default.
|
32
|
+
# Use this method to change the what class each row is mapped to.
|
33
|
+
# The given class must respond to a parameter-less #new and all attribute mappings defined.
|
34
|
+
# Providing a hash of defaults will ensure that each resulting object will have the providing name and attribute values
|
35
|
+
# unless overridden by a mapping
|
36
|
+
def map_to(klass, defaults={})
|
37
|
+
@map_to_klass = klass
|
38
|
+
|
39
|
+
defaults.each do |name, value|
|
40
|
+
self.add_attribute(name, -99).map lambda{|row, index| value}
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Allow us to read the first line of a csv file to automatically generate the attribute names.
|
45
|
+
# Spaces are replaced with underscores and non-word characters are removed.
|
46
|
+
#
|
47
|
+
# Keep in mind that there is potential for overlap in using this (i.e. you have a field named
|
48
|
+
# files+ and one named files- and they both get named 'files').
|
49
|
+
#
|
50
|
+
# You can specify aliases to rename fields to prevent conflicts and/or improve readability and compatibility.
|
51
|
+
#
|
52
|
+
# i.e. read_attributes_from_file('files+' => 'files_plus', 'files-' => 'files_minus)
|
53
|
+
def read_attributes_from_file aliases = {}
|
54
|
+
unnamed_number = 1
|
55
|
+
iterate_headers do |name, index|
|
56
|
+
if name.nil?
|
57
|
+
name = "_field_#{unnamed_number}"
|
58
|
+
unnamed_number += 1
|
59
|
+
end
|
60
|
+
name.strip!
|
61
|
+
use_name = aliases[name] || attributize_field_name(name)
|
62
|
+
add_attribute use_name, index
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Specify a hash of FasterCSV options to be used for CSV parsing
|
67
|
+
#
|
68
|
+
# Can be anything FasterCSV::new()[http://fastercsv.rubyforge.org/classes/FasterCSV.html#M000018] accepts
|
69
|
+
def parser_options(opts=nil)
|
70
|
+
@parser_options = opts if opts
|
71
|
+
@parser_options.merge :col_sep => @delimited_by
|
72
|
+
end
|
73
|
+
|
74
|
+
# Default csv_mapper behaviour is to use the ordinal position of a mapped attribute.
|
75
|
+
# If you prefer to look for a column with the name of the attribute, use this method.
|
76
|
+
def named_columns
|
77
|
+
@named_columns = true
|
78
|
+
end
|
79
|
+
|
80
|
+
# Convenience method to 'move' the cursor skipping the current index.
|
81
|
+
def _SKIP_
|
82
|
+
self.move_cursor
|
83
|
+
end
|
84
|
+
|
85
|
+
# Specify the CSV column delimiter. Defaults to comma.
|
86
|
+
def delimited_by(delimiter=nil)
|
87
|
+
@delimited_by = delimiter if delimiter
|
88
|
+
@delimited_by
|
89
|
+
end
|
90
|
+
|
91
|
+
# Declare what row to begin parsing the CSV.
|
92
|
+
# This is useful for skipping headers and such.
|
93
|
+
def start_at_row(row_number=nil)
|
94
|
+
@start_at_row = row_number if row_number
|
95
|
+
@start_at_row
|
96
|
+
end
|
97
|
+
|
98
|
+
# Declare the last row to be parsed in a CSV.
|
99
|
+
def stop_at_row(row_number=nil)
|
100
|
+
@stop_at_row = row_number if row_number
|
101
|
+
@stop_at_row
|
102
|
+
end
|
103
|
+
|
104
|
+
# Declare method name symbols and/or lambdas to be executed before each row.
|
105
|
+
# Each method or lambda must accept to parameters: +csv_row+, +target_object+
|
106
|
+
# Methods names should refer to methods available within the RowMap's provided context
|
107
|
+
def before_row(*befores)
|
108
|
+
self.add_filters(@before_filters, *befores)
|
109
|
+
end
|
110
|
+
|
111
|
+
# Declare method name symbols and/or lambdas to be executed before each row.
|
112
|
+
# Each method or lambda must accept to parameters: +csv_row+, +target_object+
|
113
|
+
# Methods names should refer to methods available within the RowMap's provided context
|
114
|
+
def after_row(*afters)
|
115
|
+
self.add_filters(@after_filters, *afters)
|
116
|
+
end
|
117
|
+
|
118
|
+
# Add a new attribute to this map. Mostly used internally, but is useful for dynamic map creation.
|
119
|
+
# returns the newly created CsvMapper::AttributeMap
|
120
|
+
def add_attribute(name, index=nil)
|
121
|
+
attr_mapping = CsvMapper::AttributeMap.new(name.to_sym, index, @context)
|
122
|
+
self.mapped_attributes << attr_mapping
|
123
|
+
attr_mapping
|
124
|
+
end
|
125
|
+
|
126
|
+
# The current cursor location
|
127
|
+
def cursor # :nodoc:
|
128
|
+
@cursor ||= 0
|
129
|
+
end
|
130
|
+
|
131
|
+
# Move the cursor relative to it's current position
|
132
|
+
def move_cursor(positions=1) # :nodoc:
|
133
|
+
self.cursor += positions
|
134
|
+
end
|
135
|
+
|
136
|
+
# Given a CSV row return an instance of an object defined by this mapping
|
137
|
+
def parse(csv_row)
|
138
|
+
target = self.map_to_class.new
|
139
|
+
@before_filters.each {|filter| filter.call(csv_row, target) }
|
140
|
+
|
141
|
+
self.mapped_attributes.each do |attr_map|
|
142
|
+
target.send("#{attr_map.name}=", attr_map.parse(csv_row))
|
143
|
+
end
|
144
|
+
|
145
|
+
@after_filters.each {|filter| filter.call(csv_row, target) }
|
146
|
+
|
147
|
+
return target
|
148
|
+
end
|
149
|
+
|
150
|
+
protected # :nodoc:
|
151
|
+
|
152
|
+
# The Hacktastic "magic"
|
153
|
+
# Used to dynamically create CsvMapper::AttributeMaps based on unknown method calls that
|
154
|
+
# should represent the names of mapped attributes.
|
155
|
+
#
|
156
|
+
# An optional first argument is used to move this maps cursor position and as the index of the
|
157
|
+
# new AttributeMap
|
158
|
+
def method_missing(name, *args) # :nodoc:
|
159
|
+
existing_map = self.mapped_attributes.find {|attr| attr.name == name}
|
160
|
+
return existing_map if existing_map
|
161
|
+
|
162
|
+
# Effectively add an alias when we see new_field('With/Aliased/Name')
|
163
|
+
if args[0].is_a? String
|
164
|
+
return add_attribute(name, headers_to_indices.fetch(args[0].downcase))
|
165
|
+
end
|
166
|
+
|
167
|
+
if @named_columns
|
168
|
+
return add_attribute(name, headers_to_indices.fetch(name.to_s))
|
169
|
+
end
|
170
|
+
|
171
|
+
if index = args[0]
|
172
|
+
self.move_cursor(index - self.cursor)
|
173
|
+
else
|
174
|
+
index = self.cursor
|
175
|
+
self.move_cursor
|
176
|
+
end
|
177
|
+
|
178
|
+
add_attribute(name, index)
|
179
|
+
end
|
180
|
+
|
181
|
+
def add_filters(to_hook, *filters) # :nodoc:
|
182
|
+
(to_hook << filters.collect do |filter|
|
183
|
+
filter.is_a?(Symbol) ? lambda{|row, target| @context.send(filter, row, target)} : filter
|
184
|
+
end).flatten!
|
185
|
+
end
|
186
|
+
|
187
|
+
def iterate_headers
|
188
|
+
@start_at_row = [ @start_at_row, 1 ].max
|
189
|
+
|
190
|
+
csv = FasterCSV.new(@csv_data, @parser_options)
|
191
|
+
|
192
|
+
# Header is for now assumed to be one row above data
|
193
|
+
(@start_at_row - 1).times { csv.readline } if @start_at_row > 1
|
194
|
+
|
195
|
+
attributes = csv.readline
|
196
|
+
@csv_data.rewind
|
197
|
+
attributes.each_with_index { |name, index| yield name, index }
|
198
|
+
end
|
199
|
+
|
200
|
+
def headers_to_indices
|
201
|
+
return @h_to_i if @h_to_i
|
202
|
+
@h_to_i = {}
|
203
|
+
iterate_headers { |name, index| @h_to_i[name.strip.downcase] = index if name }
|
204
|
+
@h_to_i
|
205
|
+
end
|
206
|
+
|
207
|
+
def map_to_class # :nodoc:
|
208
|
+
unless @map_to_klass
|
209
|
+
attrs = mapped_attributes.collect {|attr_map| attr_map.name}
|
210
|
+
@map_to_klass = Struct.new(nil, *attrs)
|
211
|
+
end
|
212
|
+
|
213
|
+
@map_to_klass
|
214
|
+
end
|
215
|
+
|
216
|
+
def cursor=(value) # :nodoc:
|
217
|
+
@cursor=value
|
218
|
+
end
|
219
|
+
|
220
|
+
def attributize_field_name(name)
|
221
|
+
name.gsub(/\s+/, '_').gsub(/[\W]+/, '').downcase
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../spec_helper.rb'
|
2
|
+
|
3
|
+
describe CsvMapper::AttributeMap do
|
4
|
+
|
5
|
+
class TestContext
|
6
|
+
def transform_it(row, index)
|
7
|
+
:transform_it_success
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
before(:each) do
|
12
|
+
@row_attr = CsvMapper::AttributeMap.new('foo', 1, TestContext.new)
|
13
|
+
@csv_row = ['first_name', 'last_name']
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should map a destination attribute name" do
|
17
|
+
@row_attr.name.should == 'foo'
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should map a CSV column index" do
|
21
|
+
@row_attr.index.should be(1)
|
22
|
+
end
|
23
|
+
|
24
|
+
it "should map a transformation between the CSV value and destination value and chain method calls" do
|
25
|
+
@row_attr.map(:named_transform).should be(@row_attr)
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should provide ability to set the index and chain method calls" do
|
29
|
+
@row_attr.at(9).should be(@row_attr)
|
30
|
+
@row_attr.index.should be(9)
|
31
|
+
end
|
32
|
+
|
33
|
+
it "should parse values" do
|
34
|
+
@row_attr.parse(@csv_row).should == @csv_row[1]
|
35
|
+
end
|
36
|
+
|
37
|
+
it "should parse values using a mapped lambda transformers" do
|
38
|
+
@row_attr.map( lambda{|row, index| :success } )
|
39
|
+
@row_attr.parse(@csv_row).should == :success
|
40
|
+
end
|
41
|
+
|
42
|
+
it "should parse values using a mapped lambda transformer that only accepts the row" do
|
43
|
+
@row_attr.map( lambda{|row| :success } )
|
44
|
+
@row_attr.parse(@csv_row).should == :success
|
45
|
+
end
|
46
|
+
|
47
|
+
it "should parse values using a mapped block transformers" do
|
48
|
+
@row_attr.map {|row, index| :success }
|
49
|
+
@row_attr.parse(@csv_row).should == :success
|
50
|
+
end
|
51
|
+
|
52
|
+
it "should parse values using a mapped block transformer that only accepts the row" do
|
53
|
+
@row_attr.map {|row, index| :success }
|
54
|
+
@row_attr.parse(@csv_row).should == :success
|
55
|
+
end
|
56
|
+
|
57
|
+
it "should parse values using a named method on the context" do
|
58
|
+
@row_attr.map(:transform_it).parse(@csv_row).should == :transform_it_success
|
59
|
+
end
|
60
|
+
|
61
|
+
it "should provide access to the raw value" do
|
62
|
+
@row_attr.raw_value(@csv_row).should be(@csv_row[@row_attr.index])
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
@@ -0,0 +1,139 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../spec_helper.rb'
|
2
|
+
|
3
|
+
describe CsvMapper::RowMap do
|
4
|
+
|
5
|
+
class TestMapToClass
|
6
|
+
attr_accessor :foo, :bar, :baz
|
7
|
+
end
|
8
|
+
|
9
|
+
class TestMapContext
|
10
|
+
def transform(row, index)
|
11
|
+
:transform_success
|
12
|
+
end
|
13
|
+
|
14
|
+
def change_name(row, target)
|
15
|
+
row[0] = :changed_name
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
let(:test_context) { TestMapContext.new }
|
20
|
+
|
21
|
+
before(:each) do
|
22
|
+
@row_map = CsvMapper::RowMap.new(test_context)
|
23
|
+
@csv_row = ['first_name', 'last_name']
|
24
|
+
end
|
25
|
+
|
26
|
+
it "should provide access to the context" do
|
27
|
+
@row_map.context.should == test_context
|
28
|
+
end
|
29
|
+
|
30
|
+
it "should parse a CSV row" do
|
31
|
+
@row_map.parse(@csv_row).should_not be_nil
|
32
|
+
end
|
33
|
+
|
34
|
+
it "should map to a Struct by default" do
|
35
|
+
@row_map.parse(@csv_row).should be_kind_of(Struct)
|
36
|
+
end
|
37
|
+
|
38
|
+
it "should parse a CSV row returning the mapped result" do
|
39
|
+
@row_map.fname
|
40
|
+
@row_map.lname
|
41
|
+
|
42
|
+
result = @row_map.parse(@csv_row)
|
43
|
+
result.fname.should == @csv_row[0]
|
44
|
+
result.lname.should == @csv_row[1]
|
45
|
+
end
|
46
|
+
|
47
|
+
it "should map to a ruby class with optional default attribute values" do
|
48
|
+
@row_map.map_to TestMapToClass, :baz => :default_baz
|
49
|
+
|
50
|
+
@row_map.foo
|
51
|
+
@row_map.bar
|
52
|
+
|
53
|
+
(result = @row_map.parse(@csv_row)).should be_instance_of(TestMapToClass)
|
54
|
+
result.foo.should == @csv_row[0]
|
55
|
+
result.bar.should == @csv_row[1]
|
56
|
+
result.baz.should == :default_baz
|
57
|
+
end
|
58
|
+
|
59
|
+
it "should define Infinity" do
|
60
|
+
CsvMapper::RowMap::Infinity.should == 1.0/0
|
61
|
+
end
|
62
|
+
|
63
|
+
it "should start at the specified CSV row" do
|
64
|
+
@row_map.start_at_row.should == 0
|
65
|
+
@row_map.start_at_row(1)
|
66
|
+
@row_map.start_at_row.should == 1
|
67
|
+
end
|
68
|
+
|
69
|
+
it "should stop at the specified row" do
|
70
|
+
@row_map.stop_at_row.should be(CsvMapper::RowMap::Infinity)
|
71
|
+
@row_map.stop_at_row(6)
|
72
|
+
@row_map.stop_at_row.should == 6
|
73
|
+
end
|
74
|
+
|
75
|
+
it "should allow before row processing" do
|
76
|
+
@row_map.before_row :change_name, lambda{|row, target| row[1] = 'bar'}
|
77
|
+
|
78
|
+
@row_map.first_name
|
79
|
+
@row_map.foo
|
80
|
+
|
81
|
+
result = @row_map.parse(@csv_row)
|
82
|
+
result.first_name.should == :changed_name
|
83
|
+
result.foo.should == 'bar'
|
84
|
+
end
|
85
|
+
|
86
|
+
it "should allow after row processing" do
|
87
|
+
filter_var = nil
|
88
|
+
@row_map.after_row lambda{|row, target| filter_var = :woot}
|
89
|
+
|
90
|
+
@row_map.parse(@csv_row)
|
91
|
+
filter_var.should == :woot
|
92
|
+
end
|
93
|
+
|
94
|
+
it "should have a moveable cursor" do
|
95
|
+
@row_map.cursor.should be(0)
|
96
|
+
@row_map.move_cursor
|
97
|
+
@row_map.cursor.should be(1)
|
98
|
+
@row_map.move_cursor 3
|
99
|
+
@row_map.cursor.should be(4)
|
100
|
+
end
|
101
|
+
|
102
|
+
it "should skip indexes" do
|
103
|
+
pre_cursor = @row_map.cursor
|
104
|
+
@row_map._SKIP_
|
105
|
+
@row_map.cursor.should be(pre_cursor + 1)
|
106
|
+
end
|
107
|
+
|
108
|
+
it "should accept FasterCSV parser options" do
|
109
|
+
@row_map.parser_options :row_sep => :auto
|
110
|
+
@row_map.parser_options[:row_sep].should == :auto
|
111
|
+
end
|
112
|
+
|
113
|
+
it "should have a configurable the column delimiter" do
|
114
|
+
@row_map.delimited_by '|'
|
115
|
+
@row_map.delimited_by.should == '|'
|
116
|
+
end
|
117
|
+
|
118
|
+
it "should maintain a collection of attribute mappings" do
|
119
|
+
@row_map.mapped_attributes.should be_kind_of(Enumerable)
|
120
|
+
end
|
121
|
+
|
122
|
+
it "should lazy initialize attribute maps and move the cursor" do
|
123
|
+
pre_cursor = @row_map.cursor
|
124
|
+
(attr_map = @row_map.first_name).should be_instance_of(CsvMapper::AttributeMap)
|
125
|
+
attr_map.index.should be(pre_cursor)
|
126
|
+
@row_map.cursor.should be(pre_cursor + 1)
|
127
|
+
end
|
128
|
+
|
129
|
+
it "should lazy initialize attribute maps with optional cursor position" do
|
130
|
+
pre_cursor = @row_map.cursor
|
131
|
+
@row_map.last_name(1).index.should be(1)
|
132
|
+
@row_map.cursor.should be(1)
|
133
|
+
end
|
134
|
+
|
135
|
+
it "should share its context with its mappings" do
|
136
|
+
@row_map.first_name.map(:transform)
|
137
|
+
@row_map.parse(@csv_row).first_name.should == :transform_success
|
138
|
+
end
|
139
|
+
end
|
@@ -0,0 +1,238 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper.rb'
|
2
|
+
|
3
|
+
describe CsvMapper do
|
4
|
+
describe "included" do
|
5
|
+
before(:each) do
|
6
|
+
@mapped_klass = Class.new do
|
7
|
+
include CsvMapper
|
8
|
+
|
9
|
+
def upcase_name(row, index)
|
10
|
+
row[index].upcase
|
11
|
+
end
|
12
|
+
end
|
13
|
+
@mapped = @mapped_klass.new
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should allow the creation of CSV mappings" do
|
17
|
+
mapping = @mapped.map_csv do
|
18
|
+
start_at_row 2
|
19
|
+
end
|
20
|
+
|
21
|
+
mapping.should be_instance_of(CsvMapper::RowMap)
|
22
|
+
mapping.start_at_row.should == 2
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should import a CSV IO" do
|
26
|
+
io = 'foo,bar,00,01'
|
27
|
+
results = @mapped.import(io, :type => :io) do
|
28
|
+
first
|
29
|
+
second
|
30
|
+
end
|
31
|
+
|
32
|
+
results.should be_kind_of(Enumerable)
|
33
|
+
results.should have(1).things
|
34
|
+
results[0].first.should == 'foo'
|
35
|
+
results[0].second.should == 'bar'
|
36
|
+
end
|
37
|
+
|
38
|
+
it "should import a CSV File IO" do
|
39
|
+
results = @mapped.import(File.dirname(__FILE__) + '/test.csv') do
|
40
|
+
start_at_row 1
|
41
|
+
[first_name, last_name, age]
|
42
|
+
end
|
43
|
+
|
44
|
+
results.size.should == 3
|
45
|
+
end
|
46
|
+
|
47
|
+
it "should stop importing at a specified row" do
|
48
|
+
results = @mapped.import(File.dirname(__FILE__) + '/test.csv') do
|
49
|
+
start_at_row 1
|
50
|
+
stop_at_row 2
|
51
|
+
[first_name, last_name, age]
|
52
|
+
end
|
53
|
+
|
54
|
+
results.size.should == 2
|
55
|
+
end
|
56
|
+
|
57
|
+
it "should be able to read attributes from a csv file" do
|
58
|
+
results = @mapped.import(File.dirname(__FILE__) + '/test.csv') do
|
59
|
+
# we'll alias age here just as an example
|
60
|
+
read_attributes_from_file('Age' => 'number_of_years_old')
|
61
|
+
end
|
62
|
+
results[1].first_name.should == 'Jane'
|
63
|
+
results[1].last_name.should == 'Doe'
|
64
|
+
results[1].number_of_years_old.should == '26'
|
65
|
+
end
|
66
|
+
|
67
|
+
it "should import non-comma delimited files" do
|
68
|
+
piped_io = 'foo|bar|00|01'
|
69
|
+
|
70
|
+
results = @mapped.import(piped_io, :type => :io) do
|
71
|
+
delimited_by '|'
|
72
|
+
[first, second]
|
73
|
+
end
|
74
|
+
|
75
|
+
results.should have(1).things
|
76
|
+
results[0].first.should == 'foo'
|
77
|
+
results[0].second.should == 'bar'
|
78
|
+
end
|
79
|
+
|
80
|
+
it "should allow named tranformation mappings" do
|
81
|
+
def upcase_name(row)
|
82
|
+
row[0].upcase
|
83
|
+
end
|
84
|
+
|
85
|
+
results = @mapped.import(File.dirname(__FILE__) + '/test.csv') do
|
86
|
+
start_at_row 1
|
87
|
+
|
88
|
+
first_name.map :upcase_name
|
89
|
+
end
|
90
|
+
|
91
|
+
results[0].first_name.should == 'JOHN'
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
describe "extended" do
|
96
|
+
it "should allow the creation of CSV mappings" do
|
97
|
+
mapping = CsvMapper.map_csv do
|
98
|
+
start_at_row 2
|
99
|
+
end
|
100
|
+
|
101
|
+
mapping.should be_instance_of(CsvMapper::RowMap)
|
102
|
+
mapping.start_at_row.should == 2
|
103
|
+
end
|
104
|
+
|
105
|
+
it "should import a CSV IO" do
|
106
|
+
io = 'foo,bar,00,01'
|
107
|
+
results = CsvMapper.import(io, :type => :io) do
|
108
|
+
first
|
109
|
+
second
|
110
|
+
end
|
111
|
+
|
112
|
+
results.should be_kind_of(Enumerable)
|
113
|
+
results.should have(1).things
|
114
|
+
results[0].first.should == 'foo'
|
115
|
+
results[0].second.should == 'bar'
|
116
|
+
end
|
117
|
+
|
118
|
+
it "should import a CSV File IO" do
|
119
|
+
results = CsvMapper.import(File.dirname(__FILE__) + '/test.csv') do
|
120
|
+
start_at_row 1
|
121
|
+
[first_name, last_name, age]
|
122
|
+
end
|
123
|
+
|
124
|
+
results.size.should == 3
|
125
|
+
end
|
126
|
+
|
127
|
+
it "should stop importing at a specified row" do
|
128
|
+
results = CsvMapper.import(File.dirname(__FILE__) + '/test.csv') do
|
129
|
+
start_at_row 1
|
130
|
+
stop_at_row 2
|
131
|
+
[first_name, last_name, age]
|
132
|
+
end
|
133
|
+
|
134
|
+
results.size.should == 2
|
135
|
+
end
|
136
|
+
|
137
|
+
it "should be able to read attributes from a csv file" do
|
138
|
+
results = CsvMapper.import(File.dirname(__FILE__) + '/test.csv') do
|
139
|
+
# we'll alias age here just as an example
|
140
|
+
read_attributes_from_file('Age' => 'number_of_years_old')
|
141
|
+
end
|
142
|
+
results[1].first_name.should == 'Jane'
|
143
|
+
results[1].last_name.should == 'Doe'
|
144
|
+
results[1].number_of_years_old.should == '26'
|
145
|
+
end
|
146
|
+
|
147
|
+
describe "Adding only certain attributes by name or alias" do
|
148
|
+
context "A file with headers and empty column names" do
|
149
|
+
before :all do
|
150
|
+
@results = CsvMapper.import(File.dirname(__FILE__) + '/test_with_empty_column_names.csv') do
|
151
|
+
named_columns
|
152
|
+
surname('Last Name')
|
153
|
+
age.map { |row, index| row[index].to_i }
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
it "should have Last name aliased as surname" do
|
158
|
+
@results[1].surname.should == 'Doe'
|
159
|
+
end
|
160
|
+
|
161
|
+
it "should transform age to 26 (a Fixnum)" do
|
162
|
+
@results[1].age.should == 26
|
163
|
+
end
|
164
|
+
|
165
|
+
it "should not have First Name at all" do
|
166
|
+
lambda { @results[1].first_name }.should raise_error(NoMethodError)
|
167
|
+
end
|
168
|
+
|
169
|
+
it "should raise IndexError when adding non-existent fields" do
|
170
|
+
lambda {
|
171
|
+
@results = CsvMapper.import(File.dirname(__FILE__) + '/test_with_empty_column_names.csv') do
|
172
|
+
add_attributes_by_name('doesnt_exist')
|
173
|
+
end
|
174
|
+
}.should raise_error(IndexError)
|
175
|
+
end
|
176
|
+
|
177
|
+
it "should raise IndexError when adding non-existent aliases" do
|
178
|
+
lambda {
|
179
|
+
@results = CsvMapper.import(File.dirname(__FILE__) + '/test_with_empty_column_names.csv') do
|
180
|
+
my_new_field('doesnt_exist')
|
181
|
+
end
|
182
|
+
}.should raise_error(IndexError)
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
context "A crazy not-really CSV file with some lines to ignore at the top" do
|
187
|
+
before :all do
|
188
|
+
@results = CsvMapper.import(File.dirname(__FILE__) + '/test_with_pushed_down_header.csv') do
|
189
|
+
start_at_row 5
|
190
|
+
named_columns
|
191
|
+
surname('Last Name')
|
192
|
+
age.map { |row, index| row[index].to_i }
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
it "should transform age to 26" do
|
197
|
+
@results[1].age.should == 26
|
198
|
+
end
|
199
|
+
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
it "should be able to assign default column names when column names are null" do
|
204
|
+
results = CsvMapper.import(File.dirname(__FILE__) + '/test_with_empty_column_names.csv') do
|
205
|
+
read_attributes_from_file
|
206
|
+
end
|
207
|
+
|
208
|
+
results[1]._field_1.should == 'unnamed_value'
|
209
|
+
end
|
210
|
+
|
211
|
+
it "should import non-comma delimited files" do
|
212
|
+
piped_io = 'foo|bar|00|01'
|
213
|
+
|
214
|
+
results = CsvMapper.import(piped_io, :type => :io) do
|
215
|
+
delimited_by '|'
|
216
|
+
[first, second]
|
217
|
+
end
|
218
|
+
|
219
|
+
results.should have(1).things
|
220
|
+
results[0].first.should == 'foo'
|
221
|
+
results[0].second.should == 'bar'
|
222
|
+
end
|
223
|
+
|
224
|
+
it "should not allow transformation mappings" do
|
225
|
+
def upcase_name(row)
|
226
|
+
row[0].upcase
|
227
|
+
end
|
228
|
+
|
229
|
+
(lambda do
|
230
|
+
results = CsvMapper.import(File.dirname(__FILE__) + '/test.csv') do
|
231
|
+
start_at_row 1
|
232
|
+
|
233
|
+
first_name.map :upcase_name
|
234
|
+
end
|
235
|
+
end).should raise_error(Exception)
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
data/spec/spec.opts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/spec/spec_helper.rb
ADDED
data/spec/test.csv
ADDED
metadata
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: artforge-csv-mapper
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Luke Pillow
|
9
|
+
- Russell Garner
|
10
|
+
- Adam Singer
|
11
|
+
autorequire:
|
12
|
+
bindir: bin
|
13
|
+
cert_chain: []
|
14
|
+
date: 2013-06-13 00:00:00.000000000 Z
|
15
|
+
dependencies:
|
16
|
+
- !ruby/object:Gem::Dependency
|
17
|
+
name: rspec
|
18
|
+
requirement: !ruby/object:Gem::Requirement
|
19
|
+
none: false
|
20
|
+
requirements:
|
21
|
+
- - ! '>='
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 2.0.0
|
24
|
+
type: :development
|
25
|
+
prerelease: false
|
26
|
+
version_requirements: !ruby/object:Gem::Requirement
|
27
|
+
none: false
|
28
|
+
requirements:
|
29
|
+
- - ! '>='
|
30
|
+
- !ruby/object:Gem::Version
|
31
|
+
version: 2.0.0
|
32
|
+
- !ruby/object:Gem::Dependency
|
33
|
+
name: fastercsv
|
34
|
+
requirement: !ruby/object:Gem::Requirement
|
35
|
+
none: false
|
36
|
+
requirements:
|
37
|
+
- - ! '>='
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '0'
|
40
|
+
type: :runtime
|
41
|
+
prerelease: false
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
43
|
+
none: false
|
44
|
+
requirements:
|
45
|
+
- - ! '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
description: CSV Mapper makes it easy to import data from CSV files directly to a
|
49
|
+
collection of any type of Ruby object. The simplest way to create mappings is declare
|
50
|
+
the names of the attributes in the order corresponding to the CSV file column order.
|
51
|
+
email: adam@artforge.com
|
52
|
+
executables: []
|
53
|
+
extensions: []
|
54
|
+
extra_rdoc_files:
|
55
|
+
- History.txt
|
56
|
+
- LICENSE
|
57
|
+
- README.rdoc
|
58
|
+
files:
|
59
|
+
- History.txt
|
60
|
+
- LICENSE
|
61
|
+
- README.rdoc
|
62
|
+
- Rakefile
|
63
|
+
- VERSION
|
64
|
+
- artforge-csv-mapper-1.0.1.gem
|
65
|
+
- lib/csv-mapper.rb
|
66
|
+
- lib/csv-mapper/attribute_map.rb
|
67
|
+
- lib/csv-mapper/row_map.rb
|
68
|
+
- spec/csv-mapper/attribute_map_spec.rb
|
69
|
+
- spec/csv-mapper/row_map_spec.rb
|
70
|
+
- spec/csv-mapper_spec.rb
|
71
|
+
- spec/spec.opts
|
72
|
+
- spec/spec_helper.rb
|
73
|
+
- spec/test.csv
|
74
|
+
- spec/test_with_empty_column_names.csv
|
75
|
+
- spec/test_with_pushed_down_header.csv
|
76
|
+
homepage: http://github.com/Artforge/csv-mapper
|
77
|
+
licenses: []
|
78
|
+
post_install_message:
|
79
|
+
rdoc_options: []
|
80
|
+
require_paths:
|
81
|
+
- lib
|
82
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
83
|
+
none: false
|
84
|
+
requirements:
|
85
|
+
- - ! '>='
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: '0'
|
88
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ! '>='
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
94
|
+
requirements: []
|
95
|
+
rubyforge_project:
|
96
|
+
rubygems_version: 1.8.25
|
97
|
+
signing_key:
|
98
|
+
specification_version: 3
|
99
|
+
summary: artforge-CsvMapper is a fork of a small library intended to simplify the
|
100
|
+
common steps involved with importing CSV files to a usable form in Ruby. It has
|
101
|
+
support for null column names. When this is merged, this gem will be removed.
|
102
|
+
test_files: []
|