artforge-csv-mapper 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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.
@@ -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.
@@ -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
@@ -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
@@ -0,0 +1 @@
1
+ --color
@@ -0,0 +1,3 @@
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
2
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
3
+ require 'csv-mapper'
@@ -0,0 +1,4 @@
1
+ First Name, Last Name, Age
2
+ John,Doe,27
3
+ Jane,Doe,26
4
+ Bat,Man,52
@@ -0,0 +1,4 @@
1
+ First Name, Last Name, Age,,,
2
+ John,Doe,27,,,
3
+ Jane,Doe,26,unnamed_value,,
4
+ Bat,Man,52,,,
@@ -0,0 +1,8 @@
1
+ "Some data"
2
+
3
+ This is a test file whose header line starts at the 0-indexed 4th row and the data starts at the 5th. I know. It's not a CSV as such, but they exist...
4
+
5
+ First Name, Last Name, Age,,,
6
+ John,Doe,27,,,
7
+ Jane,Doe,26,unnamed_value,,
8
+ Bat,Man,52,,,
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: []