drive_time 0.0.3

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,262 @@
1
+ module DriveTime
2
+
3
+ class WorksheetConverter
4
+
5
+ attr_accessor :row_map
6
+
7
+ class NoClassWithTitleError < StandardError; end
8
+ class NoFieldNameError < StandardError; end
9
+
10
+ class NoKeyError < StandardError; end
11
+ class PolymorphicAssociationError < StandardError; end
12
+
13
+ def initialize(model_store, class_name_map, loader, namespace)
14
+ @class_name_map = class_name_map
15
+ @model_store = model_store
16
+ @loader = loader
17
+ @namespace = namespace
18
+ @field_expander = FieldExpander.new(@loader)
19
+ end
20
+
21
+ def convert(worksheet)
22
+ Logger.log_as_header "Converting worksheet: #{worksheet.title}"
23
+ # Use the spreadsheet name unless 'map_to_class' is set
24
+ class_name = DriveTime.class_name_from_title(worksheet.title)
25
+ class_name = @class_name_map.resolve_mapped_from_original class_name
26
+ Logger.debug "Converting Worksheet #{worksheet.title} to class #{class_name}"
27
+ # Check class exists - better we know immediately
28
+ begin
29
+ clazz = namespaced_class_name class_name
30
+ rescue StandardError => error
31
+ raise NoClassWithTitleError, "Worksheet named #{worksheet.title} doesn't exists as class #{class_name}"
32
+ end
33
+ rows = worksheet.rows.dup
34
+ # Remove the first row and use it for field-names
35
+ fields = rows.shift.map{ |row| row }
36
+ # Reject rows of only empty strings (empty cells).
37
+ rows.reject! {|row| row.all?(&:empty?)}
38
+
39
+ rows.each do |row|
40
+ generate_model_from_row clazz, worksheet.mapping, fields, row
41
+ end
42
+
43
+ Logger.log_as_header "Conversion Complete. Woot Woot."
44
+ end
45
+
46
+ protected
47
+
48
+ def generate_model_from_row(clazz, mapping, fields, row)
49
+ Logger.log_as_header "Converting row to class: #{clazz.name}"
50
+ # Create a hash of field-names and row cell values
51
+ build_row_map(fields, row)
52
+
53
+ # If a row has been marked as not complete, ignore it
54
+ if @row_map[:complete] == 'No'
55
+ Logger.debug 'Row marked as not complete. Ignoring'
56
+ return
57
+ end
58
+
59
+ model_key = build_id_for_model mapping
60
+ Logger.log_as_sub_header "Model Key: #{model_key}"
61
+ # Build hash ready to pass into model
62
+ model_fields = row_fields_to_model_fields mapping, model_key
63
+
64
+ # Set the model key as an attribute on the model
65
+ if mapping[:key_to]
66
+ model_fields[mapping[:key_to]] = model_key
67
+ end
68
+
69
+ Logger.debug "Creating Model of class '#{clazz.name.to_s}' with Model Fields #{model_fields.to_s}"
70
+ # Create new model
71
+ model = clazz.new(model_fields, without_protection: true)
72
+ # Add its associations
73
+ add_associations(model, mapping)
74
+ # Store the model using its ID
75
+ @model_store.add_model model, model_key, clazz
76
+ end
77
+
78
+ # Convert worksheet row into hash
79
+ def build_row_map(fields, row)
80
+ @row_map = HashWithIndifferentAccess.new
81
+ Logger.log_as_sub_header "Mapping fields to cells"
82
+ row.dup.each_with_index do |cell, index|
83
+ # Sanitise
84
+ field_name = DriveTime.underscore_from_text fields[index]
85
+ @row_map[field_name] = row[index]
86
+ Logger.debug "- #{field_name} -> #{row[index]}"
87
+ end
88
+ end
89
+
90
+ # Run through the mapping's fields and convert them
91
+ def row_fields_to_model_fields(mapping, model_key)
92
+ model_fields = HashWithIndifferentAccess.new
93
+ # Run through the mapping, pulling values from row_map as required
94
+ if mapping[:fields]
95
+ mapping[:fields].each do |field_mapping|
96
+ field_name = field_mapping[:name]
97
+ mapped_to_field_name = field_mapping[:map_to]
98
+ unless field_name
99
+ raise NoFieldNameError "Missing Field: Name for field: #{value} in mapping: #{mapping}"
100
+ end
101
+
102
+ field_value = @row_map[field_name]
103
+ # Check for token pattern: {{some_value}}
104
+ match = /\{\{(.*?)\}\}/.match(field_value)
105
+ if match
106
+ field_value = @field_expander.expand(match[1], model_key)
107
+ end
108
+
109
+ # Check for Boolean values
110
+ if field_value
111
+ downcased = field_value.downcase
112
+ if downcased == 'y' || downcased == 'yes' || downcased == 'true'
113
+ field_value = true
114
+ elsif downcased == 'n' || downcased == 'no' || downcased == 'false'
115
+ field_value = false
116
+ end
117
+ end
118
+
119
+ # Make sure any empty cells give us nil (rather than an empty string)
120
+ field_value = nil if field_value.blank?
121
+ model_fields[mapped_to_field_name || field_name] = field_value
122
+
123
+ end
124
+ end
125
+ return model_fields
126
+ end
127
+
128
+ # TODO: Refactor this big ugly beast
129
+ def add_associations(model, mapping)
130
+ associations_mapping = mapping[:associations]
131
+ if associations_mapping
132
+ Logger.log_as_sub_header "Adding associations to model "
133
+
134
+ # Loop through any associations defined in the mapping for this model
135
+ associations_mapping.each do |association_mapping|
136
+ # Use the spreadsheet name unless 'map_to_class' is set
137
+ if !association_mapping[:polymorphic]
138
+ association_class_name = @class_name_map.resolve_mapped_from_original association_mapping[:name].classify
139
+ else
140
+ possible_class_names = association_mapping[:name]
141
+ # The classname will be taken from the type collumn
142
+ association_class_name = DriveTime::class_name_from_title @row_map[:type]
143
+ # if !possible_class_names.include? association_class_name.underscore
144
+ # raise PolymorphicAssociationError, "Mapping for polymorphic associations: #{possible_class_names.inspect} doesn't include #{association_class_name}"
145
+ # end
146
+ end
147
+ # Get class reference using class name
148
+ begin
149
+ clazz = namespaced_class_name association_class_name
150
+ rescue
151
+ raise NoClassWithTitleError, "Association defined in worksheet doesn't exist as class: #{association_class_name}"
152
+ end
153
+
154
+ # Assemble associated model instances to satisfy this association
155
+ associated_models = gather_associated_models(association_mapping, association_class_name, clazz)
156
+ set_associations_on_model(model, associated_models, association_mapping, association_class_name)
157
+ end
158
+ end
159
+ end
160
+
161
+ def gather_associated_models(association_mapping, association_class_name, clazz)
162
+ associated_models = []
163
+ if association_mapping[:builder]
164
+ associated_models = associated_models_from_builder association_mapping, association_class_name
165
+ else # It's a single text value, so convert it to an ID
166
+ association_class = @class_name_map.resolve_original_from_mapped(association_class_name).underscore
167
+ if !association_mapping[:polymorphic]
168
+ association_id = @row_map[association_class]
169
+ else
170
+ association_id = @row_map[association_mapping[:polymorphic][:association]]
171
+ end
172
+ raise MissingAssociationError, "No field #{association_class_name.underscore} to satisfy association" if !association_id
173
+ if association_id.length > 0 || association_mapping[:required] == true
174
+ associated_models << model_for_id(association_id, clazz)
175
+ end
176
+ end
177
+ return associated_models
178
+ end
179
+
180
+ def set_associations_on_model(model, associated_models, association_mapping, association_class_name)
181
+ # We now have one or more associated_models to set as associations on our model
182
+ associated_models.each do |associated_model|
183
+ Logger.debug " - Associated Model: #{associated_model}"
184
+ unless association_mapping[:inverse] == true
185
+ association_name = association_class_name.underscore
186
+ if association_mapping[:singular] == true
187
+ Logger.debug " - Adding association #{associated_model} to #{model}::#{association_name}"
188
+ # Set the association
189
+ model.send("#{association_name}=", associated_model)
190
+ else
191
+ association_name = association_name.pluralize
192
+ model_associations = model.send(association_name)
193
+ Logger.debug " - Adding association #{associated_model} to #{model}::#{association_name}"
194
+ # Push the association
195
+ model_associations << associated_model
196
+ end
197
+ else # The relationship is actually inverted, so save the model as an association on the associated_model
198
+ model_name = model.class.name.split('::').last.underscore
199
+ if association_mapping[:singular] == true
200
+ Logger.debug " - Adding association #{model} to #{associated_model}::#{association_name}"
201
+ associated_model.send model_name, model
202
+ else
203
+ model_name = model_name.pluralize
204
+ model_associations = associated_model.send model_name
205
+ Logger.debug " - Adding association #{model} to #{associated_model}::#{model_name}"
206
+ model_associations << model
207
+ end
208
+ end
209
+ end
210
+ end
211
+
212
+ def associated_models_from_builder(association_mapping, class_name)
213
+ associated_models = []
214
+ if association_mapping[:builder] == 'multi' # It's a multi value, find a matching cell and split its value by comma
215
+ cell_value = @row_map[class_name.underscore.pluralize]
216
+ raise MissingAssociationError "No field #{class_name.underscore.pluralize} to satisfy multi association" if !cell_value && association_mapping[:optional] != true
217
+ components = cell_value.split ','
218
+ components.each do |component|
219
+ associated_models << model_for_id(component, namespaced_class_name(class_name))
220
+ end
221
+ elsif association_mapping[:builder] == 'use_fields' # Use column names as values if cell contains 'yes' or 'y'
222
+ association_mapping[:field_names].each do |field_name|
223
+ cell_value = @row_map[field_name]
224
+ if DriveTime.is_affirmative? cell_value
225
+ associated_models << model_for_id(field_name, namespaced_class_name(class_name))
226
+ end
227
+ end
228
+ end
229
+ return associated_models
230
+ end
231
+
232
+ def model_for_id(value, clazz)
233
+ model_key = DriveTime.underscore_from_text value
234
+ return @model_store.get_model clazz, model_key
235
+ end
236
+
237
+ def build_id_for_model(mapping)
238
+ raise NoKeyError, 'All mappings must declare a key' unless mapping.has_key? :key
239
+ key_node = mapping[:key]
240
+ if key_node.is_a? Hash
241
+ if key_node[:builder] == 'join'
242
+ key = JoinBuilder.new.build key_node[:from_fields], @row_map
243
+ elsif key_node[:builder] == 'name'
244
+ key = NameBuilder.new.build key_node[:from_fields], @row_map
245
+ else
246
+ raise "No builder for key on worksheet #{mapping[:title]}"
247
+ end
248
+ else # If it's a string, it refers to a spreadsheet column
249
+ key_attribute = key_node
250
+ # Is there a column
251
+ key = @row_map[key_attribute]
252
+ raise NoFieldNameError, "No column #{key_attribute} on worksheet #{mapping[:title]}" if !key
253
+ DriveTime.underscore_from_text key
254
+ end
255
+ end
256
+
257
+ def namespaced_class_name class_name
258
+ class_name = "#{@namespace}::#{class_name}" unless @namespace.blank?
259
+ class_name.constantize
260
+ end
261
+ end
262
+ end
@@ -0,0 +1,73 @@
1
+ module DriveTime
2
+
3
+ class TokenExpansionError < StandardError; end
4
+
5
+ class FieldExpander
6
+
7
+
8
+
9
+ def initialize(loader)
10
+ @loader = loader
11
+ end
12
+
13
+ def expand(value, model_key)
14
+ filename = model_key
15
+ # Check for token
16
+ match = /\[(.*?)\]/.match(value)
17
+ # Expand token into file
18
+ # Is there a different filename defined in hard brackets [file_name]
19
+ if match
20
+ filename = match[1]
21
+ value = value.split('[').first
22
+ end
23
+
24
+ if value == 'expand_file'
25
+ file = @loader.load_file_direct(filename+'.txt');
26
+ if file.blank?
27
+ raise TokenExpansionError, "Missing file named: #{filename} when expanding from value: #{value} in model: #{model_key}"
28
+ end
29
+ value = expand_file(file)
30
+ elsif value == 'expand_spreadsheet'
31
+ spreadsheet = @loader.load_spreadsheet_direct(filename)
32
+ if spreadsheet.blank?
33
+ raise TokenExpansionError, "Missing spreadsheet named: #{filename} when expanding from value: #{value} in model: #{model_key}"
34
+ end
35
+ # Use first Worksheet
36
+ worksheet = spreadsheet.worksheets[0]
37
+ value = expand_worksheet(worksheet)
38
+ else
39
+ raise TokenExpansionError, "Don't know how to expand the value #{value} for model: #{model_key}"
40
+ end
41
+ return value
42
+ end
43
+
44
+ protected
45
+
46
+ # Build a JSON object from the columns
47
+ def expand_worksheet(worksheet)
48
+ rows = worksheet.rows.dup
49
+
50
+ # Take the first row which will be the column names and use the cell value as the field name
51
+ fields = rows.shift.map{ |row| row[/\w+/] }
52
+
53
+ instances = []
54
+ # Reject rows of only empty strings (empty cells).
55
+ rows.reject! {|row| row.all?(&:empty?)}
56
+
57
+ rows.each do |row|
58
+ key_value_pairs = [fields.dup, row.dup].transpose
59
+ hash = Hash[*key_value_pairs.flatten]
60
+ instances.push(hash)
61
+ end
62
+
63
+ root = {objects: instances }
64
+ return root.to_json.to_s
65
+ end
66
+
67
+ def expand_file(file)
68
+ return file.download_to_string()
69
+ end
70
+
71
+ end
72
+
73
+ end
@@ -0,0 +1,100 @@
1
+ module DriveTime
2
+
3
+ # Load a Spreadsheet from Google Drive
4
+ class Loader
5
+
6
+ class SpreadsheetNotFoundError < StandardError; end
7
+ class WorksheetNotFoundError < StandardError; end
8
+
9
+ def initialize
10
+ begin_session
11
+ end
12
+
13
+ def load_file_direct(title)
14
+ return @session.file_by_title title
15
+ end
16
+
17
+ def load_spreadsheet_direct(title)
18
+ @session.spreadsheet_by_title title
19
+ end
20
+
21
+ def load_spreadsheet(title, use_cache=true)
22
+ cached_directory = ENV['CACHED_DIR']
23
+ spreadsheet_name = "#{title}.yml"
24
+ spreadsheet_file_path = File.join(cached_directory, spreadsheet_name) if cached_directory
25
+ spreadsheet = nil
26
+ # Try and pull the file from the cache
27
+ if cached_directory && use_cache
28
+
29
+ if File.exist? spreadsheet_file_path
30
+ File.open(spreadsheet_file_path, 'r') do |file|
31
+ Logger.info "Pulling spreadsheet '#{title}' from cache"
32
+ spreadsheet = YAML::load(file)
33
+ end
34
+ end
35
+ end
36
+
37
+ # If we haven't loaded a spreadsheet from cache, get it from drive
38
+ unless spreadsheet
39
+ Logger.info "Loading spreadsheet '#{title}' from Drive"
40
+ spreadsheet = @session.spreadsheet_by_title(title)
41
+
42
+ raise SpreadsheetNotFoundError, "Spreadsheet #{title} not found" if spreadsheet.nil?
43
+ # Save the file to cache directory
44
+ if cached_directory && use_cache
45
+ # Save the spreadsheet
46
+ Logger.info "Saving spreadsheet '#{title}' to cache"
47
+ File.open(spreadsheet_file_path, 'w') do |file|
48
+ file.puts YAML::dump(spreadsheet)
49
+ end
50
+
51
+ # Save its worksheets
52
+ spreadsheet.worksheets.each do |worksheet|
53
+ Logger.info "Saving worksheet '#{worksheet.title}' to cache"
54
+ # Force worksheet to down of cells data
55
+ #worksheet.reload
56
+ worksheet_file_path = File.join(cached_directory, worksheet.title) + '.yml'
57
+ File.open(worksheet_file_path, 'w') do |file|
58
+ file.puts YAML::dump(worksheet)
59
+ end
60
+ end
61
+ end
62
+ end
63
+
64
+ return spreadsheet
65
+ end
66
+
67
+ def load_worksheet_from_spreadsheet(spreadsheet, title, use_cache=true)
68
+ cached_directory = ENV['CACHED_DIR']
69
+ worksheet_name = "#{title}.yml"
70
+
71
+ worksheet = nil
72
+
73
+ # Get the worksheet from the cache
74
+ if cached_directory && use_cache
75
+ worksheet_file_path = File.join(cached_directory, title)
76
+ if File.exist? worksheet_file_path
77
+ File.open(worksheet_file_path, 'r') do |file|
78
+ Logger.info "Pulling worksheet '#{title}' from cache"
79
+ worksheet = YAML::load(file)
80
+ end
81
+ end
82
+ end
83
+
84
+ # If we don't yet have a worksheet, pull it from Google Drive via the Spreadsheet
85
+ unless worksheet
86
+ worksheet = spreadsheet.worksheet_by_title title
87
+ raise WorksheetNotFoundError, "Worksheet '#{title}'' not found in spreadsheet '#{spreadsheet.title}'" if worksheet.nil?
88
+ end
89
+
90
+ return worksheet
91
+ end
92
+
93
+ protected
94
+
95
+ def begin_session
96
+ @session = GoogleDrive.login( ENV['GOOGLE_USERNAME'], ENV['GOOGLE_PASSWORD'])
97
+ end
98
+
99
+ end
100
+ end
@@ -0,0 +1,19 @@
1
+ require "log4r"
2
+
3
+ module Log4r
4
+ class Logger
5
+ def log_as_header(message)
6
+ puts "\n"
7
+ info "=============================================================================="
8
+ info "#{message}"
9
+ info '=============================================================================='
10
+ end
11
+
12
+ def log_as_sub_header(message)
13
+ puts "\n" if self.level <= DEBUG
14
+ debug "--------------------------------------------------------------------------------"
15
+ debug " #{message}"
16
+ debug '--------------------------------------------------------------------------------'
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,73 @@
1
+ require 'log4r'
2
+
3
+ module DriveTime
4
+
5
+ include Log4r
6
+
7
+ # Store model instances by class and link
8
+ # This way we can look them up as needed to
9
+ # satisfy dependencies and avoid duplication
10
+ class ModelStore
11
+
12
+ # Errors
13
+ class NoModelsOfClassInStoreError < StandardError; end
14
+ class NoModelOfClassWithKeyInStoreError < StandardError; end
15
+ class ModelAddedTwiceError < StandardError; end
16
+
17
+ def initialize(log_level=Log4r::INFO)
18
+ @store = {}
19
+
20
+ # Set up logging
21
+ formatter = Log4r::PatternFormatter.new(:pattern => "[%c] %M")
22
+ outputter = Log4r::Outputter.stdout
23
+ outputter.formatter = formatter
24
+
25
+ @logger = Log4r::Logger.new ' Model Store '
26
+ @logger.level = log_level
27
+ @logger.outputters = outputter
28
+ end
29
+
30
+ # Store the model by class to avoid key collisions
31
+ def add_model(instance, key, clazz)
32
+ class_string = clazz.to_s
33
+ # Sanitise key
34
+ key = DriveTime.underscore_from_text(key)
35
+ @logger.debug "Adding model with key #{key} of class #{clazz}"
36
+ if !@store[class_string]
37
+ @store[class_string] = {}
38
+ elsif @store[class_string][key]
39
+ raise ModelAddedTwiceError, "#{instance} has already been added to model store"
40
+ end
41
+ @store[class_string][key] = instance
42
+ end
43
+
44
+ def get_model(clazz, key)
45
+ @logger.debug "Request for model with key #{key} of class #{clazz}"
46
+
47
+ models_for_class = @store[clazz.to_s]
48
+ # Are there any classes of this type in the store?
49
+ if models_for_class.nil?
50
+ raise NoModelsOfClassInStoreError, "No classes of type: #{clazz} in model store"
51
+ end
52
+
53
+ # Is there an instance
54
+ model = models_for_class[key]
55
+
56
+ if !model
57
+ raise NoModelOfClassWithKeyInStoreError, "No model of class #{clazz} with a key of #{key} in model store"
58
+ end
59
+
60
+ return model
61
+ end
62
+
63
+ def save_all
64
+ @store.each do |key, models|
65
+ models.each do |key, model|
66
+ model.save!
67
+ end
68
+ end
69
+ end
70
+
71
+ end
72
+
73
+ end
@@ -0,0 +1,3 @@
1
+ module DriveTime
2
+ VERSION = "0.0.3"
3
+ end
data/lib/drive_time.rb ADDED
@@ -0,0 +1,71 @@
1
+ require "drive_time/version"
2
+
3
+ require "log4r"
4
+ require 'google_drive'
5
+ require 'deep_end'
6
+ require 'active_support'
7
+ require 'active_support/inflector'
8
+ require 'active_support/core_ext/hash'
9
+ require 'active_support/core_ext/module'
10
+
11
+ require 'drive_time/model_store'
12
+ require 'drive_time/field_expander'
13
+ require 'drive_time/bi_directional_hash'
14
+ require 'drive_time/loader'
15
+ require 'drive_time/class_name_map'
16
+ require 'drive_time/builders/join_builder'
17
+ require 'drive_time/builders/name_builder'
18
+ require 'drive_time/converters/spreadsheets_converter'
19
+ require 'drive_time/converters/worksheet_converter'
20
+ require 'drive_time/logging'
21
+
22
+ module DriveTime
23
+
24
+ mattr_accessor :log_level
25
+
26
+ class MissingAssociationError < StandardError; end
27
+
28
+ include ActiveSupport::Inflector
29
+ include Log4r
30
+
31
+ # Set up logging
32
+ formatter = PatternFormatter.new(:pattern => "[%c] %M")
33
+ outputter = Outputter.stdout
34
+ outputter.formatter = formatter
35
+
36
+ @@log_level = INFO
37
+ # Create constants for loggers - available in inner classes
38
+ Logger = Log4r::Logger.new ' Primary '
39
+ Logger.level = @@log_level
40
+ Logger.outputters = outputter
41
+
42
+ # Store the mapping on the spreadsheets and worksheets
43
+ class GoogleDrive::Spreadsheet
44
+ attr_accessor :mapping
45
+ end
46
+
47
+ class GoogleDrive::Worksheet
48
+ attr_accessor :mapping
49
+ end
50
+
51
+ def self.underscore_from_text(text)
52
+ text.strip.downcase.parameterize('_')
53
+ end
54
+
55
+ def self.class_name_from_title(title)
56
+ self.underscore_from_text(title).classify
57
+ end
58
+
59
+ def self.is_affirmative?(value)
60
+ return false if !value
61
+ value.
62
+ strip.
63
+ downcase == 'yes' || value.downcase == 'y'
64
+ end
65
+
66
+ def self.log_level=(log_level)
67
+ @@log_level = log_level
68
+ Logger.level = log_level
69
+ end
70
+
71
+ end
@@ -0,0 +1,54 @@
1
+ require 'spec_helper'
2
+
3
+ module DriveTime
4
+
5
+ describe 'BiDirectionalHash' do
6
+
7
+ before(:each) do
8
+ @hash = BiDirectionalHash.new
9
+ end
10
+
11
+ it 'should correctly declare if a value exists for a key' do
12
+ @hash.insert('A', 1);
13
+ @hash.insert('B', 2);
14
+ @hash.insert(3, 'A');
15
+
16
+ @hash.has_value_for_key('A').should be_true
17
+ @hash.has_value_for_key('B').should be_true
18
+ @hash.has_value_for_key(3).should be_true
19
+ @hash.has_value_for_key('Nope').should be_false
20
+ end
21
+
22
+ it 'should correctly declare if a key exists for a value' do
23
+ @hash.insert('A', 1);
24
+ @hash.insert('B', 2);
25
+ @hash.insert(3, 'A');
26
+
27
+ @hash.has_key_for_value(1).should be_true
28
+ @hash.has_key_for_value(2).should be_true
29
+ @hash.has_key_for_value('A').should be_true
30
+ @hash.has_key_for_value('Nope').should be_false
31
+ end
32
+
33
+ it 'should allow values to be looked up using keys' do
34
+ @hash.insert('A', 1);
35
+ @hash.insert('B', 2);
36
+ @hash.insert(3, 'A');
37
+
38
+ @hash.value_for_key('A').should == 1
39
+ @hash.value_for_key('B').should == 2
40
+ @hash.value_for_key(3).should == 'A'
41
+ end
42
+
43
+ it 'should allow keys to be looked up using values' do
44
+ @hash.insert('A', 1);
45
+ @hash.insert('B', 2);
46
+ @hash.insert(3, 'A');
47
+
48
+ @hash.key_for_value(1).should == 'A'
49
+ @hash.key_for_value(2).should == 'B'
50
+ @hash.key_for_value('A').should == 3
51
+ end
52
+
53
+ end
54
+ end