tree_support 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +3 -0
- data/.rspec +2 -0
- data/.travis.yml +4 -0
- data/Gemfile +2 -0
- data/README.org +471 -0
- data/Rakefile +6 -0
- data/examples/0100_simple.rb +22 -0
- data/examples/0110_embeded_node_class.rb +26 -0
- data/examples/0120_graphiz_output_image.rb +6 -0
- data/examples/0130_node_class_example.rb +98 -0
- data/examples/0140_active_record.rb +81 -0
- data/examples/0150_acts_as_tree.rb +101 -0
- data/examples/0160_acts_as_tree_and_list.rb +133 -0
- data/examples/0170_node_class.rb +28 -0
- data/examples/0180_replace_to_active_record_tree.rb +120 -0
- data/examples/0190_generate_ruby_code.rb +68 -0
- data/examples/0200_ar_tree_model.rb +82 -0
- data/examples/0201_safe_destroy_all.rb +55 -0
- data/examples/0210_take_drop.rb +40 -0
- data/examples/0220_it_will_not_be_strange_to_tojson.rb +27 -0
- data/examples/0230_list_to_tree.rb +68 -0
- data/examples/0240_memory_record.rb +34 -0
- data/examples/Gemfile +12 -0
- data/examples/Gemfile.lock +137 -0
- data/examples/demo.rb +126 -0
- data/images/drop.png +0 -0
- data/images/take.png +0 -0
- data/images/take_drop.png +0 -0
- data/images/tree.png +0 -0
- data/images/tree_color.png +0 -0
- data/images/tree_label.png +0 -0
- data/lib/tree_support/ar_tree_model.rb +74 -0
- data/lib/tree_support/graphviz_builder.rb +78 -0
- data/lib/tree_support/inspector.rb +116 -0
- data/lib/tree_support/node.rb +124 -0
- data/lib/tree_support/railtie.rb +9 -0
- data/lib/tree_support/tree_support.rb +28 -0
- data/lib/tree_support/treeable.rb +52 -0
- data/lib/tree_support/version.rb +3 -0
- data/lib/tree_support.rb +2 -0
- data/spec/ar_tree_model_spec.rb +82 -0
- data/spec/node_spec.rb +52 -0
- data/spec/spec_helper.rb +8 -0
- data/spec/tree_support_spec.rb +28 -0
- data/spec/treeable_spec.rb +59 -0
- data/tree_support.gemspec +30 -0
- metadata +196 -0
@@ -0,0 +1,40 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
require "bundler/setup"
|
3
|
+
require "tree_support"
|
4
|
+
puts "--------------------------------------------------------------------------------"
|
5
|
+
puts TreeSupport.example.to_s_tree(drop: 3)
|
6
|
+
puts "--------------------------------------------------------------------------------"
|
7
|
+
puts TreeSupport.example.to_s_tree(take: 3)
|
8
|
+
puts "--------------------------------------------------------------------------------"
|
9
|
+
puts TreeSupport.example.to_s_tree(take: 3, drop: 1)
|
10
|
+
# >> --------------------------------------------------------------------------------
|
11
|
+
# >> Shake the sword
|
12
|
+
# >> Attack magic
|
13
|
+
# >> ├─Summoned Beast X
|
14
|
+
# >> └─Summoned Beast Y
|
15
|
+
# >> Repel sword in length
|
16
|
+
# >> Place a trap
|
17
|
+
# >> Shoot a bow and arrow
|
18
|
+
# >> Recovery magic
|
19
|
+
# >> Drink recovery medicine
|
20
|
+
# >> --------------------------------------------------------------------------------
|
21
|
+
# >> *root*
|
22
|
+
# >> ├─Battle
|
23
|
+
# >> │ ├─Attack
|
24
|
+
# >> │ └─Defense
|
25
|
+
# >> ├─Withdraw
|
26
|
+
# >> │ ├─To stop
|
27
|
+
# >> │ └─To escape
|
28
|
+
# >> └─Break
|
29
|
+
# >> ├─Stop
|
30
|
+
# >> └─Recover
|
31
|
+
# >> --------------------------------------------------------------------------------
|
32
|
+
# >> Battle
|
33
|
+
# >> ├─Attack
|
34
|
+
# >> └─Defense
|
35
|
+
# >> Withdraw
|
36
|
+
# >> ├─To stop
|
37
|
+
# >> └─To escape
|
38
|
+
# >> Break
|
39
|
+
# >> ├─Stop
|
40
|
+
# >> └─Recover
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# It will not be strange to tojson
|
2
|
+
#
|
3
|
+
require "bundler/setup"
|
4
|
+
|
5
|
+
require "rails"
|
6
|
+
require "active_record"
|
7
|
+
require "tree_support"
|
8
|
+
require "byebug"
|
9
|
+
|
10
|
+
Class.new(Rails::Application){config.eager_load = false}.initialize! # In order to read Railtie
|
11
|
+
|
12
|
+
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
|
13
|
+
ActiveRecord::Migration.verbose = false
|
14
|
+
|
15
|
+
ActiveRecord::Schema.define do
|
16
|
+
create_table :nodes do |t|
|
17
|
+
t.belongs_to :parent
|
18
|
+
t.string :name
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class Node < ActiveRecord::Base
|
23
|
+
ar_tree_model
|
24
|
+
end
|
25
|
+
|
26
|
+
Node.create!(name: "*root*")
|
27
|
+
Node.first.to_json # => "{\"id\":1,\"parent_id\":null,\"name\":\"*root*\"}"
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require "bundler/setup"
|
2
|
+
require "tree_support"
|
3
|
+
|
4
|
+
records = [
|
5
|
+
{key: :a0, parent: nil},
|
6
|
+
{key: :a1, parent: :a0},
|
7
|
+
{key: :a2, parent: :a1},
|
8
|
+
|
9
|
+
{key: :b0, parent: nil},
|
10
|
+
{key: :b1, parent: :b0},
|
11
|
+
{key: :b2, parent: :b1},
|
12
|
+
]
|
13
|
+
|
14
|
+
# ---------- Convert with function of library
|
15
|
+
if true
|
16
|
+
puts TreeSupport.records_to_tree(records).collect(&:to_s_tree)
|
17
|
+
puts TreeSupport.records_to_tree(records, root_key: :root).to_s_tree
|
18
|
+
end
|
19
|
+
|
20
|
+
# ---------- How to write by yourself
|
21
|
+
if true
|
22
|
+
source_hash = records.inject({}) { |a, e| a.merge(e[:key] => e) }
|
23
|
+
node_hash = records.inject({}) { |a, e| a.merge(e[:key] => TreeSupport::Node.new(e[:key])) }
|
24
|
+
node_hash.each_value { |node|
|
25
|
+
if parent = source_hash[node.key][:parent]
|
26
|
+
parent_node = node_hash[parent]
|
27
|
+
node.parent = parent_node
|
28
|
+
parent_node.children << node
|
29
|
+
end
|
30
|
+
}
|
31
|
+
roots = node_hash.each_value.find_all { |e| e.parent == nil }
|
32
|
+
puts roots.collect(&:to_s_tree)
|
33
|
+
|
34
|
+
# When you want to make one route
|
35
|
+
root = TreeSupport::Node.new(:root)
|
36
|
+
roots.each do |e|
|
37
|
+
e.parent = root
|
38
|
+
root.children << e
|
39
|
+
end
|
40
|
+
puts root.to_s_tree
|
41
|
+
end
|
42
|
+
|
43
|
+
# >> a0
|
44
|
+
# >> └─a1
|
45
|
+
# >> └─a2
|
46
|
+
# >> b0
|
47
|
+
# >> └─b1
|
48
|
+
# >> └─b2
|
49
|
+
# >> root
|
50
|
+
# >> ├─a0
|
51
|
+
# >> │ └─a1
|
52
|
+
# >> │ └─a2
|
53
|
+
# >> └─b0
|
54
|
+
# >> └─b1
|
55
|
+
# >> └─b2
|
56
|
+
# >> a0
|
57
|
+
# >> └─a1
|
58
|
+
# >> └─a2
|
59
|
+
# >> b0
|
60
|
+
# >> └─b1
|
61
|
+
# >> └─b2
|
62
|
+
# >> root
|
63
|
+
# >> ├─a0
|
64
|
+
# >> │ └─a1
|
65
|
+
# >> │ └─a2
|
66
|
+
# >> └─b0
|
67
|
+
# >> └─b1
|
68
|
+
# >> └─b2
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# Example of combining with MemoryRecord
|
2
|
+
require "bundler/setup"
|
3
|
+
require "tree_support"
|
4
|
+
require "memory_record"
|
5
|
+
|
6
|
+
class Foo
|
7
|
+
include MemoryRecord
|
8
|
+
memory_record [
|
9
|
+
{key: :a, name: "A", parent: nil},
|
10
|
+
{key: :b, name: "B", parent: :a},
|
11
|
+
{key: :c, name: "C", parent: :b},
|
12
|
+
]
|
13
|
+
|
14
|
+
# Any structure can be used as long as it can respond to parent and children
|
15
|
+
concerning :TreeMethods do
|
16
|
+
included do
|
17
|
+
include TreeSupport::Treeable
|
18
|
+
include TreeSupport::Stringify
|
19
|
+
end
|
20
|
+
|
21
|
+
def parent
|
22
|
+
self.class[super]
|
23
|
+
end
|
24
|
+
|
25
|
+
def children
|
26
|
+
self.class.find_all {|e| e.parent == self }
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
puts Foo.find_all(&:root?).collect(&:to_s_tree)
|
32
|
+
# >> A
|
33
|
+
# >> └─B
|
34
|
+
# >> └─C
|
data/examples/Gemfile
ADDED
@@ -0,0 +1,137 @@
|
|
1
|
+
PATH
|
2
|
+
remote: ..
|
3
|
+
specs:
|
4
|
+
tree_support (0.0.1)
|
5
|
+
activesupport
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: https://rubygems.org/
|
9
|
+
specs:
|
10
|
+
actioncable (5.0.0)
|
11
|
+
actionpack (= 5.0.0)
|
12
|
+
nio4r (~> 1.2)
|
13
|
+
websocket-driver (~> 0.6.1)
|
14
|
+
actionmailer (5.0.0)
|
15
|
+
actionpack (= 5.0.0)
|
16
|
+
actionview (= 5.0.0)
|
17
|
+
activejob (= 5.0.0)
|
18
|
+
mail (~> 2.5, >= 2.5.4)
|
19
|
+
rails-dom-testing (~> 2.0)
|
20
|
+
actionpack (5.0.0)
|
21
|
+
actionview (= 5.0.0)
|
22
|
+
activesupport (= 5.0.0)
|
23
|
+
rack (~> 2.0)
|
24
|
+
rack-test (~> 0.6.3)
|
25
|
+
rails-dom-testing (~> 2.0)
|
26
|
+
rails-html-sanitizer (~> 1.0, >= 1.0.2)
|
27
|
+
actionview (5.0.0)
|
28
|
+
activesupport (= 5.0.0)
|
29
|
+
builder (~> 3.1)
|
30
|
+
erubis (~> 2.7.0)
|
31
|
+
rails-dom-testing (~> 2.0)
|
32
|
+
rails-html-sanitizer (~> 1.0, >= 1.0.2)
|
33
|
+
activejob (5.0.0)
|
34
|
+
activesupport (= 5.0.0)
|
35
|
+
globalid (>= 0.3.6)
|
36
|
+
activemodel (5.0.0)
|
37
|
+
activesupport (= 5.0.0)
|
38
|
+
activerecord (5.0.0)
|
39
|
+
activemodel (= 5.0.0)
|
40
|
+
activesupport (= 5.0.0)
|
41
|
+
arel (~> 7.0)
|
42
|
+
activesupport (5.0.0)
|
43
|
+
concurrent-ruby (~> 1.0, >= 1.0.2)
|
44
|
+
i18n (~> 0.7)
|
45
|
+
minitest (~> 5.1)
|
46
|
+
tzinfo (~> 1.1)
|
47
|
+
acts_as_list (0.7.6)
|
48
|
+
activerecord (>= 3.0)
|
49
|
+
acts_as_tree (2.4.0)
|
50
|
+
activerecord (>= 3.0.0)
|
51
|
+
arel (7.1.0)
|
52
|
+
builder (3.2.2)
|
53
|
+
byebug (9.0.5)
|
54
|
+
concurrent-ruby (1.0.2)
|
55
|
+
erubis (2.7.0)
|
56
|
+
globalid (0.3.6)
|
57
|
+
activesupport (>= 4.1.0)
|
58
|
+
gviz (0.3.5)
|
59
|
+
thor
|
60
|
+
i18n (0.7.0)
|
61
|
+
loofah (2.0.3)
|
62
|
+
nokogiri (>= 1.5.9)
|
63
|
+
mail (2.6.4)
|
64
|
+
mime-types (>= 1.16, < 4)
|
65
|
+
memory_record (0.0.6)
|
66
|
+
activemodel
|
67
|
+
activesupport
|
68
|
+
method_source (0.8.2)
|
69
|
+
mime-types (3.1)
|
70
|
+
mime-types-data (~> 3.2015)
|
71
|
+
mime-types-data (3.2016.0521)
|
72
|
+
mini_portile2 (2.1.0)
|
73
|
+
minitest (5.9.0)
|
74
|
+
nio4r (1.2.1)
|
75
|
+
nokogiri (1.6.8)
|
76
|
+
mini_portile2 (~> 2.1.0)
|
77
|
+
pkg-config (~> 1.1.7)
|
78
|
+
pkg-config (1.1.7)
|
79
|
+
rack (2.0.1)
|
80
|
+
rack-test (0.6.3)
|
81
|
+
rack (>= 1.0)
|
82
|
+
rails (5.0.0)
|
83
|
+
actioncable (= 5.0.0)
|
84
|
+
actionmailer (= 5.0.0)
|
85
|
+
actionpack (= 5.0.0)
|
86
|
+
actionview (= 5.0.0)
|
87
|
+
activejob (= 5.0.0)
|
88
|
+
activemodel (= 5.0.0)
|
89
|
+
activerecord (= 5.0.0)
|
90
|
+
activesupport (= 5.0.0)
|
91
|
+
bundler (>= 1.3.0, < 2.0)
|
92
|
+
railties (= 5.0.0)
|
93
|
+
sprockets-rails (>= 2.0.0)
|
94
|
+
rails-dom-testing (2.0.1)
|
95
|
+
activesupport (>= 4.2.0, < 6.0)
|
96
|
+
nokogiri (~> 1.6.0)
|
97
|
+
rails-html-sanitizer (1.0.3)
|
98
|
+
loofah (~> 2.0)
|
99
|
+
railties (5.0.0)
|
100
|
+
actionpack (= 5.0.0)
|
101
|
+
activesupport (= 5.0.0)
|
102
|
+
method_source
|
103
|
+
rake (>= 0.8.7)
|
104
|
+
thor (>= 0.18.1, < 2.0)
|
105
|
+
rake (11.2.2)
|
106
|
+
sprockets (3.7.0)
|
107
|
+
concurrent-ruby (~> 1.0)
|
108
|
+
rack (> 1, < 3)
|
109
|
+
sprockets-rails (3.1.1)
|
110
|
+
actionpack (>= 4.0)
|
111
|
+
activesupport (>= 4.0)
|
112
|
+
sprockets (>= 3.0.0)
|
113
|
+
sqlite3 (1.3.11)
|
114
|
+
thor (0.19.1)
|
115
|
+
thread_safe (0.3.5)
|
116
|
+
tzinfo (1.2.2)
|
117
|
+
thread_safe (~> 0.1)
|
118
|
+
websocket-driver (0.6.4)
|
119
|
+
websocket-extensions (>= 0.1.0)
|
120
|
+
websocket-extensions (0.1.2)
|
121
|
+
|
122
|
+
PLATFORMS
|
123
|
+
ruby
|
124
|
+
|
125
|
+
DEPENDENCIES
|
126
|
+
activerecord
|
127
|
+
acts_as_list
|
128
|
+
acts_as_tree
|
129
|
+
byebug
|
130
|
+
gviz
|
131
|
+
memory_record
|
132
|
+
rails
|
133
|
+
sqlite3
|
134
|
+
tree_support!
|
135
|
+
|
136
|
+
BUNDLED WITH
|
137
|
+
1.16.0.pre.3
|
data/examples/demo.rb
ADDED
@@ -0,0 +1,126 @@
|
|
1
|
+
# output ../images/*
|
2
|
+
require "bundler/setup"
|
3
|
+
require "tree_support"
|
4
|
+
|
5
|
+
root = TreeSupport.example
|
6
|
+
|
7
|
+
p root.each_node.collect.with_index {|n, i| [n.name, i]}
|
8
|
+
puts root.to_s_tree
|
9
|
+
puts TreeSupport.tree(root)
|
10
|
+
puts TreeSupport.tree(root, drop: 1)
|
11
|
+
puts TreeSupport.tree(root, take: 3)
|
12
|
+
puts TreeSupport.tree(root, take: 3, drop: 1)
|
13
|
+
puts TreeSupport.tree(root) {|node, _locals| node.object_id}
|
14
|
+
# TreeSupport.graph_open(root)
|
15
|
+
|
16
|
+
TreeSupport.graphviz(root) {|node|
|
17
|
+
if node.name.include?("Attack")
|
18
|
+
{fillcolor: "lightblue", style: "filled"}
|
19
|
+
elsif node.name.include?("Recover")
|
20
|
+
{fillcolor: "lightpink", style: "filled"}
|
21
|
+
end
|
22
|
+
}.output("../images/tree_color.png")
|
23
|
+
|
24
|
+
TreeSupport.graphviz(root) {|node|
|
25
|
+
{label: node.name.chars.first}
|
26
|
+
}.output("../images/tree_label.png")
|
27
|
+
|
28
|
+
TreeSupport.graphviz(root).output("../images/tree.png")
|
29
|
+
TreeSupport.graphviz(root, take: 3).output("../images/take.png")
|
30
|
+
TreeSupport.graphviz(root, drop: 1).output("../images/drop.png")
|
31
|
+
TreeSupport.graphviz(root, take: 3, drop: 1).output("../images/take_drop.png")
|
32
|
+
# >> [["*root*", 0], ["Battle", 1], ["Attack", 2], ["Shake the sword", 3], ["Attack magic", 4], ["Summoned Beast X", 5], ["Summoned Beast Y", 6], ["Repel sword in length", 7], ["Defense", 8], ["Withdraw", 9], ["To stop", 10], ["Place a trap", 11], ["Shoot a bow and arrow", 12], ["To escape", 13], ["Break", 14], ["Stop", 15], ["Recover", 16], ["Recovery magic", 17], ["Drink recovery medicine", 18]]
|
33
|
+
# >> *root*
|
34
|
+
# >> ├─Battle
|
35
|
+
# >> │ ├─Attack
|
36
|
+
# >> │ │ ├─Shake the sword
|
37
|
+
# >> │ │ ├─Attack magic
|
38
|
+
# >> │ │ │ ├─Summoned Beast X
|
39
|
+
# >> │ │ │ └─Summoned Beast Y
|
40
|
+
# >> │ │ └─Repel sword in length
|
41
|
+
# >> │ └─Defense
|
42
|
+
# >> ├─Withdraw
|
43
|
+
# >> │ ├─To stop
|
44
|
+
# >> │ │ ├─Place a trap
|
45
|
+
# >> │ │ └─Shoot a bow and arrow
|
46
|
+
# >> │ └─To escape
|
47
|
+
# >> └─Break
|
48
|
+
# >> ├─Stop
|
49
|
+
# >> └─Recover
|
50
|
+
# >> ├─Recovery magic
|
51
|
+
# >> └─Drink recovery medicine
|
52
|
+
# >> *root*
|
53
|
+
# >> ├─Battle
|
54
|
+
# >> │ ├─Attack
|
55
|
+
# >> │ │ ├─Shake the sword
|
56
|
+
# >> │ │ ├─Attack magic
|
57
|
+
# >> │ │ │ ├─Summoned Beast X
|
58
|
+
# >> │ │ │ └─Summoned Beast Y
|
59
|
+
# >> │ │ └─Repel sword in length
|
60
|
+
# >> │ └─Defense
|
61
|
+
# >> ├─Withdraw
|
62
|
+
# >> │ ├─To stop
|
63
|
+
# >> │ │ ├─Place a trap
|
64
|
+
# >> │ │ └─Shoot a bow and arrow
|
65
|
+
# >> │ └─To escape
|
66
|
+
# >> └─Break
|
67
|
+
# >> ├─Stop
|
68
|
+
# >> └─Recover
|
69
|
+
# >> ├─Recovery magic
|
70
|
+
# >> └─Drink recovery medicine
|
71
|
+
# >> Battle
|
72
|
+
# >> ├─Attack
|
73
|
+
# >> │ ├─Shake the sword
|
74
|
+
# >> │ ├─Attack magic
|
75
|
+
# >> │ │ ├─Summoned Beast X
|
76
|
+
# >> │ │ └─Summoned Beast Y
|
77
|
+
# >> │ └─Repel sword in length
|
78
|
+
# >> └─Defense
|
79
|
+
# >> Withdraw
|
80
|
+
# >> ├─To stop
|
81
|
+
# >> │ ├─Place a trap
|
82
|
+
# >> │ └─Shoot a bow and arrow
|
83
|
+
# >> └─To escape
|
84
|
+
# >> Break
|
85
|
+
# >> ├─Stop
|
86
|
+
# >> └─Recover
|
87
|
+
# >> ├─Recovery magic
|
88
|
+
# >> └─Drink recovery medicine
|
89
|
+
# >> *root*
|
90
|
+
# >> ├─Battle
|
91
|
+
# >> │ ├─Attack
|
92
|
+
# >> │ └─Defense
|
93
|
+
# >> ├─Withdraw
|
94
|
+
# >> │ ├─To stop
|
95
|
+
# >> │ └─To escape
|
96
|
+
# >> └─Break
|
97
|
+
# >> ├─Stop
|
98
|
+
# >> └─Recover
|
99
|
+
# >> Battle
|
100
|
+
# >> ├─Attack
|
101
|
+
# >> └─Defense
|
102
|
+
# >> Withdraw
|
103
|
+
# >> ├─To stop
|
104
|
+
# >> └─To escape
|
105
|
+
# >> Break
|
106
|
+
# >> ├─Stop
|
107
|
+
# >> └─Recover
|
108
|
+
# >> 70279213508120
|
109
|
+
# >> ├─70279213507760
|
110
|
+
# >> │ ├─70279213507500
|
111
|
+
# >> │ │ ├─70279213507240
|
112
|
+
# >> │ │ ├─70279213507000
|
113
|
+
# >> │ │ │ ├─70279213506800
|
114
|
+
# >> │ │ │ └─70279213506660
|
115
|
+
# >> │ │ └─70279213507200
|
116
|
+
# >> │ └─70279213509960
|
117
|
+
# >> ├─70279213498160
|
118
|
+
# >> │ ├─70279213497920
|
119
|
+
# >> │ │ ├─70279213497740
|
120
|
+
# >> │ │ └─70279213497600
|
121
|
+
# >> │ └─70279213497380
|
122
|
+
# >> └─70279213497180
|
123
|
+
# >> ├─70279213496960
|
124
|
+
# >> └─70279213496720
|
125
|
+
# >> ├─70279213496480
|
126
|
+
# >> └─70279213496320
|
data/images/drop.png
ADDED
Binary file
|
data/images/take.png
ADDED
Binary file
|
Binary file
|
data/images/tree.png
ADDED
Binary file
|
Binary file
|
Binary file
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# acts_as_tree replacement
|
2
|
+
#
|
3
|
+
# class Node < ActiveRecord::Base
|
4
|
+
# ar_tree_model # default
|
5
|
+
# ar_tree_model scope: -> { order(:name) } # Designate the scope yourself
|
6
|
+
# ar_tree_model scope: -> { order(:id).where(active: true) } # You can also specify where
|
7
|
+
# end
|
8
|
+
#
|
9
|
+
require "active_support/concern"
|
10
|
+
|
11
|
+
module TreeSupport
|
12
|
+
module ArTreeModel
|
13
|
+
extend ActiveSupport::Concern
|
14
|
+
|
15
|
+
included do
|
16
|
+
end
|
17
|
+
|
18
|
+
class_methods do
|
19
|
+
def ar_tree_model(options = {})
|
20
|
+
return if ar_tree_model_defined?
|
21
|
+
|
22
|
+
class_attribute :ar_tree_model_configuration
|
23
|
+
self.ar_tree_model_configuration = {
|
24
|
+
scope: -> { order(:id) },
|
25
|
+
}.merge(options)
|
26
|
+
|
27
|
+
if block_given?
|
28
|
+
yield ar_tree_model_configuration
|
29
|
+
end
|
30
|
+
|
31
|
+
include SingletonMethods
|
32
|
+
end
|
33
|
+
|
34
|
+
def ar_tree_model_defined?
|
35
|
+
ancestors.include?(SingletonMethods)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
concern :SingletonMethods do
|
40
|
+
include Treeable
|
41
|
+
include Stringify
|
42
|
+
|
43
|
+
included do
|
44
|
+
scope :tree_default_scope, ar_tree_model_configuration[:scope]
|
45
|
+
|
46
|
+
belongs_to :parent, -> { tree_default_scope }, class_name: name, foreign_key: :parent_id, required: false
|
47
|
+
has_many :children, -> { tree_default_scope }, class_name: name, foreign_key: :parent_id, dependent: :destroy, inverse_of: :parent
|
48
|
+
scope :roots, -> { tree_default_scope.where(parent_id: nil) }
|
49
|
+
end
|
50
|
+
|
51
|
+
class_methods do
|
52
|
+
def ar_tree_model?
|
53
|
+
ar_tree_model_defined? # && columns_hash.has_key?(:id)
|
54
|
+
end
|
55
|
+
|
56
|
+
def root
|
57
|
+
roots.first
|
58
|
+
end
|
59
|
+
|
60
|
+
# In combination with acts_as_list accident in destroy_all, we have to erase from the leaves in order
|
61
|
+
def safe_destroy_all
|
62
|
+
roots.collect(&:destroy)
|
63
|
+
end
|
64
|
+
|
65
|
+
def destroy_all(*args)
|
66
|
+
if respond_to?(:acts_as_list)
|
67
|
+
ActiveSupport::Deprecation.warn("When you use acts_as_list destroy_all do not get id failed to accidentally delete_all Please use safe_destroy_all to erase from the end")
|
68
|
+
end
|
69
|
+
super
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
require "gviz"
|
2
|
+
|
3
|
+
# for GvizEx#output
|
4
|
+
require "pathname"
|
5
|
+
require "delegate"
|
6
|
+
require "fileutils"
|
7
|
+
|
8
|
+
require "active_support/core_ext/module/delegation"
|
9
|
+
|
10
|
+
module TreeSupport
|
11
|
+
def self.graphviz(*args, &block)
|
12
|
+
GraphvizBuilder.build(*args, &block)
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.graph_open(*args, &block)
|
16
|
+
graphviz(*args, &block).output("_output.png")
|
17
|
+
`open _output.png`
|
18
|
+
end
|
19
|
+
|
20
|
+
class GraphvizBuilder
|
21
|
+
# For Gviz#save only for some reason
|
22
|
+
class GvizEx < SimpleDelegator
|
23
|
+
def output(filename)
|
24
|
+
filename = Pathname(filename).expand_path
|
25
|
+
FileUtils.makedirs(filename.dirname)
|
26
|
+
save("#{filename.dirname}/#{filename.basename(".*")}", filename.extname.delete("."))
|
27
|
+
end
|
28
|
+
|
29
|
+
# alias to_dot to_s can not
|
30
|
+
def to_dot
|
31
|
+
to_s
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.build(object, *args, &block)
|
36
|
+
new(*args, &block).build(object)
|
37
|
+
end
|
38
|
+
|
39
|
+
def initialize(**options, &block)
|
40
|
+
@options = {
|
41
|
+
take: 256,
|
42
|
+
drop: 0,
|
43
|
+
}.merge(options)
|
44
|
+
@block = block
|
45
|
+
end
|
46
|
+
|
47
|
+
def build(object)
|
48
|
+
GvizEx.new(Gviz.new).tap do |gv|
|
49
|
+
gv.global(rankdir: "LR", concentrate: "true")
|
50
|
+
visit(gv, object)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def visit(gv, object, depth = 0)
|
57
|
+
if depth < @options[:take]
|
58
|
+
if @options[:drop] <= depth
|
59
|
+
attrs = {}
|
60
|
+
if @block
|
61
|
+
attrs = @block.call(object) || {}
|
62
|
+
end
|
63
|
+
attrs[:label] ||= TreeSupport.node_name(object)
|
64
|
+
gv.node(node_code(object), attrs)
|
65
|
+
if depth.next < @options[:take]
|
66
|
+
gv.route node_code(object) => object.children.collect {|node| node_code(node) }
|
67
|
+
end
|
68
|
+
end
|
69
|
+
object.children.each {|e| visit(gv, e, depth.next) }
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def node_code(object)
|
74
|
+
# I do not want to be a symbol because it is not subject to GC, but because I get angry with Gviz it is a symbolic symbol
|
75
|
+
:"n#{object.object_id}"
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|