seamusabshere-csv_dictionary 0.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/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Seamus Abshere
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,7 @@
1
+ = csv_dictionary
2
+
3
+ Description goes here.
4
+
5
+ == Copyright
6
+
7
+ Copyright (c) 2009 Seamus Abshere. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,56 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "csv_dictionary"
8
+ gem.summary = %Q{TODO}
9
+ gem.email = "seamus@abshere.net"
10
+ gem.homepage = "http://github.com/seamusabshere/csv_dictionary"
11
+ gem.authors = ["Seamus Abshere"]
12
+
13
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
14
+ end
15
+ rescue LoadError
16
+ puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
17
+ end
18
+
19
+ require 'rake/testtask'
20
+ Rake::TestTask.new(:test) do |test|
21
+ test.libs << 'lib' << 'test'
22
+ test.pattern = 'test/**/*_test.rb'
23
+ test.verbose = true
24
+ end
25
+
26
+ begin
27
+ require 'rcov/rcovtask'
28
+ Rcov::RcovTask.new do |test|
29
+ test.libs << 'test'
30
+ test.pattern = 'test/**/*_test.rb'
31
+ test.verbose = true
32
+ end
33
+ rescue LoadError
34
+ task :rcov do
35
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
36
+ end
37
+ end
38
+
39
+
40
+ task :default => :test
41
+
42
+ require 'rake/rdoctask'
43
+ Rake::RDocTask.new do |rdoc|
44
+ if File.exist?('VERSION.yml')
45
+ config = YAML.load(File.read('VERSION.yml'))
46
+ version = "#{config[:major]}.#{config[:minor]}.#{config[:patch]}"
47
+ else
48
+ version = ""
49
+ end
50
+
51
+ rdoc.rdoc_dir = 'rdoc'
52
+ rdoc.title = "csv_dictionary #{version}"
53
+ rdoc.rdoc_files.include('README*')
54
+ rdoc.rdoc_files.include('lib/**/*.rb')
55
+ end
56
+
@@ -0,0 +1,236 @@
1
+ require 'rubygems'
2
+ require 'fastercsv'
3
+ require 'open-uri'
4
+ require 'active_support'
5
+
6
+ class CsvDictionary
7
+ attr_accessor :url, :key_sprintf, :key_name, :sheet # set in options
8
+ attr_accessor :dictionary, :value_name
9
+
10
+ def replace_attributes!(attributes)
11
+ load_dictionary unless dictionary_loaded?
12
+ attributes[value_name] = lookup(attributes[key_name])
13
+ attributes.delete(key_name)
14
+ end
15
+
16
+ def lookup(key)
17
+ load_dictionary unless dictionary_loaded?
18
+ dictionary[interpret_key(key)]
19
+ rescue
20
+ nil
21
+ end
22
+
23
+ private
24
+
25
+ def initialize(key_name, options = {})
26
+ @key_name = key_name
27
+ @sheet = options[:sheet] ? options[:sheet] - 1 : nil
28
+ @key_sprintf = options[:key_sprintf] || '%s'
29
+ @url = options[:url]
30
+ validate
31
+ end
32
+
33
+ def validate
34
+ raise 'Please specify a key name.' if key_name.blank?
35
+ raise 'Please specify a url.' if url.blank?
36
+ raise ':sheet option is only supported for Google Docs Spreadsheet URLs.' if not sheet.nil? and not google_docs_spreadsheet_url?(url)
37
+ if google_docs_spreadsheet_url?(url) and /\&gid=(.*)(\&|$)/.match(url)
38
+ raise "Your Google Docs Spreadsheet URL includes a sheet number (&gid=#{$1}). Please remove it and set the sheet with :sheet => #{$1.to_i + 1}"
39
+ end
40
+ end
41
+
42
+ def google_docs_spreadsheet_url?(url)
43
+ url.include?('spreadsheets.google.com')
44
+ end
45
+
46
+ def dictionary_loaded?
47
+ !dictionary.nil?
48
+ end
49
+
50
+ def load_dictionary
51
+ self.dictionary = {}
52
+ open(interpret_url(url)) do |data|
53
+ FasterCSV.parse(data, :headers => :first_row) do |row|
54
+ self.value_name ||= row.headers[1].to_sym
55
+ next if row.fields[0..1].any? { |c| c.blank? }
56
+ key = interpret_key(row.fields[0])
57
+ value = row.fields[1].to_s.strip
58
+ dictionary[key] = value
59
+ end
60
+ end
61
+ end
62
+
63
+ def interpret_url(url)
64
+ if google_docs_spreadsheet_url?(url)
65
+ url = url.gsub(/\&output=.*(\&|$)/, '')
66
+ url << "&output=csv&gid=#{sheet}"
67
+ end
68
+ url
69
+ end
70
+
71
+ def interpret_key(k)
72
+ k = k.to_s.strip
73
+ k = k.to_i if /\%[0-9\.]*d/.match(key_sprintf)
74
+ key_sprintf % k
75
+ end
76
+ end
77
+
78
+ module ActiveRecord
79
+ module CsvDictionaryExt
80
+ def self.included(klass)
81
+ klass.class_eval do
82
+ self.class_inheritable_accessor :csv_dictionaries
83
+ extend ClassMethods
84
+ end
85
+ end
86
+
87
+ module ClassMethods
88
+ def csv_dictionary(key_name, options = {})
89
+ class_eval { @old_method_missing = instance_method(:method_missing) }
90
+ self.csv_dictionaries ||= {}
91
+ csv_dictionaries[key_name] = CsvDictionary.new(key_name, options)
92
+ extend InstanceMethods
93
+ end
94
+
95
+ def clear_csv_dictionaries
96
+ self.csv_dictionaries = {}
97
+ end
98
+
99
+ def replace_csv_dictionary_attributes!(attributes)
100
+ csv_dictionaries.values.each { |d| d.replace_attributes!(attributes) }
101
+ end
102
+ end
103
+
104
+ module InstanceMethods
105
+ def all_attributes_or_csv_dictionary_attributes_exists?(attribute_names)
106
+ attribute_names = expand_attribute_names_for_aggregates(attribute_names)
107
+ attribute_names.all? { |name| column_methods_hash.include?(name.to_sym) or csv_dictionaries.has_key?(name.to_sym) }
108
+ end
109
+
110
+ # like Rails 2.3.2
111
+ # copied from http://github.com/rails/rails/raw/dd2eb1ea7c34eb6496feaf7e42100f37a8dae76b/activerecord/lib/active_record/base.rb
112
+ def method_missing_with_csv_dictionary_attributes(method_id, *arguments, &block)
113
+ if match = DynamicFinderMatch.match(method_id)
114
+ attribute_names = match.attribute_names
115
+ return method_missing_without_csv_dictionary_attributes(method_id, arguments, block) unless all_attributes_or_csv_dictionary_attributes_exists?(attribute_names)
116
+ if match.finder?
117
+ finder = match.finder
118
+ bang = match.bang?
119
+ # def self.find_by_login_and_activated(*args)
120
+ # options = args.extract_options!
121
+ # attributes = construct_attributes_from_arguments(
122
+ # [:login,:activated],
123
+ # args
124
+ # )
125
+ #
126
+ # replace_csv_dictionary_attributes!(attributes) # ADDED BY SEAMUS
127
+ #
128
+ # finder_options = { :conditions => attributes }
129
+ # validate_find_options(options)
130
+ # set_readonly_option!(options)
131
+ #
132
+ # if options[:conditions]
133
+ # with_scope(:find => finder_options) do
134
+ # find(:first, options)
135
+ # end
136
+ # else
137
+ # find(:first, options.merge(finder_options))
138
+ # end
139
+ # end
140
+ self.class_eval %{
141
+ def self.#{method_id}(*args)
142
+ options = args.extract_options!
143
+ attributes = construct_attributes_from_arguments(
144
+ [:#{attribute_names.join(',:')}],
145
+ args
146
+ )
147
+
148
+ replace_csv_dictionary_attributes!(attributes)
149
+
150
+ finder_options = { :conditions => attributes }
151
+ validate_find_options(options)
152
+ set_readonly_option!(options)
153
+
154
+ #{'result = ' if bang}if options[:conditions]
155
+ with_scope(:find => finder_options) do
156
+ find(:#{finder}, options)
157
+ end
158
+ else
159
+ find(:#{finder}, options.merge(finder_options))
160
+ end
161
+ #{'result || raise(RecordNotFound, "Couldn\'t find #{name} with #{attributes.to_a.collect {|pair| "#{pair.first} = #{pair.second}"}.join(\', \')}")' if bang}
162
+ end
163
+ }, __FILE__, __LINE__
164
+ send(method_id, *arguments)
165
+ elsif match.instantiator?
166
+ instantiator = match.instantiator
167
+ # def self.find_or_create_by_user_id(*args)
168
+ # guard_protected_attributes = false
169
+ #
170
+ # if args[0].is_a?(Hash)
171
+ # guard_protected_attributes = true
172
+ # attributes = args[0].with_indifferent_access
173
+ # find_attributes = attributes.slice(*[:user_id])
174
+ # else
175
+ # find_attributes = attributes = construct_attributes_from_arguments([:user_id], args)
176
+ # end
177
+ #
178
+ # replace_csv_dictionary_attributes!(attributes) # ADDED BY SEAMUS
179
+ # replace_csv_dictionary_attributes!(find_attributes) # ADDED BY SEAMUS
180
+ #
181
+ # options = { :conditions => find_attributes }
182
+ # set_readonly_option!(options)
183
+ #
184
+ # record = find(:first, options)
185
+ #
186
+ # if record.nil?
187
+ # record = self.new { |r| r.send(:attributes=, attributes, guard_protected_attributes) }
188
+ # yield(record) if block_given?
189
+ # record.save
190
+ # record
191
+ # else
192
+ # record
193
+ # end
194
+ # end
195
+ self.class_eval %{
196
+ def self.#{method_id}(*args)
197
+ guard_protected_attributes = false
198
+
199
+ if args[0].is_a?(Hash)
200
+ guard_protected_attributes = true
201
+ attributes = args[0].with_indifferent_access
202
+ find_attributes = attributes.slice(*[:#{attribute_names.join(',:')}])
203
+ replace_csv_dictionary_attributes!(attributes)
204
+ replace_csv_dictionary_attributes!(find_attributes)
205
+ else
206
+ find_attributes = attributes = construct_attributes_from_arguments([:#{attribute_names.join(',:')}], args)
207
+ replace_csv_dictionary_attributes!(attributes) # only have to do this once, since find_attributes.object_id == attributes.object_id
208
+ end
209
+
210
+ options = { :conditions => find_attributes }
211
+ set_readonly_option!(options)
212
+
213
+ record = find(:first, options)
214
+
215
+ if record.nil?
216
+ record = self.new { |r| r.send(:attributes=, attributes, guard_protected_attributes) }
217
+ #{'yield(record) if block_given?'}
218
+ #{'record.save' if instantiator == :create}
219
+ record
220
+ else
221
+ record
222
+ end
223
+ end
224
+ }, __FILE__, __LINE__
225
+ send(method_id, *arguments, &block)
226
+ end
227
+ else
228
+ method_missing_without_csv_dictionary_attributes(method_id, arguments, block)
229
+ end
230
+ end
231
+ alias_method_chain :method_missing, :csv_dictionary_attributes
232
+ end
233
+ end
234
+ end
235
+
236
+ ActiveRecord::Base.send :include, ActiveRecord::CsvDictionaryExt
@@ -0,0 +1,82 @@
1
+ require 'test_helper'
2
+
3
+ class CsvDictionaryExtTest < Test::Unit::TestCase
4
+ def setup
5
+ CsvDictionaryTestHelper.setup_database
6
+ end
7
+
8
+ def teardown
9
+ Aircraft.clear_csv_dictionaries
10
+ CsvDictionaryTestHelper.teardown_database
11
+ end
12
+
13
+ # sanity check
14
+ def test_should_have_dictionary_value_name
15
+ assert_nothing_raised do
16
+ Aircraft.new.brighter_planet_aircraft_class_code
17
+ end
18
+ end
19
+
20
+ # sanity check
21
+ def test_should_not_have_dictionary_key_name
22
+ assert_raise NoMethodError do
23
+ Aircraft.new.bts_aircraft_type
24
+ end
25
+ end
26
+
27
+ def test_should_mixin_methods
28
+ assert Aircraft.respond_to?(:csv_dictionary)
29
+ assert Aircraft.respond_to?(:clear_csv_dictionaries)
30
+ assert Aircraft.respond_to?(:replace_csv_dictionary_attributes!)
31
+ end
32
+
33
+ def test_should_not_affect_other_activerecord_models
34
+ Aircraft.csv_dictionary :bts_aircraft_type, :url => CSV_URL
35
+ assert Hash === Aircraft.csv_dictionaries
36
+ assert NilClass === Scrum.csv_dictionaries
37
+ end
38
+
39
+ def test_should_configure_csv_dictionary
40
+ key_name = :bts_aircraft_type
41
+ options = { :url => GDOCS_URL, :sheet => 1 }
42
+ CsvDictionary.expects(:new).with(key_name, options)
43
+ Aircraft.csv_dictionary key_name, options
44
+ end
45
+
46
+ def test_should_replace_attributes
47
+ Aircraft.csv_dictionary :bts_aircraft_type, :url => CSV_URL
48
+ Aircraft.csv_dictionaries[:bts_aircraft_type].expects(:replace_attributes!)
49
+ Aircraft.replace_csv_dictionary_attributes!({})
50
+ end
51
+
52
+ def test_should_recognize_dynamic_finders
53
+ fixture = Aircraft.find_or_create_by_brighter_planet_aircraft_class_code('SJ')
54
+ Aircraft.csv_dictionary :bts_aircraft_type, :url => CSV_URL
55
+ Aircraft.csv_dictionaries[:bts_aircraft_type].expects(:lookup).with('103').returns('SJ').times(4)
56
+ assert_equal fixture, Aircraft.find_by_bts_aircraft_type('103')
57
+ assert_equal fixture, Aircraft.find_by_bts_aircraft_type_and_brighter_planet_aircraft_class_code('103', 'SJ')
58
+ assert_equal [fixture], Aircraft.find_all_by_bts_aircraft_type('103')
59
+ assert_equal [fixture], Aircraft.find_all_by_bts_aircraft_type_and_brighter_planet_aircraft_class_code('103', 'SJ')
60
+ end
61
+
62
+ def test_should_recognize_dynamic_initializers
63
+ fixture = Aircraft.find_or_create_by_brighter_planet_aircraft_class_code('SJ')
64
+ Aircraft.csv_dictionary :bts_aircraft_type, :url => CSV_URL
65
+ Aircraft.csv_dictionaries[:bts_aircraft_type].expects(:lookup).with('103').returns('SJ').twice
66
+ assert_equal fixture, Aircraft.find_or_create_by_bts_aircraft_type('103')
67
+ assert_equal fixture, Aircraft.find_or_create_by_bts_aircraft_type_and_brighter_planet_aircraft_class_code('103', 'SJ')
68
+ end
69
+
70
+ def test_should_not_recognize_bad_dynamic_finders
71
+ Aircraft.csv_dictionary :bts_aircraft_type, :url => CSV_URL
72
+ assert_raise NoMethodError do
73
+ Aircraft.find_or_create_by_foobar('X')
74
+ end
75
+ assert_raise NoMethodError do
76
+ Aircraft.find_or_create_by_foobar_and_id('X', 'Y')
77
+ end
78
+ assert_raise NoMethodError do
79
+ Aircraft.find_or_create_by_foobar_and_bts_aircraft_type('X', 'Y')
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,60 @@
1
+ require 'test_helper'
2
+
3
+ class CsvDictionaryTest < Test::Unit::TestCase
4
+ def setup
5
+ CsvDictionaryTestHelper.setup_database
6
+ @dictionary = CsvDictionary.new(:bts_aircraft_type, :url => GDOCS_URL, :sheet => 1)
7
+ end
8
+
9
+ def teardown
10
+ Aircraft.clear_csv_dictionaries
11
+ CsvDictionaryTestHelper.teardown_database
12
+ end
13
+
14
+ def test_should_set_key_name
15
+ assert_equal :bts_aircraft_type, @dictionary.key_name
16
+ end
17
+
18
+ def test_should_set_url
19
+ dictionary = CsvDictionary.new(:bts_aircraft_type, :url => CSV_URL)
20
+ assert_equal CSV_URL, dictionary.url
21
+ end
22
+
23
+ def test_should_interpret_gdocs_urls
24
+ assert_equal GDOCS_URL + '&output=csv&gid=0', @dictionary.send(:interpret_url, @dictionary.url)
25
+ end
26
+
27
+ def test_should_raise_if_gdocs_url_includes_gid
28
+ assert_raise RuntimeError do
29
+ CsvDictionary.new(:bts_aircraft_type, :url => GDOCS_URL + '&gid=1')
30
+ end
31
+ end
32
+
33
+ def test_should_raise_if_sheet_option_set_on_non_gdocs_url
34
+ assert_raise RuntimeError do
35
+ CsvDictionary.new(:bts_aircraft_type, :url => CSV_URL, :sheet => 1)
36
+ end
37
+ end
38
+
39
+ def test_should_set_default_sheet
40
+ assert_equal 0, @dictionary.sheet
41
+ end
42
+
43
+ def test_should_replace_attributes
44
+ attributes = { :bts_aircraft_type => '103' }
45
+ @dictionary.expects(:lookup).with('103').returns('SJ')
46
+ @dictionary.replace_attributes!(attributes)
47
+ assert_equal({ :brighter_planet_aircraft_class_code => 'SJ' }, attributes)
48
+ end
49
+
50
+ # real http call
51
+ def test_should_set_value_name_after_dictionary_loaded
52
+ @dictionary.send(:load_dictionary)
53
+ assert_equal :brighter_planet_aircraft_class_code, @dictionary.value_name
54
+ end
55
+
56
+ # real http call
57
+ def test_should_lookup
58
+ assert_equal 'SJ', @dictionary.lookup('103')
59
+ end
60
+ end
@@ -0,0 +1,71 @@
1
+ module CsvDictionaryTestHelper
2
+ VENDOR_RAILS = File.expand_path('../../../../rails', __FILE__)
3
+ OTHER_RAILS = File.expand_path('../../../rails', __FILE__)
4
+ PLUGIN_ROOT = File.expand_path('../../', __FILE__)
5
+
6
+ def self.rails_directory
7
+ if File.exist?(VENDOR_RAILS)
8
+ VENDOR_RAILS
9
+ elsif File.exist?(OTHER_RAILS)
10
+ OTHER_RAILS
11
+ end
12
+ end
13
+
14
+ def self.load_dependencies
15
+ if rails_directory
16
+ $:.unshift(File.join(rails_directory, 'activesupport', 'lib'))
17
+ $:.unshift(File.join(rails_directory, 'activerecord', 'lib'))
18
+ else
19
+ require 'rubygems' rescue LoadError
20
+ end
21
+
22
+ require 'active_record'
23
+ require 'test/unit'
24
+ require 'mocha'
25
+
26
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
27
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
28
+ require 'csv_dictionary'
29
+ end
30
+
31
+ def self.configure_database
32
+ ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :dbfile => ":memory:")
33
+ ActiveRecord::Migration.verbose = false
34
+ end
35
+
36
+ def self.setup_database
37
+ ActiveRecord::Schema.define do
38
+ create_table :aircraft do |t|
39
+ t.string :brighter_planet_aircraft_class_code
40
+ t.timestamps
41
+ end
42
+ create_table :scrums do |t|
43
+ t.string :name
44
+ t.timestamps
45
+ end
46
+ end
47
+ end
48
+
49
+ def self.teardown_database
50
+ ActiveRecord::Base.connection.tables.each do |table|
51
+ ActiveRecord::Base.connection.drop_table(table)
52
+ end
53
+ end
54
+
55
+ def self.start
56
+ load_dependencies
57
+ configure_database
58
+ end
59
+ end
60
+
61
+ CsvDictionaryTestHelper.start
62
+
63
+ ActiveSupport::Inflector.inflections do |inflect|
64
+ inflect.uncountable('aircraft')
65
+ end
66
+
67
+ class Aircraft < ActiveRecord::Base; end
68
+ class Scrum < ActiveRecord::Base; end
69
+
70
+ GDOCS_URL = 'http://spreadsheets.google.com/pub?key=p70r3FHguhimD8YmyA34RdA'
71
+ CSV_URL = 'http://static.brighterplanet.com/science/data/transport/air/bts_aircraft_types-brighter_planet_aircraft_classes.csv'
metadata ADDED
@@ -0,0 +1,62 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: seamusabshere-csv_dictionary
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Seamus Abshere
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-04-07 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description:
17
+ email: seamus@abshere.net
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files:
23
+ - LICENSE
24
+ - README.rdoc
25
+ files:
26
+ - LICENSE
27
+ - README.rdoc
28
+ - Rakefile
29
+ - lib/csv_dictionary.rb
30
+ - test/csv_dictionary_ext_test.rb
31
+ - test/csv_dictionary_test.rb
32
+ - test/test_helper.rb
33
+ has_rdoc: true
34
+ homepage: http://github.com/seamusabshere/csv_dictionary
35
+ post_install_message:
36
+ rdoc_options:
37
+ - --charset=UTF-8
38
+ require_paths:
39
+ - lib
40
+ required_ruby_version: !ruby/object:Gem::Requirement
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ version: "0"
45
+ version:
46
+ required_rubygems_version: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: "0"
51
+ version:
52
+ requirements: []
53
+
54
+ rubyforge_project:
55
+ rubygems_version: 1.2.0
56
+ signing_key:
57
+ specification_version: 2
58
+ summary: TODO
59
+ test_files:
60
+ - test/csv_dictionary_ext_test.rb
61
+ - test/csv_dictionary_test.rb
62
+ - test/test_helper.rb