chronic_tree 1.0.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,73 @@
1
+ require "chronic_tree/version"
2
+ require "chronic_tree/active_record/element"
3
+ require "chronic_tree/travesal"
4
+ require "chronic_tree/operation"
5
+ require "chronic_tree/active_record/relation"
6
+ require "set"
7
+
8
+ module ChronicTree
9
+ require 'chronic_tree/railtie' if defined?(Rails)
10
+
11
+ class Error < RuntimeError; end
12
+ class InvalidObjectError < Error; end
13
+
14
+ include ChronicTree::ActiveRecord::Relation
15
+ include ChronicTree::Travesal
16
+ include ChronicTree::Operation
17
+
18
+ def self.included(base)
19
+ base.class_eval <<-RUBY
20
+ attr_reader :current_time_at, :current_scope_name
21
+
22
+ @@defined_chronic_tree_scopes = Set.new
23
+
24
+ def self.defined_chronic_tree_scopes
25
+ @@defined_chronic_tree_scopes
26
+ end
27
+ RUBY
28
+
29
+ base.extend(ClassMethods)
30
+ end
31
+
32
+ module ClassMethods
33
+
34
+ def chronic_tree(scope_name = 'default')
35
+ self.class_eval <<-RUBY
36
+ has_many :"elements_under_#{scope_name}_parent",
37
+ proc { |owner| where(scope_name: scope_name, tree_type: owner.class.name) },
38
+ class_name: 'ChronicTree::ActiveRecord::Element',
39
+ foreign_key: 'parent_id',
40
+ dependent: :destroy
41
+
42
+ has_many :"elements_as_#{scope_name}_child",
43
+ proc { |owner| where(scope_name: scope_name, tree_type: owner.class.name) },
44
+ class_name: 'ChronicTree::ActiveRecord::Element',
45
+ foreign_key: 'child_id',
46
+ dependent: :destroy
47
+
48
+ has_many :"elements_under_#{scope_name}_root",
49
+ proc { |owner| where(scope_name: scope_name, tree_type: owner.class.name) },
50
+ class_name: 'ChronicTree::ActiveRecord::Element',
51
+ foreign_key: 'root_id',
52
+ dependent: :destroy
53
+ RUBY
54
+
55
+ defined_chronic_tree_scopes << scope_name
56
+ end
57
+
58
+ end
59
+
60
+ def as_tree(time_at = Time.now, scope_name = 'default')
61
+ time_at, scope_name = Time.now, time_at if time_at.is_a?(String)
62
+
63
+ raise "Scope name is wrong. It should be equal to the name that has " \
64
+ "been set up." unless self.class.defined_chronic_tree_scopes.include?(scope_name)
65
+
66
+ raise "Time can't be later than now." if time_at > Time.now
67
+
68
+ @current_time_at = time_at
69
+ @current_scope_name = scope_name
70
+
71
+ self
72
+ end
73
+ end
@@ -0,0 +1,26 @@
1
+ module ChronicTree
2
+ module ActiveRecord
3
+ class Element < ::ActiveRecord::Base
4
+ self.table_name = "chronic_tree_elements"
5
+
6
+ belongs_to :parent, polymorphic: true, foreign_type: 'tree_type'
7
+ belongs_to :child, polymorphic: true, foreign_type: 'tree_type'
8
+ belongs_to :root, polymorphic: true, foreign_type: 'tree_type'
9
+
10
+ scope :at, -> (time = Time.now) {
11
+ start_time_col = self.arel_table[:start_time]
12
+ end_time_col = self.arel_table[:end_time]
13
+ where(start_time_col.lteq(time)).where(end_time_col.gt(time))
14
+ }
15
+
16
+ scope :exclude_root, -> {
17
+ root_id_col = self.arel_table[:root_id]
18
+ child_id_col = self.arel_table[:child_id]
19
+ where(root_id_col.not_eq(child_id_col))
20
+ }
21
+
22
+ scope :direct, -> { where(distance: 1) }
23
+ scope :all_distance, -> { unscope(where: :distance) }
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,34 @@
1
+ module ChronicTree
2
+ module ActiveRecord
3
+ module Relation
4
+ def children_relation(time_at, scope_name)
5
+ send("elements_under_#{scope_name}_parent").
6
+ at(time_at).
7
+ direct.
8
+ exclude_root.
9
+ includes(:child)
10
+ end
11
+
12
+ def parent_relation(time_at, scope_name)
13
+ existed_relation(time_at, scope_name).
14
+ direct.
15
+ exclude_root
16
+ end
17
+
18
+ def existed_relation(time_at, scope_name)
19
+ send("elements_as_#{scope_name}_child").
20
+ at(time_at)
21
+ end
22
+
23
+ def descendants_relation(time_at, scope_name)
24
+ children_relation(time_at, scope_name).order(:distance).all_distance
25
+ end
26
+
27
+ def ancestors_relation(time_at, scope_name)
28
+ existed_relation(time_at, scope_name).
29
+ includes(:parent).
30
+ order(:distance)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,165 @@
1
+ module ChronicTree
2
+ module Command
3
+
4
+ class AddRootElement
5
+ attr_reader :source
6
+
7
+ def initialize(source)
8
+ @source = source
9
+ end
10
+
11
+ def do
12
+ source.send("elements_under_#{source.current_scope_name}_root").create(
13
+ child: source,
14
+ parent: source,
15
+ distance: 0,
16
+ start_time: source.current_time_at,
17
+ end_time: 1000.years.since(source.current_time_at)
18
+ )
19
+ end
20
+ end
21
+
22
+ class AddChildElement
23
+ attr_reader :source, :child_id, :distance, :source_root_id
24
+
25
+ def initialize(source, child_object_or_child_id, distance = 1, source_root_id = nil)
26
+ @source = source
27
+ @source_root_id = if source_root_id.nil?
28
+ source.root.id
29
+ else
30
+ source_root_id
31
+ end
32
+
33
+ @distance = distance
34
+
35
+ @child_id = if child_object_or_child_id.respond_to?(:id)
36
+ child_object_or_child_id.send(:id)
37
+ else
38
+ child_object_or_child_id
39
+ end
40
+ end
41
+
42
+ def do
43
+ source.send("elements_under_#{source.current_scope_name}_parent").create(
44
+ root_id: source_root_id,
45
+ child_id: child_id,
46
+ distance: distance,
47
+ start_time: source.current_time_at,
48
+ end_time: 1000.years.since(source.current_time_at)
49
+ )
50
+ end
51
+ end
52
+
53
+ class RemoveChildElementsFromAncestors
54
+ attr_reader :ancestor_ids, :child_ids, :source
55
+
56
+ def initialize(source, ancestor_ids, child_ids)
57
+ @source = source
58
+ @ancestor_ids = ancestor_ids
59
+ @child_ids = child_ids
60
+ end
61
+
62
+ def do
63
+ ChronicTree::ActiveRecord::Element.at(source.current_time_at).
64
+ where(tree_type: source.class.name).
65
+ where(scope_name: source.current_scope_name).
66
+ where(parent_id: ancestor_ids).
67
+ where(child_id: child_ids).
68
+ update_all(end_time: source.current_time_at)
69
+ end
70
+ end
71
+
72
+ class AddChildElementsToNewAncestors
73
+ attr_reader :source, :source_root_id, :new_ancestors, :child_elements
74
+
75
+ def initialize(source, source_root_id, new_ancestors, child_elements)
76
+ @source = source
77
+ @source_root_id = source_root_id
78
+ @new_ancestors = new_ancestors
79
+ @child_elements = child_elements
80
+ end
81
+
82
+ def do
83
+ new_ancestors.each_with_index do |parent_object, i|
84
+ child_elements.each do |e|
85
+ AddChildElement.new(parent_object, e.child_id, e.distance + i, source_root_id).do
86
+ end
87
+ end
88
+ end
89
+ end
90
+
91
+ class AddChildElementToOrigAncestors
92
+ attr_reader :source, :child
93
+
94
+ def initialize(source, child)
95
+ @source = source
96
+ @child = child
97
+ end
98
+
99
+ def do
100
+ source.ancestors.each_with_index do |parent_object, index|
101
+ AddChildElement.new(parent_object, child, index + 2, source.root.id).do
102
+ end
103
+ end
104
+ end
105
+
106
+ class AddReplacedObjToOrigAncestors
107
+ attr_reader :source, :source_root_id, :source_ancestors, :target
108
+
109
+ def initialize(source, source_root_id, source_ancestors, target)
110
+ @source = source
111
+ @source_root_id = source_root_id
112
+ @source_ancestors = source_ancestors
113
+ @target = target
114
+ end
115
+
116
+ def do
117
+ source_ancestors.each_with_index do |parent_object, i|
118
+ AddChildElement.new(parent_object, target.id, i + 1, source_root_id).do
119
+ end
120
+ end
121
+ end
122
+
123
+ class AddChildElementsToReplacedObject
124
+ attr_reader :source, :source_root_id, :target, :child_elements
125
+
126
+ def initialize(source, source_root_id, target, child_elements)
127
+ @source = source
128
+ @source_root_id = source_root_id
129
+ @target = target
130
+ @child_elements = child_elements
131
+ end
132
+
133
+ def do
134
+ child_elements.each { |el| AddChildElement.new(target, el.child_id, el.distance).do }
135
+ end
136
+ end
137
+
138
+ class RemoveSelfElement
139
+ attr_reader :source
140
+
141
+ def initialize(source)
142
+ @source = source
143
+ end
144
+
145
+ def do
146
+ source.existed_relation(source.current_time_at, source.current_scope_name).each do |el|
147
+ el.update_attribute(:end_time, source.current_time_at)
148
+ end
149
+ end
150
+ end
151
+
152
+ class RemoveDescendantElements
153
+ attr_reader :source
154
+
155
+ def initialize(source)
156
+ @source = source
157
+ end
158
+
159
+ def do
160
+ source.flat_descendants.each { |object| RemoveSelfElement.new(object).do }
161
+ end
162
+ end
163
+
164
+ end
165
+ end
@@ -0,0 +1,212 @@
1
+ require 'chronic_tree/command'
2
+
3
+ module ChronicTree
4
+ module Operation
5
+
6
+ class ChangeParent
7
+ attr_reader :source, :source_root_id, :ready_to_move_elements, :new_ancestors
8
+
9
+ def initialize(source, new_parent_object)
10
+ @source = source
11
+
12
+ @ready_to_move_elements = source.
13
+ descendants_relation(source.current_time_at,
14
+ source.current_scope_name).map do |el|
15
+
16
+ OpenStruct.new(child_id: el.child_id, distance: el.distance + 1)
17
+ end
18
+ @ready_to_move_elements << OpenStruct.new(child_id: source.id, distance: 1)
19
+
20
+ @source_root_id = source.root.id
21
+
22
+ @new_ancestors = new_parent_object.ancestors
23
+ @new_ancestors.unshift(new_parent_object) unless new_parent_object.id == @source_root_id
24
+ end
25
+
26
+ def act
27
+ ::ActiveRecord::Base.transaction do
28
+ ChronicTree::Command::RemoveChildElementsFromAncestors.new(
29
+ source,
30
+ source.ancestors.map(&:id),
31
+ ready_to_move_elements.map(&:child_id)).do
32
+
33
+ ChronicTree::Command::AddChildElementsToNewAncestors.new(
34
+ source,
35
+ source_root_id,
36
+ new_ancestors,
37
+ ready_to_move_elements).do
38
+ end
39
+
40
+ source
41
+ end
42
+ end
43
+
44
+ class ReplaceBy
45
+ attr_reader :source, :target, :source_root_id, :source_ancestors, :ready_to_move_elements
46
+
47
+ def initialize(source, target)
48
+ @source = source
49
+ @target = target
50
+
51
+ @ready_to_move_elements = source.descendants_relation(
52
+ source.current_time_at,
53
+ source.current_scope_name).map do |el|
54
+
55
+ OpenStruct.new(id: el.id, child_id: el.child_id, distance: el.distance)
56
+ end
57
+
58
+ @source_root_id = (source == source.root) ? target.id : source.root.id
59
+ @source_ancestors = source.ancestors
60
+ end
61
+
62
+ def act
63
+ ::ActiveRecord::Base.transaction do
64
+ remove_obsolete_elements
65
+ add_new_elements
66
+ end
67
+
68
+ source
69
+ end
70
+
71
+ private
72
+
73
+ def remove_obsolete_elements
74
+ ChronicTree::Command::RemoveChildElementsFromAncestors.new(
75
+ source,
76
+ [source.id],
77
+ ready_to_move_elements.map(&:child_id)).do
78
+
79
+ ChronicTree::Command::RemoveSelfElement.new(source).do
80
+ end
81
+
82
+ def add_new_elements
83
+ if source.id == source_root_id
84
+ ChronicTree::Command::AddRootElement.new(source).do
85
+ else
86
+ ChronicTree::Command::AddReplacedObjToOrigAncestors.new(
87
+ source, source_root_id, source_ancestors, target).do
88
+ end
89
+
90
+ ChronicTree::Command::AddChildElementsToReplacedObject.new(
91
+ source, source_root_id, target, ready_to_move_elements).do
92
+ end
93
+ end
94
+
95
+
96
+
97
+ def add_as_root
98
+ as_tree(Time.now, current_scope_name || 'default')
99
+
100
+ raise_if_tree_is_not_empty
101
+
102
+ ChronicTree::Command::AddRootElement.new(self).do
103
+
104
+ self
105
+ end
106
+
107
+ def add_child(object)
108
+ as_tree(Time.now, current_scope_name || 'default') && object.as_tree(current_time_at, current_scope_name)
109
+
110
+ raise_if_object_unmatched(object)
111
+ raise_if_object_is_in_the_tree(object)
112
+ raise_if_self_is_not_in_the_tree
113
+
114
+ ::ActiveRecord::Base.transaction do
115
+ ChronicTree::Command::AddChildElement.new(self, object).do
116
+ ChronicTree::Command::AddChildElementToOrigAncestors.new(self, object).do unless self == root
117
+ end
118
+
119
+ self
120
+ end
121
+
122
+ def remove_self
123
+ as_tree(Time.now, current_scope_name || 'default')
124
+
125
+ raise_if_self_is_not_in_the_tree
126
+
127
+ ::ActiveRecord::Base.transaction do
128
+ ChronicTree::Command::RemoveSelfElement.new(self).do
129
+ ChronicTree::Command::RemoveDescendantElements.new(self).do
130
+ end
131
+
132
+ self
133
+ end
134
+
135
+ def remove_descendants
136
+ as_tree(Time.now, current_scope_name || 'default')
137
+
138
+ raise_if_self_is_not_in_the_tree
139
+
140
+ ::ActiveRecord::Base.transaction { ChronicTree::Command::RemoveDescendantElements.new(self).do }
141
+
142
+ self
143
+ end
144
+
145
+ def change_parent(object)
146
+ as_tree(Time.now, current_scope_name || 'default') && object.as_tree(current_time_at, current_scope_name)
147
+ return self if self != root && parent == object
148
+
149
+ raise_if_object_unmatched(object)
150
+ raise_if_object_is_not_in_the_tree(object)
151
+ raise_if_object_equals_to_self(object)
152
+ raise_if_object_is_a_child_of_self(object)
153
+ raise_if_self_is_not_in_the_tree
154
+
155
+ ChangeParent.new(self, object).act
156
+ end
157
+
158
+ def replace_by(object)
159
+ as_tree(Time.now, current_scope_name || 'default') && object.as_tree(current_time_at, current_scope_name)
160
+
161
+ raise_if_object_unmatched(object)
162
+ raise_if_object_is_in_the_tree(object)
163
+ raise_if_self_is_not_in_the_tree
164
+
165
+ ReplaceBy.new(self, object).act
166
+ end
167
+
168
+ private
169
+
170
+ def raise_if_object_unmatched(object)
171
+ if object.class.name != self.class.name
172
+ raise InvalidObjectError, "Object invalid. You can't add two types of objects in a tree."
173
+ end
174
+
175
+ raise InvalidObjectError, "Object invalid. You must save it first." if object.new_record?
176
+ end
177
+
178
+ def raise_if_object_is_in_the_tree(object)
179
+ if object.existed_in_tree?(current_time_at, current_scope_name)
180
+ raise InvalidObjectError, "Object must not be in the tree now."
181
+ end
182
+ end
183
+
184
+ def raise_if_object_is_not_in_the_tree(object)
185
+ unless object.existed_in_tree?(current_time_at, current_scope_name)
186
+ raise InvalidObjectError, "Object must be in the tree now."
187
+ end
188
+ end
189
+
190
+ def raise_if_object_equals_to_self(object)
191
+ raise InvalidObjectError, "Object can't be equal to self." if self == object
192
+ end
193
+
194
+ def raise_if_object_is_a_child_of_self(object)
195
+ if descendants_relation(current_time_at, current_scope_name).where(child_id: object.id).any?
196
+ raise InvalidObjectError, "Object can't be a child of self."
197
+ end
198
+ end
199
+
200
+ def raise_if_self_is_not_in_the_tree
201
+ unless existed_in_tree?(current_time_at, current_scope_name)
202
+ raise Error, "Self must be in the tree now."
203
+ end
204
+ end
205
+
206
+ def raise_if_tree_is_not_empty
207
+ unless tree_empty?(current_time_at, current_scope_name)
208
+ raise Error, "Tree isn't empty, can't add root element."
209
+ end
210
+ end
211
+ end
212
+ end