acts-as-dag 1.1.4 → 2.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -26,9 +26,15 @@ Yes, but I think they aren't as fast or feature filled. Flogic has a good simple
26
26
 
27
27
  == Requirements
28
28
 
29
- This uses named_scope so your going to need Rails 2.1 or above.
29
+ Version 1.x of Acts As Dag uses named_scope so your going to need Rails 2.1 or above.
30
30
 
31
- Currently tested using Rails 2.3.8 and Ruby 1.9.2-p0. Working on a separate version of acts-as-dag that is based on Rails 3.x and use all of the new ActiveRecord goodness found there.
31
+ Version 2.x of Acts As Dag uses validate and scope, so you are going to need Rails 3.0 or higher.
32
+
33
+ Version 1.1.1 tested using Rails 2.3.8 and Ruby 1.9.2-p0.
34
+
35
+ Version 2.0.0 tested using Rails 3.0.0 and Ruby 1.9.2-p0, without major structural changes to code from the Rails 3 framework. Removes all deprecation warnings I have received while using AAD and Rails 3.0.0.
36
+
37
+ Version 2.1.x (in development) will be based on Rails 3.0.0 and attempt to use all of the new ActiveRecord goodness found there.
32
38
 
33
39
  == Installation
34
40
 
@@ -278,5 +284,5 @@ Authors:: Matthew Leventi, Robert Schmitt
278
284
 
279
285
  Algorithm and plugin designed by Matthew Leventi Email:(first letter of my first name followed by my last name @gmail.com). Im open to questions, very open to bugs, and even more receptive of bug fixes. I am also currently (August 2008) looking for a job just having graduated University of Rochester.
280
286
 
281
- Robert Schmitt: I have modified the Rake system to support Jeweler to build this as a Ruby gem that can be installed and managed through the gem system.
287
+ Robert Schmitt: I have modified the Rake system to support Jeweler to build this as a Ruby gem that can be installed and managed through the gem system. Updated to work without deprecation warnings from Rails 3.0.0.
282
288
 
@@ -0,0 +1,803 @@
1
+ module ActiveRecord
2
+ module Acts
3
+ module Dag
4
+ def self.included(base)
5
+ base.extend(SingletonMethods)
6
+ end
7
+
8
+ module SingletonMethods
9
+ #Sets up a model to act as dag links for models specified under the :for option
10
+ def acts_as_dag_links(options = {})
11
+ conf = {
12
+ :ancestor_id_column => 'ancestor_id',
13
+ :ancestor_type_column => 'ancestor_type',
14
+ :descendant_id_column => 'descendant_id',
15
+ :descendant_type_column => 'descendant_type',
16
+ :direct_column => 'direct',
17
+ :count_column => 'count',
18
+ :polymorphic => false,
19
+ :node_class_name => nil}
20
+ conf.update(options)
21
+
22
+ unless conf[:polymorphic]
23
+ if conf[:node_class_name].nil?
24
+ raise ActiveRecord::ActiveRecordError, 'Non-polymorphic graphs need to specify :node_class_name with the receiving class like belong_to'
25
+ end
26
+ end
27
+
28
+ write_inheritable_attribute :acts_as_dag_options, conf
29
+ class_inheritable_reader :acts_as_dag_options
30
+
31
+ extend Columns
32
+ include Columns
33
+
34
+ #access to _changed? and _was for (edge,count) if not default
35
+ unless direct_column_name == 'direct'
36
+ module_eval <<-"end_eval", __FILE__, __LINE__
37
+ def direct_changed?
38
+ self.#{direct_column_name}_changed?
39
+ end
40
+ def direct_was
41
+ self.#{direct_column_name}_was
42
+ end
43
+ end_eval
44
+ end
45
+
46
+ unless count_column_name == 'count'
47
+ module_eval <<-"end_eval", __FILE__, __LINE__
48
+ def count_changed?
49
+ self.#{count_column_name}_changed?
50
+ end
51
+ def count_was
52
+ self.#{count_column_name}_was
53
+ end
54
+ end_eval
55
+ end
56
+
57
+ internal_columns = [ancestor_id_column_name, descendant_id_column_name]
58
+ edge_class_name = self.to_s
59
+
60
+ direct_column_name.intern
61
+ count_column_name.intern
62
+
63
+ #links to ancestor and descendant
64
+ if acts_as_dag_polymorphic?
65
+ extend PolyColumns
66
+ include PolyColumns
67
+
68
+ internal_columns << ancestor_type_column_name
69
+ internal_columns << descendant_type_column_name
70
+
71
+ belongs_to :ancestor, :polymorphic => true
72
+ belongs_to :descendant, :polymorphic => true
73
+
74
+ validates_presence_of ancestor_type_column_name, descendant_type_column_name
75
+ validates_uniqueness_of ancestor_id_column_name, :scope => [ancestor_type_column_name, descendant_type_column_name, descendant_id_column_name]
76
+
77
+ scope :with_ancestor, lambda { |ancestor| {:conditions => {ancestor_id_column_name => ancestor.id, ancestor_type_column_name => ancestor.class.to_s}} }
78
+ scope :with_descendant, lambda { |descendant| {:conditions => {descendant_id_column_name => descendant.id, descendant_type_column_name => descendant.class.to_s}} }
79
+
80
+ scope :with_ancestor_point, lambda { |point| {:conditions => {ancestor_id_column_name => point.id, ancestor_type_column_name => point.type}} }
81
+ scope :with_descendant_point, lambda { |point| {:conditions => {descendant_id_column_name => point.id, descendant_type_column_name => point.type}} }
82
+
83
+ extend PolyEdgeClassMethods
84
+ include PolyEdgeClasses
85
+ include PolyEdgeInstanceMethods
86
+ else
87
+ belongs_to :ancestor, :foreign_key => ancestor_id_column_name, :class_name => acts_as_dag_options[:node_class_name]
88
+ belongs_to :descendant, :foreign_key => descendant_id_column_name, :class_name => acts_as_dag_options[:node_class_name]
89
+
90
+ validates_uniqueness_of ancestor_id_column_name, :scope => [descendant_id_column_name]
91
+
92
+ scope :with_ancestor, lambda { |ancestor| {:conditions => {ancestor_id_column_name => ancestor.id}} }
93
+ scope :with_descendant, lambda { |descendant| {:conditions => {descendant_id_column_name => descendant.id}} }
94
+
95
+ scope :with_ancestor_point, lambda { |point| {:conditions => {ancestor_id_column_name => point.id}} }
96
+ scope :with_descendant_point, lambda { |point| {:conditions => {descendant_id_column_name => point.id}} }
97
+
98
+ extend NonPolyEdgeClassMethods
99
+ include NonPolyEdgeClasses
100
+ include NonPolyEdgeInstanceMethods
101
+ end
102
+
103
+ scope :direct, :conditions => {:direct => true}
104
+ scope :indirect, :conditions => {:direct => false}
105
+
106
+ scope :ancestor_nodes, :joins => :ancestor
107
+ scope :descendant_nodes, :joins => :descendant
108
+
109
+ validates_presence_of ancestor_id_column_name, descendant_id_column_name
110
+ validates_numericality_of ancestor_id_column_name, descendant_id_column_name
111
+
112
+ extend EdgeClassMethods
113
+ include EdgeInstanceMethods
114
+
115
+ before_destroy :destroyable!, :perpetuate
116
+ before_save :perpetuate
117
+ before_validation :field_check, :fill_defaults, :on => :update
118
+ before_validation :fill_defaults, :on => :create
119
+ validate :create_validation, :on => :create
120
+ validate :update_validation, :on => :update
121
+
122
+
123
+ #internal fields
124
+ code = 'def field_check ' + "\n"
125
+ internal_columns.each do |column|
126
+ code += "if " + column + "_changed? \n" + ' raise ActiveRecord::ActiveRecordError, "Column: '+column+' cannot be changed for an existing record it is immutable"' + "\n end \n"
127
+ end
128
+ code += 'end'
129
+ module_eval code
130
+
131
+ [count_column_name].each do |column|
132
+ module_eval <<-"end_eval", __FILE__, __LINE__
133
+ def #{column}=(x)
134
+ raise ActiveRecord::ActiveRecordError, "Unauthorized assignment to #{column}: it's an internal field handled by acts_as_dag code."
135
+ end
136
+ end_eval
137
+ end
138
+ end
139
+
140
+ def has_dag_links(options = {})
141
+ conf = {
142
+ :class_name => nil,
143
+ :prefix => '',
144
+ :ancestor_class_names => [],
145
+ :descendant_class_names => []
146
+ }
147
+ conf.update(options)
148
+
149
+ #check that class_name is filled
150
+ if conf[:link_class_name].nil?
151
+ raise ActiveRecord::ActiveRecordError, "has_dag must be provided with :link_class_name option"
152
+ end
153
+
154
+ #add trailing '_' to prefix
155
+ unless conf[:prefix] == ''
156
+ conf[:prefix] += '_'
157
+ end
158
+
159
+ prefix = conf[:prefix]
160
+ dag_link_class_name = conf[:link_class_name]
161
+ dag_link_class = conf[:link_class_name].constantize
162
+
163
+ if dag_link_class.acts_as_dag_polymorphic?
164
+ self.class_eval <<-EOL
165
+ has_many :#{prefix}links_as_ancestor, :as => :ancestor, :class_name => '#{dag_link_class_name}'
166
+ has_many :#{prefix}links_as_descendant, :as => :descendant, :class_name => '#{dag_link_class_name}'
167
+
168
+ has_many :#{prefix}links_as_parent, :as => :ancestor, :class_name => '#{dag_link_class_name}', :conditions => {'#{dag_link_class.direct_column_name}' => true}
169
+ has_many :#{prefix}links_as_child, :as => :descendant, :class_name => '#{dag_link_class_name}', :conditions => {'#{dag_link_class.direct_column_name}' => true}
170
+
171
+ EOL
172
+
173
+ ancestor_table_names = []
174
+ parent_table_names = []
175
+ conf[:ancestor_class_names].each do |class_name|
176
+ table_name = class_name.tableize
177
+ self.class_eval <<-EOL2
178
+ has_many :#{prefix}links_as_descendant_for_#{table_name}, :as => :descendant, :class_name => '#{dag_link_class_name}', :conditions => {'#{dag_link_class.ancestor_type_column_name}' => '#{class_name}'}
179
+ has_many :#{prefix}ancestor_#{table_name}, :through => :#{prefix}links_as_descendant_for_#{table_name}, :source => :ancestor, :source_type => '#{class_name}'
180
+ has_many :#{prefix}links_as_child_for_#{table_name}, :as => :descendant, :class_name => '#{dag_link_class_name}', :conditions => {'#{dag_link_class.ancestor_type_column_name}' => '#{class_name}','#{dag_link_class.direct_column_name}' => true}
181
+ has_many :#{prefix}parent_#{table_name}, :through => :#{prefix}links_as_child_for_#{table_name}, :source => :ancestor, :source_type => '#{class_name}'
182
+
183
+ def #{prefix}root_for_#{table_name}?
184
+ return self.links_as_descendant_for_#{table_name}.empty?
185
+ end
186
+ EOL2
187
+ ancestor_table_names << (prefix+'ancestor_'+table_name)
188
+ parent_table_names << (prefix+'parent_'+table_name)
189
+ unless conf[:descendant_class_names].include?(class_name)
190
+ #this apparently is only one way is we can create some aliases making things easier
191
+ self.class_eval "has_many :#{prefix}#{table_name}, :through => :#{prefix}links_as_descendant_for_#{table_name}, :source => :ancestor, :source_type => '#{class_name}'"
192
+ end
193
+ end
194
+
195
+ unless conf[:ancestor_class_names].empty?
196
+ self.class_eval <<-EOL25
197
+ def #{prefix}ancestors
198
+ return #{ancestor_table_names.join(' + ')}
199
+ end
200
+ def #{prefix}parents
201
+ return #{parent_table_names.join(' + ')}
202
+ end
203
+ EOL25
204
+ else
205
+ self.class_eval <<-EOL26
206
+ def #{prefix}ancestors
207
+ a = []
208
+ #{prefix}links_as_descendant.each do |link|
209
+ a << link.ancestor
210
+ end
211
+ return a
212
+ end
213
+ def #{prefix}parents
214
+ a = []
215
+ #{prefix}links_as_child.each do |link|
216
+ a << link.ancestor
217
+ end
218
+ return a
219
+ end
220
+ EOL26
221
+ end
222
+
223
+ descendant_table_names = []
224
+ child_table_names = []
225
+ conf[:descendant_class_names].each do |class_name|
226
+ table_name = class_name.tableize
227
+ self.class_eval <<-EOL3
228
+ has_many :#{prefix}links_as_ancestor_for_#{table_name}, :as => :ancestor, :class_name => '#{dag_link_class_name}', :conditions => {'#{dag_link_class.descendant_type_column_name}' => '#{class_name}'}
229
+ has_many :#{prefix}descendant_#{table_name}, :through => :#{prefix}links_as_ancestor_for_#{table_name}, :source => :descendant, :source_type => '#{class_name}'
230
+
231
+ has_many :#{prefix}links_as_parent_for_#{table_name}, :as => :ancestor, :class_name => '#{dag_link_class_name}', :conditions => {'#{dag_link_class.descendant_type_column_name}' => '#{class_name}','#{dag_link_class.direct_column_name}' => true}
232
+ has_many :#{prefix}child_#{table_name}, :through => :#{prefix}links_as_parent_for_#{table_name}, :source => :descendant, :source_type => '#{class_name}'
233
+
234
+ def #{prefix}leaf_for_#{table_name}?
235
+ return self.links_as_ancestor_for_#{table_name}.empty?
236
+ end
237
+ EOL3
238
+ descendant_table_names << (prefix+'descendant_'+table_name)
239
+ child_table_names << (prefix+'child_'+table_name)
240
+ unless conf[:ancestor_class_names].include?(class_name)
241
+ self.class_eval "has_many :#{prefix}#{table_name}, :through => :#{prefix}links_as_ancestor_for_#{table_name}, :source => :descendant, :source_type => '#{class_name}'"
242
+ end
243
+ end
244
+
245
+ unless conf[:descendant_class_names].empty?
246
+ self.class_eval <<-EOL35
247
+ def #{prefix}descendants
248
+ return #{descendant_table_names.join(' + ')}
249
+ end
250
+ def #{prefix}children
251
+ return #{child_table_names.join(' + ')}
252
+ end
253
+ EOL35
254
+ else
255
+ self.class_eval <<-EOL36
256
+ def #{prefix}descendants
257
+ d = []
258
+ #{prefix}links_as_ancestor.each do |link|
259
+ d << link.descendant
260
+ end
261
+ return d
262
+ end
263
+ def #{prefix}children
264
+ d = []
265
+ #{prefix}links_as_parent.each do |link|
266
+ d << link.descendant
267
+ end
268
+ return d
269
+ end
270
+ EOL36
271
+ end
272
+ else
273
+ self.class_eval <<-EOL4
274
+ has_many :#{prefix}links_as_ancestor, :foreign_key => '#{dag_link_class.ancestor_id_column_name}', :class_name => '#{dag_link_class_name}'
275
+ has_many :#{prefix}links_as_descendant, :foreign_key => '#{dag_link_class.descendant_id_column_name}', :class_name => '#{dag_link_class_name}'
276
+
277
+ has_many :#{prefix}ancestors, :through => :#{prefix}links_as_descendant, :source => :ancestor
278
+ has_many :#{prefix}descendants, :through => :#{prefix}links_as_ancestor, :source => :descendant
279
+
280
+ has_many :#{prefix}links_as_parent, :foreign_key => '#{dag_link_class.ancestor_id_column_name}', :class_name => '#{dag_link_class_name}', :conditions => {'#{dag_link_class.direct_column_name}' => true}
281
+ has_many :#{prefix}links_as_child, :foreign_key => '#{dag_link_class.descendant_id_column_name}', :class_name => '#{dag_link_class_name}', :conditions => {'#{dag_link_class.direct_column_name}' => true}
282
+
283
+ has_many :#{prefix}parents, :through => :#{prefix}links_as_child, :source => :ancestor
284
+ has_many :#{prefix}children, :through => :#{prefix}links_as_parent, :source => :descendant
285
+
286
+ EOL4
287
+ end
288
+ self.class_eval <<-EOL5
289
+ def #{prefix}leaf?
290
+ return self.#{prefix}links_as_ancestor.empty?
291
+ end
292
+ def #{prefix}root?
293
+ return self.#{prefix}links_as_descendant.empty?
294
+ end
295
+ EOL5
296
+ end
297
+ end
298
+
299
+
300
+ #Methods that show the columns for polymorphic DAGs
301
+ module PolyColumns
302
+ def ancestor_type_column_name
303
+ acts_as_dag_options[:ancestor_type_column]
304
+ end
305
+
306
+ def descendant_type_column_name
307
+ acts_as_dag_options[:descendant_type_column]
308
+ end
309
+ end
310
+
311
+ #Methods that show columns
312
+ module Columns
313
+ def ancestor_id_column_name
314
+ acts_as_dag_options[:ancestor_id_column]
315
+ end
316
+
317
+ def descendant_id_column_name
318
+ acts_as_dag_options[:descendant_id_column]
319
+ end
320
+
321
+ def direct_column_name
322
+ acts_as_dag_options[:direct_column]
323
+ end
324
+
325
+ def count_column_name
326
+ acts_as_dag_options[:count_column]
327
+ end
328
+
329
+ def acts_as_dag_polymorphic?
330
+ acts_as_dag_options[:polymorphic]
331
+ end
332
+ end
333
+
334
+ #Contains class methods that extend the link model for polymorphic DAGs
335
+ module PolyEdgeClassMethods
336
+ #Builds a hash that describes a link from a source and a sink
337
+ def conditions_for(source, sink)
338
+ {
339
+ ancestor_id_column_name => source.id,
340
+ ancestor_type_column_name => source.type,
341
+ descendant_id_column_name => sink.id,
342
+ descendant_type_column_name => sink.type
343
+ }
344
+ end
345
+ end
346
+ #Contains nested classes in the link model for polymorphic DAGs
347
+ module PolyEdgeClasses
348
+ #Encapsulates the necessary information about a graph node
349
+ class EndPoint
350
+ #Does the endpoint match a model or another endpoint
351
+ def matches?(other)
352
+ return (self.id == other.id) && (self.type == other.type) if other.is_a?(EndPoint)
353
+ return (self.id == other.id) && (self.type == other.class.to_s)
354
+ end
355
+
356
+ #Factory Construction method that creates an EndPoint instance from a model
357
+ def self.from_resource(resource)
358
+ self.new(resource.id, resource.class.to_s)
359
+ end
360
+
361
+ #Factory Construction method that creates an EndPoint instance from a model if necessary
362
+ def self.from(obj)
363
+ return obj if obj.kind_of?(EndPoint)
364
+ return self.from_resource(obj)
365
+ end
366
+
367
+ #Initializes the EndPoint instance with an id and type
368
+ def initialize(id, type)
369
+ @id = id
370
+ @type = type
371
+ end
372
+
373
+ attr_reader :id, :type
374
+ end
375
+
376
+ #Encapsulates information about the source of a link
377
+ class Source < EndPoint
378
+ #Factory Construction method that generates a source from a link
379
+ def self.from_edge(edge)
380
+ self.new(edge.ancestor_id, edge.ancestor_type)
381
+ end
382
+ end
383
+
384
+ #Encapsulates information about the sink (destination) of a link
385
+ class Sink < EndPoint
386
+ #Factory Construction method that generates a sink from a link
387
+ def self.from_edge(edge)
388
+ self.new(edge.descendant_id, edge.descendant_type)
389
+ end
390
+ end
391
+ end
392
+
393
+ #Contains class methods that extend the link model for a nonpolymorphic DAG
394
+ module NonPolyEdgeClassMethods
395
+ #Builds a hash that describes a link from a source and a sink
396
+ def conditions_for(source, sink)
397
+ {
398
+ ancestor_id_column_name => source.id,
399
+ descendant_id_column_name => sink.id
400
+ }
401
+ end
402
+ end
403
+ #Contains nested classes in the link model for a nonpolymorphic DAG
404
+ module NonPolyEdgeClasses
405
+ #Encapsulates the necessary information about a graph node
406
+ class EndPoint
407
+ #Does an endpoint match another endpoint or model instance
408
+ def matches?(other)
409
+ return (self.id == other.id)
410
+ end
411
+
412
+ #Factory Construction method that creates an endpoint from a model
413
+ def self.from_resource(resource)
414
+ self.new(resource.id)
415
+ end
416
+
417
+ #Factory Construction method that creates an endpoint from a model if necessary
418
+ def self.from(obj)
419
+ return obj if obj.kind_of?(EndPoint)
420
+ return self.from_resource(obj)
421
+ end
422
+
423
+ #Initializes an endpoint based on an Id
424
+ def initialize(id)
425
+ @id = id
426
+ end
427
+
428
+ attr_reader :id
429
+ end
430
+
431
+ #Encapsulates information about the source of a link
432
+ class Source < EndPoint
433
+ #Factory Construction method creates a source instance from a link
434
+ def self.from_edge(edge)
435
+ return self.new(edge.ancestor_id)
436
+ end
437
+ end
438
+ #Encapsulates information about the sink of a link
439
+ class Sink < EndPoint
440
+ #Factory Construction method creates a sink instance from a link
441
+ def self.from_edge(edge)
442
+ return self.new(edge.descendant_id)
443
+ end
444
+ end
445
+ end
446
+
447
+ #Class methods that extend the link model for both polymorphic and nonpolymorphic graphs
448
+ module EdgeClassMethods
449
+
450
+ #Returns a new edge between two points
451
+ def build_edge(ancestor, descendant)
452
+ source = self::EndPoint.from(ancestor)
453
+ sink = self::EndPoint.from(descendant)
454
+ conditions = self.conditions_for(source, sink)
455
+ path = self.new(conditions)
456
+ path.make_direct
457
+ return path
458
+ end
459
+
460
+ #Finds an edge between two points, Must be direct
461
+ def find_edge(ancestor, descendant)
462
+ source = self::EndPoint.from(ancestor)
463
+ sink = self::EndPoint.from(descendant)
464
+ edge = self.find(:first, :conditions => self.conditions_for(source, sink).merge!({direct_column_name => true}))
465
+ return edge
466
+ end
467
+
468
+ #Finds a link between two points
469
+ def find_link(ancestor, descendant)
470
+ source = self::EndPoint.from(ancestor)
471
+ sink = self::EndPoint.from(descendant)
472
+ link = self.find(:first, :conditions => self.conditions_for(source, sink))
473
+ return link
474
+ end
475
+
476
+ #Finds or builds an edge between two points
477
+ def find_or_build_edge(ancestor, descendant)
478
+ edge = self.find_edge(ancestor, descendant)
479
+ return edge unless edge.nil?
480
+ return build_edge(ancestor, descendant)
481
+ end
482
+
483
+ #Creates an edge between two points using save
484
+ def create_edge(ancestor, descendant)
485
+ link = self.find_link(ancestor, descendant)
486
+ if link.nil?
487
+ edge = self.build_edge(ancestor, descendant)
488
+ return edge.save
489
+ else
490
+ link.make_direct
491
+ return link.save
492
+ end
493
+ end
494
+
495
+ #Creates an edge between two points using save! Returns created edge
496
+ def create_edge!(ancestor, descendant)
497
+ link = self.find_link(ancestor, descendant)
498
+ if link.nil?
499
+ edge = self.build_edge(ancestor, descendant)
500
+ edge.save!
501
+ return edge
502
+ else
503
+ link.make_direct
504
+ link.save!
505
+ return link
506
+ end
507
+ end
508
+
509
+ #Alias for create_edge
510
+ def connect(ancestor, descendant)
511
+ return self.create_edge(ancestor, descendant)
512
+ end
513
+
514
+ #Alias for create_edge!
515
+ def connect!(ancestor, descendant)
516
+ return self.create_edge!(ancestor, descendant)
517
+ end
518
+
519
+ #Determines if a link exists between two points
520
+ def connected?(ancestor, descendant)
521
+ return !self.find_link(ancestor, descendant).nil?
522
+ end
523
+
524
+ #Finds the longest path between ancestor and descendant returning as an array
525
+ def longest_path_between(ancestor, descendant, path=[])
526
+ longest = []
527
+ ancestor.children.each do |child|
528
+ if child == descendent
529
+ temp = path.clone
530
+ temp << child
531
+ if temp.length > longest.length
532
+ longest = temp
533
+ end
534
+ elsif self.connected?(child, descendant)
535
+ temp = path.clone
536
+ temp << child
537
+ temp = self.longest_path_between(child, descendant, temp)
538
+ if temp.length > longest.length
539
+ longest = temp
540
+ end
541
+ end
542
+ end
543
+ return longest
544
+ end
545
+
546
+ #Determines if an edge exists between two points
547
+ def edge?(ancestor, descendant)
548
+ return !self.find_edge(ancestor, descendant).nil?
549
+ end
550
+
551
+ #Alias for edge
552
+ def direct?(ancestor, descendant)
553
+ return self.edge?(ancestor, descendant)
554
+ end
555
+ end
556
+
557
+ #Instance methods included into link model for a polymorphic DAG
558
+ module PolyEdgeInstanceMethods
559
+ def ancestor_type
560
+ return self[ancestor_type_column_name]
561
+ end
562
+
563
+ def descendant_type
564
+ return self[descendant_type_column_name]
565
+ end
566
+ end
567
+
568
+ #Instance methods included into the link model for a non-polymorphic DAG
569
+ module NonPolyEdgeInstanceMethods
570
+ end
571
+
572
+ #Instance methods included into the link model for polymorphic and non-polymorphic DAGs
573
+ module EdgeInstanceMethods
574
+
575
+ attr_accessor :do_not_perpetuate
576
+
577
+ #Validations on model instance creation. Ensures no duplicate links, no cycles, and correct count and direct attributes
578
+ def create_validation
579
+ #make sure no duplicates
580
+ if self.class.find_link(self.source, self.sink)
581
+ self.errors.add(:base, 'Link already exists between these points')
582
+ end
583
+ #make sure no long cycles
584
+ if self.class.find_link(self.sink, self.source)
585
+ self.errors.add(:base, 'Link already exists in the opposite direction')
586
+ end
587
+ #make sure no short cycles
588
+ if self.sink.matches?(self.source)
589
+ self.errors.add(:base, 'Link must start and end in different places')
590
+ end
591
+ #make sure not impossible
592
+ if self.direct?
593
+ if self.count != 0
594
+ self.errors.add(:base, 'Cannot create a direct link with a count other than 0')
595
+ end
596
+ else
597
+ if self.count < 1
598
+ self.errors.add(:base, 'Cannot create an indirect link with a count less than 1')
599
+ end
600
+ end
601
+ end
602
+
603
+ #Validations on update. Makes sure that something changed, that not making a lonely link indirect, and count is correct.
604
+ def update_validation #validate_on_update
605
+ unless self.changed?
606
+ self.errors.add(:base, 'No changes')
607
+ end
608
+ if direct_changed?
609
+ if count_changed?
610
+ self.errors.add(:base, 'Do not manually change the count value')
611
+ end
612
+ if !self.direct?
613
+ if self.count == 1
614
+ self.errors.add(:base, 'Cannot make a direct link with count 1 indirect')
615
+ end
616
+ end
617
+ end
618
+ end
619
+
620
+ #Fill default direct and count values if necessary. In place of after_initialize method
621
+ def fill_defaults
622
+ self[direct_column_name] = true if self[direct_column_name].nil?
623
+ self[count_column_name] = 0 if self[count_column_name].nil?
624
+ end
625
+
626
+ #Whether the edge can be destroyed
627
+ def destroyable?
628
+ (self.count == 0) || (self.direct? && self.count == 1)
629
+ end
630
+
631
+ #Raises an exception if the edge is not destroyable. Otherwise makes the edge indirect before destruction to cleanup graph.
632
+ def destroyable!
633
+ raise ActiveRecord::ActiveRecordError, 'Cannot destroy this edge' unless destroyable?
634
+ #this triggers rewiring on destruction via perpetuate
635
+ if self.direct?
636
+ self[direct_column_name] = false
637
+ end
638
+ return true
639
+ end
640
+
641
+ #Analyzes the changes in a model instance and rewires as necessary.
642
+ def perpetuate
643
+ #flag set by links that were modified in association
644
+ return true if self.do_not_perpetuate
645
+
646
+ #if edge changed this was manually altered
647
+ if direct_changed?
648
+ if self.direct?
649
+ self[count_column_name] += 1
650
+ else
651
+ self[count_column_name] -= 1
652
+ end
653
+ self.wiring
654
+ end
655
+ end
656
+
657
+ #Id of the ancestor
658
+ def ancestor_id
659
+ return self[ancestor_id_column_name]
660
+ end
661
+
662
+ #Id of the descendant
663
+ def descendant_id
664
+ return self[descendant_id_column_name]
665
+ end
666
+
667
+ #Count of the edge, ie the edge exists in X ways
668
+ def count
669
+ return self[count_column_name]
670
+ end
671
+
672
+ #Changes the count of the edge. DO NOT CALL THIS OUTSIDE THE PLUGIN
673
+ def internal_count=(val)
674
+ self[count_column_name] = val
675
+ end
676
+
677
+ #Whether the link is direct, ie manually created
678
+ def direct?
679
+ return self[direct_column_name]
680
+ end
681
+
682
+ #Whether the link is an edge?
683
+ def edge?
684
+ return self[direct_column_name]
685
+ end
686
+
687
+ #Makes the link direct, ie an edge
688
+ def make_direct
689
+ self[direct_column_name] = true
690
+ end
691
+
692
+ #Makes an edge indirect, ie a link.
693
+ def make_indirect
694
+ self[direct_column_name] = false
695
+ end
696
+
697
+ #Source of the edge, creates if necessary
698
+ def source
699
+ @source = self.class::Source.from_edge(self) if @source.nil?
700
+ return @source
701
+ end
702
+
703
+ #Sink (destination) of the edge, creates if necessary
704
+ def sink
705
+ @sink = self.class::Sink.from_edge(self) if @sink.nil?
706
+ return @sink
707
+ end
708
+
709
+ #All links that end at the source
710
+ def links_to_source
711
+ self.class.with_descendant_point(self.source)
712
+ end
713
+
714
+ #all links that start from the sink
715
+ def links_from_sink
716
+ self.class.with_ancestor_point(self.sink)
717
+ end
718
+
719
+ protected
720
+
721
+ #Changes on a wire based on the count (destroy! or save!) (should not be called outside this plugin)
722
+ def push_associated_modification!(edge)
723
+ raise ActiveRecord::ActiveRecordError, 'Cannot modify ourself in this way' if edge == self
724
+ edge.do_not_perpetuate = true
725
+ if edge.count == 0
726
+ edge.destroy!
727
+ else
728
+ edge.save!
729
+ end
730
+ end
731
+
732
+ #Updates the wiring of edges that dependent on the current one
733
+ def rewire_crossing(above_leg, below_leg)
734
+ if above_leg.count_changed?
735
+ was = above_leg.count_was
736
+ was = 0 if was.nil?
737
+ above_leg_count = above_leg.count - was
738
+ if below_leg.count_changed?
739
+ raise ActiveRecord::ActiveRecordError, 'ERROR: both legs cannot 0 normal count change'
740
+ else
741
+ below_leg_count = below_leg.count
742
+ end
743
+ else
744
+ above_leg_count = above_leg.count
745
+ if below_leg.count_changed?
746
+ was = below_leg.count_was
747
+ was = 0 if was.nil?
748
+ below_leg_count = below_leg.count - was
749
+ else
750
+ raise ActiveRecord::ActiveRecordError, 'ERROR: both legs cannot have count changes'
751
+ end
752
+ end
753
+ count = above_leg_count * below_leg_count
754
+ source = above_leg.source
755
+ sink = below_leg.sink
756
+ bridging_leg = self.class.find_link(source, sink)
757
+ if bridging_leg.nil?
758
+ bridging_leg = self.class.new(self.class.conditions_for(source, sink))
759
+ bridging_leg.make_indirect
760
+ bridging_leg.internal_count = 0
761
+ end
762
+ bridging_leg.internal_count = bridging_leg.count + count
763
+ return bridging_leg
764
+ end
765
+
766
+ #Find the edges that need to be updated
767
+ def wiring
768
+ source = self.source
769
+ sink = self.sink
770
+ above_sources = []
771
+ self.links_to_source.each do |edge|
772
+ above_sources << edge.source
773
+ end
774
+ below_sinks = []
775
+ self.links_from_sink.each do |edge|
776
+ below_sinks << edge.sink
777
+ end
778
+ above_bridging_legs = []
779
+ #everything above me tied to my sink
780
+ above_sources.each do |above_source|
781
+ above_leg = self.class.find_link(above_source, source)
782
+ above_bridging_leg = self.rewire_crossing(above_leg, self)
783
+ above_bridging_legs << above_bridging_leg unless above_bridging_leg.nil?
784
+ end
785
+
786
+ #everything beneath me tied to my source
787
+ below_sinks.each do |below_sink|
788
+ below_leg = self.class.find_link(sink, below_sink)
789
+ below_bridging_leg = self.rewire_crossing(self, below_leg)
790
+ self.push_associated_modification!(below_bridging_leg)
791
+ above_bridging_legs.each do |above_bridging_leg|
792
+ long_leg = self.rewire_crossing(above_bridging_leg, below_leg)
793
+ self.push_associated_modification!(long_leg)
794
+ end
795
+ end
796
+ above_bridging_legs.each do |above_bridging_leg|
797
+ self.push_associated_modification!(above_bridging_leg)
798
+ end
799
+ end
800
+ end
801
+ end
802
+ end
803
+ end