acts-as-dag 2.0.1 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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