acts-as-dag 2.0.1 → 2.5.0

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