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.
@@ -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: []