acts-as-dag 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +282 -0
- data/lib/active_record/acts/dag.rb +800 -0
- data/test/dag_test.rb +816 -0
- metadata +67 -0
data/README.rdoc
ADDED
@@ -0,0 +1,282 @@
|
|
1
|
+
= Acts As Dag
|
2
|
+
|
3
|
+
Acts As Dag, short for Acts As Directed Acyclic Graph, is a plugin which allows you to represent DAG hierarchy using your ActiveRecord models.
|
4
|
+
|
5
|
+
== Basic Information
|
6
|
+
|
7
|
+
Say you have been using one of the many great plugins that allow for Tree hierarchy like: acts_as_tree or acts_as_nested_set, acts_as_better_nested_set, etc. Yet, you feel something is missing. Tree's are just not good enough. You want to allow each record to have multiple parent objects within a hierarchy as well as multiple children. Then you are going to need a DAG instead of a tree, and thats were this plugin becomes useful.
|
8
|
+
|
9
|
+
=== What's a DAG?
|
10
|
+
|
11
|
+
http://en.wikipedia.org/wiki/Directed_acyclic_graph
|
12
|
+
|
13
|
+
=== Aren't there plugins for this already?
|
14
|
+
|
15
|
+
Yes, but I think they aren't as fast or feature filled. Flogic has a good simple acts_as_dag plugin on github which would be similar to acts_as_tree.
|
16
|
+
|
17
|
+
== Features
|
18
|
+
|
19
|
+
* DAG graph functionality
|
20
|
+
* STI support
|
21
|
+
* Polymorphic graphs
|
22
|
+
* Association injection for graph nodes
|
23
|
+
* Instance method injections (root?,leaf?) for graph nodes
|
24
|
+
* O(1) lookups, no breath or depth first searching
|
25
|
+
* O(x*y) insert, update and delete, where x & y are the number of ancestors and descendants of a node.
|
26
|
+
|
27
|
+
== Requirements
|
28
|
+
|
29
|
+
This uses named_scope so your going to need Rails 2.1 or above.
|
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.
|
32
|
+
|
33
|
+
== Installation
|
34
|
+
|
35
|
+
gem install acts-as-dag
|
36
|
+
|
37
|
+
== Terminology
|
38
|
+
|
39
|
+
- Node: A source or destination of a link or edge in the graph.
|
40
|
+
- Edge: A direct connection between two nodes. These can only be created by your application.
|
41
|
+
- Link: A indirect connection between two nodes. Denotes the existence of a path. These can only be created by the plugin internally.
|
42
|
+
|
43
|
+
== Singleton Methods
|
44
|
+
|
45
|
+
This section outlines the two methods that need to be included in your ActiveRecord models. In general this whole plugin can be thought of as one big has_many association.
|
46
|
+
|
47
|
+
=== acts_as_dag_links
|
48
|
+
|
49
|
+
This singleton class method needs to be called in your ActiveRecord model that represents the links for the DAG graph. For non-polymorphic graphs it has a required parameter:
|
50
|
+
|
51
|
+
acts_as_dag_links :node_class_name => 'Class Name of the node model'
|
52
|
+
|
53
|
+
If the graph is polymorphic :node_class_name is unnecessary. A polymorphic call would be:
|
54
|
+
|
55
|
+
acts_as_dag_links :polymorphic => true
|
56
|
+
|
57
|
+
==== Optional Parameters
|
58
|
+
|
59
|
+
- :ancestor_id_column. By default, 'ancestor_id', column to use for the ancestor reference
|
60
|
+
- :descendant_id_column. By default, 'descendant_id', column to use for the descendant reference
|
61
|
+
- :direct_column. By default, 'direct', boolean column that represents whether the link is an edge (direct)
|
62
|
+
- :count_column. By default, 'count', represents the number of ways to get from A to B.
|
63
|
+
- :polymorphic. By default, false, If you want polymorphic graphs see below.
|
64
|
+
|
65
|
+
With polymorphic graphs we also have...
|
66
|
+
|
67
|
+
- :ancestor_type_column. By default, 'ancestor_type', column to use for the ancestor type reference
|
68
|
+
- :descendant_type_column. By default, 'descendant_type', column to use for the descendant type reference
|
69
|
+
|
70
|
+
==== Required Table Columns
|
71
|
+
|
72
|
+
Each of the optional column parameters needs a field in the link table. Hence for non-polymorphic graphs a migration would look like...
|
73
|
+
|
74
|
+
create_table :links, do |t|
|
75
|
+
t.integer :ancestor_id
|
76
|
+
t.integer :descendant_id
|
77
|
+
t.boolean :direct
|
78
|
+
t.integer :count
|
79
|
+
end
|
80
|
+
|
81
|
+
And for polymorphic graphs...
|
82
|
+
|
83
|
+
create_table :links, do |t|
|
84
|
+
t.integer :ancestor_id
|
85
|
+
t.string :ancestor_type
|
86
|
+
t.integer :descendant_id
|
87
|
+
t.string :descendant_type
|
88
|
+
t.boolean :direct
|
89
|
+
t.integer :count
|
90
|
+
end
|
91
|
+
|
92
|
+
==== Injected Associations
|
93
|
+
|
94
|
+
Calling acts_as_dag_links method in the class definition injects the following associations.
|
95
|
+
|
96
|
+
belongs_to :ancestor
|
97
|
+
belongs_to :descendant
|
98
|
+
|
99
|
+
==== Injected Named Scopes
|
100
|
+
|
101
|
+
The recieves the following named scopes. The following two scopes narrows a query to only links with a certain ancestor or descendant
|
102
|
+
|
103
|
+
named_scope :with_ancestor(ancestor_instance)
|
104
|
+
named_scope :with_descendant(descendant_instance)
|
105
|
+
|
106
|
+
The scopes below narrow queries to direct or indirect links
|
107
|
+
|
108
|
+
named_scope :direct
|
109
|
+
named_scope :indirect
|
110
|
+
|
111
|
+
The scopes below attach the actual ancestor or descendant nodes
|
112
|
+
|
113
|
+
named_scope :ancestor_nodes, :joins => :ancestor
|
114
|
+
named_scope :descendant_nodes, :joins => :descendant
|
115
|
+
|
116
|
+
==== Injected Class Methods
|
117
|
+
|
118
|
+
Several class methods get added to the link model to make it easier to find and create edges, and find links.
|
119
|
+
|
120
|
+
#Finds an edge of returns nil
|
121
|
+
self.find_edge(ancestor,descendant)
|
122
|
+
#Returns true if an edge exists
|
123
|
+
self.edge?(ancestor,descendant)
|
124
|
+
self.direct?(ancestor,descendant)
|
125
|
+
|
126
|
+
#Finds a link or returns nil
|
127
|
+
self.find_link(ancestor,descendant)
|
128
|
+
#Returns true if a link exists
|
129
|
+
self.connected?(ancestor,descendant)
|
130
|
+
|
131
|
+
#Creates an edge between an ancestor and descendant
|
132
|
+
self.create_edge(ancestor,descendant)
|
133
|
+
self.connect(ancestor,descendant)
|
134
|
+
|
135
|
+
#Creates an edge using save! between an ancestor and descendant
|
136
|
+
self.create_edge!(ancestor,descendant)
|
137
|
+
self.connect!(ancestor,descendant)
|
138
|
+
|
139
|
+
#Builds an edge between an ancestor and descendant, returning an unsaved edge
|
140
|
+
self.build_edge(ancestor,descendant)
|
141
|
+
|
142
|
+
#Finds and returns the edge if it exists, or calls build_edge
|
143
|
+
self.find_or_build_edge(ancestor,descendant)
|
144
|
+
|
145
|
+
==== Injected Instance Methods
|
146
|
+
|
147
|
+
Here is a sample of some of the important instance methods that get added to the link model.
|
148
|
+
|
149
|
+
#whether the current edge can be destroyed. If the edge also has a link, ie it can be made indirectly, then it cannot be destroyed.
|
150
|
+
destroyable?()
|
151
|
+
|
152
|
+
#Make the edge indirect. Removes the direct edge but keeps the indirect links
|
153
|
+
make_indirect()
|
154
|
+
|
155
|
+
#Makes the link direct. Adds a direct edge onto a link.
|
156
|
+
make_direct()
|
157
|
+
|
158
|
+
#Number of unique ways to get from the ancestor to the descendant
|
159
|
+
count()
|
160
|
+
|
161
|
+
=== has_dag_links
|
162
|
+
|
163
|
+
This singleton class method can be optionally called from the node ActiveRecord model. If you do not call it you don't get all the nice associations within the node model, yet everything will still work fine. It takes the required parameter:
|
164
|
+
|
165
|
+
has_dag_links :link_class_name => 'Class Name of the link model'
|
166
|
+
|
167
|
+
==== Optional Parameters
|
168
|
+
|
169
|
+
- :prefix. By default, ''. Use a prefix if your model calls has_dag_links for multiple link classes.
|
170
|
+
|
171
|
+
If your link class holds a polymorphic graph you also have...
|
172
|
+
|
173
|
+
- :ancestor_class_names. By default [], array of class names that are ancestors to this class.
|
174
|
+
- :descendant_class_names. By default [], array of class names that are descendants to this class.
|
175
|
+
|
176
|
+
==== Injected Associations
|
177
|
+
|
178
|
+
has_dag_links provides a number of has_many and has_many_through associations.
|
179
|
+
|
180
|
+
- links_as_ancestor: Has many association that finds the links with the current instance as ancestor
|
181
|
+
- links_as_descendant: Has many association that finds the links with the current instance as descendant
|
182
|
+
- links_as_parent: Has many association that finds direct links (edges) with the current instance as ancestor
|
183
|
+
- links_as_child: Has many association that finds direct links (edges) with the current instance as descendant
|
184
|
+
|
185
|
+
Note that if a record is in links_as_parent it will be in links_as_ancestor. Also note that adding records to either produces the same result as you can only add direct links (edges). Currently there is also an error if a record exists for links_as_ancestor and you try to add it to links_as_parent. The correct way is to use the make_direct method on the link instance.
|
186
|
+
|
187
|
+
For non-polymorphic graphs you also get the following associations. These find the actual ancestors as opposed to the link instances.
|
188
|
+
|
189
|
+
- ancestors
|
190
|
+
- descendants
|
191
|
+
- parents
|
192
|
+
- children
|
193
|
+
|
194
|
+
For polymorphic graphs where the ancestor_class_names or descendant_class_names includes the specified class names the following associations are also built. For each ancestor_class_name:
|
195
|
+
|
196
|
+
-links_as_descendant_for_#{ancestor_class_name.tableize}
|
197
|
+
-links_as_child_for_#{ancestor_class_name.tableize}
|
198
|
+
-ancestor_#{ancestor_class_name.tableize}
|
199
|
+
-parent_#{ancestor_class_name.tableize}
|
200
|
+
|
201
|
+
For each descendant_class_name
|
202
|
+
|
203
|
+
-links_as_ancestor_for_#{ancestor_class_name.tableize}
|
204
|
+
-links_as_parent_for_#{ancestor_class_name.tableize}
|
205
|
+
-descendant_#{ancestor_class_name.tableize}
|
206
|
+
-child_#{ancestor_class_name.tableize}
|
207
|
+
|
208
|
+
==== Injected Instance Methods
|
209
|
+
|
210
|
+
Along with the above associations a number of instance methods are defined.
|
211
|
+
|
212
|
+
-leaf? , Boolean value as to whether the current node is a leaf (no descendants)
|
213
|
+
-root? , Boolean value as to whether the current node is a root (no ancestors)
|
214
|
+
|
215
|
+
With polymorphic graphs for each ancestor_class_name another method is defined.
|
216
|
+
|
217
|
+
-root_for_#{ancestor_class_name.tableize}? , Returns true if the node has no ancestors of the represented type.
|
218
|
+
|
219
|
+
Likewise for each descendant_class_name.
|
220
|
+
|
221
|
+
-leaf_for_#{descendent_class_name.tableize}? , Returns true if the node has no descendants of the represented type.
|
222
|
+
|
223
|
+
== Usage
|
224
|
+
|
225
|
+
This section goes over some basic usage examples and presents some caveats.
|
226
|
+
|
227
|
+
=== Basic Non-polymorphic Graphs.
|
228
|
+
|
229
|
+
class Node < ActiveRecord::Base
|
230
|
+
has_dag_links :link_class_name => 'Link'
|
231
|
+
end
|
232
|
+
|
233
|
+
class Link < ActiveRecord::Base
|
234
|
+
acts_as_dag_links :node_class_name => 'Node'
|
235
|
+
end
|
236
|
+
|
237
|
+
#Adding an edge
|
238
|
+
parent_node = Node.create!
|
239
|
+
child_node = Node.create!
|
240
|
+
parent_node.children << child_node
|
241
|
+
|
242
|
+
#Removing it
|
243
|
+
link = Link.find_link(parent_node,child_node)
|
244
|
+
if link.destroyable?
|
245
|
+
link.destory
|
246
|
+
else
|
247
|
+
link.make_indirect
|
248
|
+
link.save!
|
249
|
+
end
|
250
|
+
|
251
|
+
=== Caveats for Adding, Updating, Deleting Links
|
252
|
+
|
253
|
+
Due to the algorithm used there are some caveats for modifying links. Don't worry if you try to do something illegal because it will raise exceptions and not muck the graph up.
|
254
|
+
|
255
|
+
You can only create new edges where a link doesn't already exist. If the link exists and you want to make it direct you need to promote it using make_direct instance method. Also you can only create direct links. This makes sense because indirect links are dependent on direct links. An indirect link by itself makes no sense to the algorithm. In reverse, you can only delete links which are direct and have no indirect nature. Hence the link only exists because it is direct and not through a sequence of other links. Instead of deleting these types of links you can downgrade them using make_indirect. This will remove the direct edge A -> C and leave the transitive edge from A -> B and B -> C. So just remember before you add, check if it exists, or use one of the link class method create_edge! which will always work.
|
256
|
+
|
257
|
+
== The Algorithm
|
258
|
+
|
259
|
+
This section briefly explains how the plugin works. For a really detailed understanding look at the perpetuate function which is used to fix up the graph when its changed.
|
260
|
+
|
261
|
+
To start there is some terminology:
|
262
|
+
|
263
|
+
- Node is a point on the graph
|
264
|
+
- Edge is a direct connection between two nodes.
|
265
|
+
- Link is a connection using one or more edges.
|
266
|
+
|
267
|
+
This implementation works by storing every possible link as a record in the database. The links originate at the ancestor and end at the descendant. Hence checking if X is a descendant of Y can be accomplished in one SQL query. Likewise finding all descendants or ancestors of Y can also be done in a single query. There are simple queries that don't use recursive stored procedures that you would need with a parent child model. Hence getting information out of the database is really fast. You can also find out the number of unique ways X is connected to Y and whether it has a direct connection. This allows the finding of all parents or children of a node with one query.
|
268
|
+
|
269
|
+
The downside to this implementation, besides the large storage requirements, is that updating the graph by adding or removing nodes or edges is not trivial. When edges are added a sort of cross product between the parent and child nodes ancestors and descendants is done that updates the counts and creates new links as necessary. Hence the complexity of an update, add, or deletion, matters heavily on how your graph is arranged and what nodes you are connecting.
|
270
|
+
|
271
|
+
== Thanks
|
272
|
+
|
273
|
+
Whoever did the awesome_nested_set plugin. This is my first plugin and I used that as a base. The Rails Way was also a great book that essentially taught me Rails.
|
274
|
+
|
275
|
+
== Credit
|
276
|
+
|
277
|
+
Authors:: Matthew Leventi, Robert Schmitt
|
278
|
+
|
279
|
+
Algorithm and plugin designed by Matthew Leventi Email:(first letter of my first name followed by my last name @gmail.com). Im open to questions, very open to bugs, and even more receptive of bug fixes. I am also currently (August 2008) looking for a job just having graduated University of Rochester.
|
280
|
+
|
281
|
+
Robert Schmitt: I have modified the Rake system to support Jeweler to build this as a Ruby gem that can be installed and managed through the gem system.
|
282
|
+
|
@@ -0,0 +1,800 @@
|
|
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
|