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