dgrid 0.0.2

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,47 @@
1
+ module Dgrid
2
+ module API
3
+ class Place < NamedEntity
4
+ attr_accessor :lat_long
5
+
6
+ option :name, String, :required # unique for this workspace
7
+ option :description, String # typically an address
8
+ option :lat_long, GPSCoordinates # When known. Description will be geocoded if this is not supplied.
9
+ option :latitude, String # alternative input method for db restoration
10
+ option :longitude, String
11
+ option :elevation, String
12
+ def initialize(options)
13
+ parent_opts, my_opts = split_hash(options,[:name, :description])
14
+ super(parent_opts)
15
+ gps_opts, other_opts = split_hash(options,[:latitude, :longitude, :elevation])
16
+ set_members_from_hash(other_opts)
17
+ if !other_opts.include?(:lat_long) && !gps_opts.empty?
18
+ self.lat_long = coalesce_gps_opts(gps_opts)
19
+ end
20
+ end
21
+
22
+
23
+ def self.db_fields
24
+ %w(id name description latitude longitude elevation)
25
+ end
26
+
27
+ def self.pluralized
28
+ 'places'
29
+ end
30
+
31
+ def to_hash
32
+ h = super
33
+ if lat_long
34
+ h.merge!({:latitude => lat_long.lat, :longitude => lat_long.long, :elevation => lat_long.elevation})
35
+ end
36
+ h
37
+ end
38
+
39
+ protected
40
+ def coalesce_gps_opts(gps_opts)
41
+ gps_opts[:elevation] ||= 0
42
+ gps_args = [:latitude,:longitude,:elevation].map {|k| gps_opts[k]}
43
+ GPSCoordinates.new(*gps_args)
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,567 @@
1
+ module Dgrid
2
+ module API
3
+ class Workspace
4
+ include ArgumentValidation
5
+ include SetMembersFromHash
6
+
7
+ attr_accessor :id, :name, :description, :connection
8
+
9
+ argument :connection, Dgrid::API::Connection
10
+ argument :name, String
11
+ argument :description, String
12
+ def initialize(connection, name='DEFAULT WORKSPACE', description = "")
13
+ @id = nil
14
+ @connection = connection
15
+ @name = name
16
+ @item_ids = {} # lens_id => [item_id]
17
+ @entity_cache = Cache.new
18
+ end
19
+
20
+ def self.pluralized
21
+ 'workspaces'
22
+ end
23
+
24
+ def self.singular
25
+ name.split('::').last.downcase
26
+ end
27
+
28
+ def type
29
+ self.class.name.split('::').last
30
+ end
31
+
32
+ argument :person, Dgrid::API::Person
33
+ def add_person(person)
34
+ add_entity(person)
35
+ end
36
+
37
+ argument :place, Dgrid::API::Place
38
+ def add_place(place)
39
+ add_entity(place)
40
+ end
41
+
42
+ argument :organization, Dgrid::API::Organization
43
+ def add_organization(organization)
44
+ add_entity(organization)
45
+ end
46
+
47
+ argument :item, Dgrid::API::Item
48
+ def add_item(item)
49
+ add_entity(item)
50
+ end
51
+
52
+ argument :incident, Dgrid::API::Incident
53
+ argument :item, Dgrid::API::Item
54
+ def add_incident(incident, item = nil)
55
+ if item
56
+ raise "Cannot add an Incident in a Workspace unless owning Item is already in the Workspace" unless item.in_workspace?(self)
57
+ raise "Cannot add an Incident to an unsaved Item" if item.new_record?
58
+ end
59
+ if incident.new_record?
60
+ incident = connection.create_entity(incident, self.id)
61
+ end
62
+ if !incident.in_workspace?(self)
63
+ incident.add_workspace(self)
64
+ end
65
+ if item
66
+ connection.subordinate_entity_to_other_entity_in_workspace(incident, item, self.id)
67
+ end
68
+ incident
69
+ end
70
+
71
+ argument :keyword, Dgrid::API::Keyword
72
+ def add_keyword(keyword)
73
+ add_entity(keyword)
74
+ end
75
+
76
+ def subordinate_entity_to_other_entity(entity, other)
77
+ connection.subordinate_entity_to_other_entity_in_workspace(entity, other, self.id)
78
+ @entity_cache.invalidate(Link)
79
+ end
80
+
81
+ #============
82
+
83
+ # TODO auto-generate this templatesque bullshit
84
+ def create_person(options = {})
85
+ person = add_person(Dgrid::API::Person.new(options))
86
+ person
87
+ end
88
+
89
+ def create_place(options = {})
90
+ place = add_place(Dgrid::API::Place.new(options))
91
+ place
92
+ end
93
+
94
+ def create_organization(options = {}, &block)
95
+ organization = add_organization(Dgrid::API::Organization.new(options))
96
+ yield organization if block_given?
97
+ organization
98
+ end
99
+
100
+ def create_item(options = {}, &block)
101
+ item = add_item(Dgrid::API::Item.new(options))
102
+ yield item if block_given?
103
+ item
104
+ end
105
+
106
+ argument :left_entity, Dgrid::API::Entity
107
+ argument :right_entity, Dgrid::API::Entity
108
+ option :link_type, String, :required
109
+ def link(left_entity, right_entity, options = {})
110
+ raise "Entity #{left_entity.to_s} is not in this workspace" unless left_entity.in_workspace?(self)
111
+ raise "Entity #{right_entity.to_s} is not in this workspace" unless right_entity.in_workspace?(self)
112
+ connection.create_link(left_entity, right_entity, self.id, options)
113
+ @entity_cache.invalidate(Link)
114
+ end
115
+
116
+
117
+ #============
118
+
119
+ def items
120
+ get_entities(Item)
121
+ end
122
+ def people
123
+ get_entities(Person)
124
+ end
125
+ def places
126
+ get_entities(Place)
127
+ end
128
+
129
+ def organizations
130
+ get_entities(Organization)
131
+ end
132
+
133
+ def links
134
+ get_entities(Link)
135
+ end
136
+
137
+ def lenses
138
+ get_entities(Lens)
139
+ end
140
+
141
+ def incidents
142
+ get_entities(Incident)
143
+ end
144
+
145
+ def keywords
146
+ get_entities(Keyword)
147
+ end
148
+
149
+ argument :lens, Dgrid::API::Lens
150
+ def item_ids_in_lens(lens)
151
+ item_ids = connection.get_items_in_lens(self.id, lens.id)
152
+ end
153
+
154
+ argument :lens, Dgrid::API::Item
155
+ def incident_ids_in_item(item)
156
+ incident_ids = connection.get_incidents_in_item(self.id, item.id)
157
+ end
158
+
159
+ def add_entity(entity)
160
+ if entity.new_record?
161
+ entity = connection.create_entity(entity,self.id)
162
+ end
163
+ if !entity.in_workspace?(self)
164
+ entity.add_workspace(self)
165
+ end
166
+ @entity_cache.invalidate(entity.class)
167
+ entity
168
+ end
169
+
170
+ def get_entities(klass)
171
+ pluralized_name = klass.pluralized
172
+ return @entity_cache.get(klass) if @entity_cache.include?(klass)
173
+ construction_fields = klass.db_fields
174
+ entities_params = connection.get_in_workspace(self.id, pluralized_name)
175
+ entities = []
176
+ entities_params.each do |entity_params|
177
+ construction_params, other_params = split_hash(entity_params, construction_fields)
178
+ entity = klass.new(change_string_keys_to_symbol_keys(construction_params))
179
+ entity.add_workspace(self)
180
+ entities << entity
181
+ end
182
+ @entity_cache.store(klass,entities)
183
+ end
184
+
185
+ def invalidate_cache(klass)
186
+ @entity_cache.invalidate(klass)
187
+ end
188
+
189
+ class Backup
190
+ include SetMembersFromHash
191
+
192
+ def initialize(workspace = nil)
193
+ raise "Backup is a pure virtual class. Use Backup.create instead of Backup.new" unless self.class != Backup
194
+ hashify_all_members(workspace) unless workspace.nil?
195
+ end
196
+
197
+ def self.current_valid_backup_class
198
+ # Backup_0_1
199
+ Backup_0_2
200
+ end
201
+
202
+ def current_valid_backup_class
203
+ self.class.current_valid_backup_class
204
+ end
205
+
206
+ def self.create(workspace)
207
+ self.current_valid_backup_class.new(workspace)
208
+ end
209
+
210
+ def self.entity_classes
211
+ raise "Pure Virtual self.entity_classes needs to be overridden in #{self.class.name}"
212
+ end
213
+
214
+ def entity_classes
215
+ self.class.entity_classes
216
+ end
217
+
218
+ def self.backed_up_classes
219
+ self.entity_classes
220
+ end
221
+
222
+ def backed_up_classes
223
+ self.class.backed_up_classes
224
+ end
225
+
226
+ def restore_into(workspace)
227
+ if self.class == current_valid_backup_class
228
+ do_restore(workspace)
229
+ else
230
+ restorable_backup = current_valid_backup_class.migrate(self)
231
+ restorable_backup.restore_into(workspace)
232
+ end
233
+ end
234
+
235
+ def self.migrate(backup_of_wrong_class)
236
+ raise "Cannot migrate anything to class #{self.name}" unless self.predecessor
237
+ backup_of_predecessor_class = (backup_of_wrong_class.class == self.predecessor) ? backup_of_wrong_class : self.predecessor.migrate(backup_of_wrong_class)
238
+ migrated = self.new
239
+ migrated.migrate_from_predecessor(backup_of_predecessor_class)
240
+ migrated
241
+ end
242
+
243
+ def migrate_from_predecessor(predecessor)
244
+ raise "Pure Virtual migrate_from_predecessor needs to be overridden in #{self.class.name}"
245
+ end
246
+
247
+
248
+ protected
249
+ def self.predecessor
250
+ raise "Pure Virtual self.predecessor needs to be overridden in #{self.class.name}"
251
+ end
252
+
253
+ def do_restore(workspace)
254
+ raise "Pure Virtual do_restore needs to be overridden in #{self.class.name}"
255
+ end
256
+
257
+ def set_entities(klass, obj_list)
258
+ raise "argumet_error: expected array of #{klass.name}" unless obj_list.is_a?(Array) && (obj_list.empty? || obj_list.first.is_a?(klass))
259
+ set_entities_hash(klass,hashify_obj_list(obj_list))
260
+ end
261
+
262
+ def get_entities_hash(klass)
263
+ raise "invalid member type #{type}" unless backed_up_classes.include?(klass)
264
+ @members[klass.pluralized]
265
+ end
266
+
267
+ def set_entities_hash(klass,h)
268
+ raise "invalid member type #{type}" unless backed_up_classes.include?(klass)
269
+ raise "invalid hash #{h}" unless h.is_a?(Hash) && h.values.all? {|v| v.is_a?(Hash)}
270
+ @members[klass.pluralized] = h
271
+ end
272
+
273
+ def lensings(lens_id)
274
+ @members['lensings'][lens_id]
275
+ end
276
+
277
+ def set_lensings(lens_id,item_ids)
278
+ @members['lensings'][lens_id] = item_ids
279
+ end
280
+
281
+ def hashify_obj_list(obj_list)
282
+ result = {}
283
+ obj_list.each do |obj|
284
+ result[obj.id] = obj.to_hash
285
+ end
286
+ result
287
+ end
288
+
289
+ private
290
+ def hashify_all_members(workspace)
291
+ raise "Pure Virtual hashify_all_members needs to be overridden in #{self.class.name}"
292
+ end
293
+
294
+ end
295
+
296
+ # ==============
297
+ #
298
+ class Backup_0_1 < Backup
299
+
300
+ def self.entity_classes
301
+ @@entity_classes ||= Set.new(
302
+ [Item,
303
+ Person,
304
+ Place,
305
+ Organization,
306
+ Link,
307
+ Lens,
308
+ ])
309
+ end
310
+
311
+ def self.backed_up_classes
312
+ self.entity_classes + [ Incident]
313
+ end
314
+
315
+ protected
316
+ def self.predecessor
317
+ nil
318
+ end
319
+
320
+ def do_restore(workspace)
321
+ @new_entities = {}
322
+ restore_entities_into(workspace)
323
+ restore_weird_special_cases_into(workspace)
324
+ restore_lenses_into(workspace)
325
+ restore_links_into(workspace)
326
+ end
327
+
328
+ def incidents_are_entities?
329
+ if @incidents_are_entities.nil?
330
+ @incidents_are_entities = entity_classes.include?(Incident)
331
+ end
332
+ return @incidents_are_entities
333
+ end
334
+
335
+ private
336
+
337
+ def incidents(item_id)
338
+ raise "The method incidents(item_id) is invalid in class #{self.class.name}" if incidents_are_entities?
339
+ @members[Incident.pluralized][item_id]
340
+ end
341
+
342
+ def set_incidents(item_id,incidents)
343
+ raise "The method set_incidents(item_id,incidents) is invalid in class #{self.class.name}" if incidents_are_entities?
344
+ raise "argument_error: expected String id" unless item_id.is_a?(String)
345
+ raise "argumet_error: expected array of Incident" unless incidents.is_a?(Array) && (incidents.empty? || incidents.first.is_a?(Dgrid::API::Incident))
346
+ @members[Incident.pluralized][item_id] = hashify_obj_list(incidents)
347
+ end
348
+
349
+ def hashify_all_members(workspace)
350
+ @members = {'lensings'=>{}}
351
+ @members[Incident.pluralized] = {} unless incidents_are_entities?
352
+ hashify_entities(workspace)
353
+ if !workspace.nil?
354
+ hashify_incidents(workspace) unless incidents_are_entities?
355
+ hashify_lensings(workspace)
356
+ end
357
+ end
358
+
359
+ def hashify_entities(workspace)
360
+ backed_up_classes.each do |klass|
361
+ if !get_entities_hash(klass)
362
+ set_entities(klass, workspace.nil? ? nil : workspace.send(klass.pluralized))
363
+ end
364
+ end
365
+ end
366
+
367
+ def hashify_incidents(workspace)
368
+ workspace.items.each do |item|
369
+ set_incidents(item.id,item.incidents)
370
+ end
371
+ end
372
+
373
+ def hashify_lensings(workspace)
374
+ workspace.lenses.each do |lens|
375
+ set_lensings(lens.id,lens.item_ids)
376
+ end
377
+ end
378
+
379
+ def restore_entities_into(workspace)
380
+ entity_classes.each do |entity_class|
381
+ next if entity_class.type == 'Link'
382
+ entity_hash = get_entities_hash(entity_class)
383
+ entity_hash.each do |old_id, entity_params|
384
+ new_entity = workspace.add_entity(entity_class.new(change_string_keys_to_symbol_keys(entity_params)))
385
+ entity_key =[entity_class.type,old_id]
386
+ @new_entities[entity_key] = new_entity
387
+ end
388
+ end
389
+ end
390
+
391
+ def restore_lenses_into(workspace)
392
+ get_entities_hash(Lens).each_pair do |old_lens_id, old_lens_params|
393
+ lens_key = [Lens.type, old_lens_id]
394
+ new_lens = @new_entities[lens_key]
395
+ raise "lens #{lens_key.inspect} not found in #{@new_entities.inspect}" unless new_lens
396
+
397
+ lensings(old_lens_id).each do |old_item_id|
398
+ item_key = [Item.type, old_item_id]
399
+ new_item = @new_entities[item_key]
400
+ raise "item #{item_key.inspect} not found in #{@new_entities.inspect}" unless new_item
401
+ new_lens.add_item(new_item)
402
+ end
403
+ end
404
+ end
405
+
406
+ def restore_links_into(workspace)
407
+ get_entities_hash(Link).each_pair do |old_link_id, old_link_params|
408
+ left_type = old_link_params[:left_type]
409
+ right_type = old_link_params[:right_type]
410
+ left_guid = old_link_params[:left_guid]
411
+ right_guid = old_link_params[:right_guid]
412
+ # Incident to Item links are covered during incident creation so we can skip those
413
+ new_left = @new_entities[[left_type,left_guid]]
414
+ new_right = @new_entities[[right_type,right_guid]]
415
+ if left_type == 'Incident' && right_type == 'Item'
416
+ if incidents_are_entities?
417
+ workspace.subordinate_entity_to_other_entity(new_left,new_right)
418
+ end
419
+ next
420
+ end
421
+ if right_type == 'Incident' && left_type == 'Item'
422
+ if incidents_are_entities?
423
+ workspace.subordinate_entity_to_other_entity(new_right,new_left)
424
+ end
425
+ next
426
+ end
427
+
428
+ STDERR.puts "Warning: Dangling link #{old_link_id}: #{old_link_params}" unless new_left && new_right
429
+ if new_left.is_a?(Dgrid::API::Incident) || (new_left.is_a?(Dgrid::API::Organization) && new_right.is_a?(Dgrid::API::Person))
430
+ workspace.subordinate_entity_to_other_entity(new_right,new_left)
431
+ elsif new_right.is_a?(Dgrid::API::Incident) || (new_right.is_a?(Dgrid::API::Organization) && new_left.is_a?(Dgrid::API::Person))
432
+ workspace.subordinate_entity_to_other_entity(new_left,new_right)
433
+ else
434
+ workspace.link(new_left,new_right, old_link_params[:description])
435
+ end
436
+ end
437
+ end
438
+
439
+ def restore_weird_special_cases_into(workspace)
440
+ get_entities_hash(Item).each_pair do |old_item_id, old_item_params|
441
+ item_key = [Item.type, old_item_id]
442
+ new_item = @new_entities[item_key]
443
+ raise "item #{item_key.inspect} not found in #{@new_entities.inspect}" unless new_item
444
+ incidents(old_item_id).each_pair do |old_incident_id, old_incident_params|
445
+ new_incident = workspace.add_incident(Incident.new(change_string_keys_to_symbol_keys(old_incident_params)), new_item)
446
+ incident_key =[Incident.type,old_incident_id]
447
+ @new_entities[incident_key] = new_incident
448
+ end
449
+ end
450
+ end
451
+
452
+ end # of class Backup_0_1
453
+
454
+ # ==============
455
+ #
456
+ class Backup_0_2 < Backup_0_1
457
+
458
+ def self.entity_classes
459
+ # This is the only real difference between versions 0_1 and 0_2
460
+ # Incident promoted to base-level entity and Keywords added.
461
+ @@entity_classes ||= predecessor.entity_classes + [Incident, Keyword]
462
+ end
463
+
464
+ def self.backed_up_classes
465
+ self.entity_classes
466
+ end
467
+
468
+ def migrate_from_predecessor(predecessor)
469
+ pred_entity_classes = Set.new(predecessor.class.entity_classes)
470
+ self.class.entity_classes.each do |entity_class|
471
+ if pred_entity_classes.include?(entity_class)
472
+ pred_entities = predecessor.get_entities_hash(entity_class)
473
+ set_entities_hash(my_entity_class,pred_entities.clone)
474
+ end
475
+ end
476
+ if self.incidents_are_entities? && !pred_entity_classes.include?(Incident)
477
+ migrate_item_subord_incidents_to_entity_incidents(predecessor)
478
+ end
479
+ end
480
+
481
+
482
+ protected
483
+
484
+ def self.predecessor
485
+ Backup_0_1
486
+ end
487
+
488
+ private
489
+ def restore_weird_special_cases_into(workspace)
490
+ # nothing to do, just overriding predecessor's implementation
491
+ end
492
+
493
+ def migrate_item_subord_incidents_to_entity_incidents(predecessor)
494
+ pred_item_ids = predecessor.get_entities_hash(Item).keys
495
+ incidents_hash = {}
496
+ links_hash = get_entities_hash(Link)
497
+ pred_item_ids.each do |item_id|
498
+ item_incidents = incidents(item_id)
499
+ item_incidents.each_pair do |incident_id,incident_params|
500
+ incidents_hash[incident_id] = incident_params
501
+ ensure_links_include_item_incident(links_hash, item_id,incident_id)
502
+ end
503
+ end
504
+ end
505
+
506
+ def ensure_links_include_item_incident(links_hash,item_id,incident_id)
507
+ if !links_include_relationship_between(links_hash,Item,item_id,Incident,incident_id)
508
+ new_link_id = generate_new_link_id
509
+ links_hash[new_link_id] = {
510
+ :left_type => Item.name,
511
+ :left_guid => item_id,
512
+ :right_type => Incident.guid,
513
+ :right_id => incident_id,
514
+ :description => 'item-incident'
515
+ }
516
+ end
517
+ end
518
+ def links_include_relationship_between(links_hash,type1,guid1,type2,guid2)
519
+ links_hash.values.any? { |link|
520
+ (link[:left_type] == type1 && link[:left_guid] == guid1 &&
521
+ link[:right_type] == type2 && link[:right_guid] == guid2) ||
522
+ (link[:right_type] == type1 && link[:right_guid] == guid1 &&
523
+ link[:left_type] == type2 && link[:left_guid] == guid2)
524
+ }
525
+ end
526
+ end # of class Backup_0_2
527
+
528
+ def backup()
529
+ backup = Backup.create(self)
530
+ end
531
+
532
+ argument :connection, Dgrid::API::Workspace
533
+ def clone_members_into(other_workspace)
534
+ backup().restore_into(other_workspace)
535
+ end
536
+
537
+
538
+ protected
539
+ class Cache
540
+ def initialize
541
+ @entries = {}
542
+ end
543
+
544
+ def include?(type)
545
+ @entries.include?(type)
546
+ end
547
+
548
+ def store(type,value)
549
+ @entries[type] = value
550
+ end
551
+
552
+ def get(type)
553
+ @entries[type]
554
+ end
555
+
556
+ def invalidate(type = nil)
557
+ if type.nil?
558
+ @entries = {}
559
+ else
560
+ @entries.delete(type)
561
+ end
562
+ end
563
+ end # class Cache
564
+
565
+ end
566
+ end
567
+ end