acts-as-dag 1.1.0 → 1.1.2

Sign up to get free protection for your applications and to get access to all the features.
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