acts-as-dag 1.1.0

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