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.
- data/.gitignore +61 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +240 -0
- data/Rakefile +7 -0
- data/drive_time.gemspec +30 -0
- data/lib/drive_time/bi_directional_hash.rb +33 -0
- data/lib/drive_time/builders/join_builder.rb +39 -0
- data/lib/drive_time/builders/name_builder.rb +19 -0
- data/lib/drive_time/class_name_map.rb +41 -0
- data/lib/drive_time/converters/spreadsheets_converter.rb +125 -0
- data/lib/drive_time/converters/worksheet_converter.rb +262 -0
- data/lib/drive_time/field_expander.rb +73 -0
- data/lib/drive_time/loader.rb +100 -0
- data/lib/drive_time/logging.rb +19 -0
- data/lib/drive_time/model_store.rb +73 -0
- data/lib/drive_time/version.rb +3 -0
- data/lib/drive_time.rb +71 -0
- data/spec/drive_time/bi_directional_hash_spec.rb +54 -0
- data/spec/drive_time/builder/join_builder_spec.rb +48 -0
- data/spec/drive_time/builder/name_builder_spec.rb +26 -0
- data/spec/drive_time/class_name_map_spec.rb +77 -0
- data/spec/drive_time/converters/worksheet_converter_spec.rb +64 -0
- data/spec/drive_time/field_expander_spec.rb +130 -0
- data/spec/drive_time/loader_spec.rb +111 -0
- data/spec/drive_time/model_store_spec.rb +52 -0
- data/spec/drive_time_spec.rb +44 -0
- data/spec/fixtures/mapping.yml +55 -0
- data/spec/full_tests.rb +48 -0
- data/spec/spec_helper.rb +6 -0
- metadata +233 -0
@@ -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
|
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
|