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