tree_support 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +3 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +4 -0
  5. data/Gemfile +2 -0
  6. data/README.org +471 -0
  7. data/Rakefile +6 -0
  8. data/examples/0100_simple.rb +22 -0
  9. data/examples/0110_embeded_node_class.rb +26 -0
  10. data/examples/0120_graphiz_output_image.rb +6 -0
  11. data/examples/0130_node_class_example.rb +98 -0
  12. data/examples/0140_active_record.rb +81 -0
  13. data/examples/0150_acts_as_tree.rb +101 -0
  14. data/examples/0160_acts_as_tree_and_list.rb +133 -0
  15. data/examples/0170_node_class.rb +28 -0
  16. data/examples/0180_replace_to_active_record_tree.rb +120 -0
  17. data/examples/0190_generate_ruby_code.rb +68 -0
  18. data/examples/0200_ar_tree_model.rb +82 -0
  19. data/examples/0201_safe_destroy_all.rb +55 -0
  20. data/examples/0210_take_drop.rb +40 -0
  21. data/examples/0220_it_will_not_be_strange_to_tojson.rb +27 -0
  22. data/examples/0230_list_to_tree.rb +68 -0
  23. data/examples/0240_memory_record.rb +34 -0
  24. data/examples/Gemfile +12 -0
  25. data/examples/Gemfile.lock +137 -0
  26. data/examples/demo.rb +126 -0
  27. data/images/drop.png +0 -0
  28. data/images/take.png +0 -0
  29. data/images/take_drop.png +0 -0
  30. data/images/tree.png +0 -0
  31. data/images/tree_color.png +0 -0
  32. data/images/tree_label.png +0 -0
  33. data/lib/tree_support/ar_tree_model.rb +74 -0
  34. data/lib/tree_support/graphviz_builder.rb +78 -0
  35. data/lib/tree_support/inspector.rb +116 -0
  36. data/lib/tree_support/node.rb +124 -0
  37. data/lib/tree_support/railtie.rb +9 -0
  38. data/lib/tree_support/tree_support.rb +28 -0
  39. data/lib/tree_support/treeable.rb +52 -0
  40. data/lib/tree_support/version.rb +3 -0
  41. data/lib/tree_support.rb +2 -0
  42. data/spec/ar_tree_model_spec.rb +82 -0
  43. data/spec/node_spec.rb +52 -0
  44. data/spec/spec_helper.rb +8 -0
  45. data/spec/tree_support_spec.rb +28 -0
  46. data/spec/treeable_spec.rb +59 -0
  47. data/tree_support.gemspec +30 -0
  48. metadata +196 -0
@@ -0,0 +1,116 @@
1
+ require "kconv"
2
+ require "active_support/concern"
3
+ require "active_support/core_ext/module/attribute_accessors" # mattr_accessor
4
+
5
+ module TreeSupport
6
+ mattr_accessor :name_methods
7
+ self.name_methods = [:to_s_tree_name, :name, :subject, :title]
8
+
9
+ def self.tree(*args, &block)
10
+ Inspector.tree(*args, &block)
11
+ end
12
+
13
+ def self.node_name(object)
14
+ object.send(name_methods.find {|e| object.respond_to?(e)} || :to_s)
15
+ end
16
+
17
+ class Inspector
18
+ def self.tree(object, *args, &block)
19
+ new(*args, &block).tree(object)
20
+ end
21
+
22
+ def initialize(**options, &block)
23
+ @options = {
24
+ take: 256, # Up to depth N (for when the tree can not be displayed because it is huge)
25
+ drop: 0, # From the depth N (when set to 1 you can hide the route)
26
+ root_label: nil, # A valid alternative label for displaying the route
27
+ tab_space: 4, # Indent width from halfway
28
+ connect_char: "├",
29
+ tab_visible_char: "│",
30
+ edge_char: "└",
31
+ branch_char: "─",
32
+ debug: false,
33
+ }.merge(options)
34
+
35
+ @block = block
36
+ end
37
+
38
+ # 木構造の可視化
39
+ #
40
+ # 必要なメソッド
41
+ # parent.children
42
+ # name
43
+ #
44
+ def tree(object, **locals)
45
+ locals = {
46
+ depth: [],
47
+ }.merge(locals)
48
+
49
+ if locals[:depth].size > @options[:drop]
50
+ if object == object.parent.children.last
51
+ prefix_char = @options[:edge_char]
52
+ else
53
+ prefix_char = @options[:connect_char]
54
+ end
55
+ else
56
+ prefix_char = ""
57
+ end
58
+
59
+ indents = locals[:depth].each.with_index.collect {|e, i|
60
+ if i > @options[:drop]
61
+ tab = e ? @options[:tab_visible_char] : ""
62
+ tab.toeuc.ljust(@options[:tab_space]).toutf8
63
+ end
64
+ }.join
65
+
66
+ if @block
67
+ label = @block.call(object, locals)
68
+ else
69
+ if locals[:depth].empty? && @options[:root_label] # Change if there is root and alternative label
70
+ label = @options[:root_label]
71
+ else
72
+ label = TreeSupport.node_name(object)
73
+ end
74
+ end
75
+
76
+ buffer = ""
77
+ branch_char = nil
78
+
79
+ if locals[:depth].size > @options[:drop]
80
+ branch_char = @options[:branch_char]
81
+ end
82
+ if locals[:depth].size < @options[:take]
83
+ if locals[:depth].size >= @options[:drop]
84
+ buffer = "#{indents}#{prefix_char}#{branch_char}#{label}#{@options[:debug] ? locals[:depth].inspect : ""}\n"
85
+ end
86
+ end
87
+
88
+ flag = false
89
+ if object.parent
90
+ flag = (object != object.parent.children.last)
91
+ end
92
+
93
+ locals[:depth].push(flag)
94
+ if locals[:depth].size < @options[:take]
95
+ buffer << object.children.collect {|node| tree(node, locals)}.join
96
+ end
97
+ locals[:depth].pop
98
+
99
+ buffer
100
+ end
101
+ end
102
+
103
+ module Stringify
104
+ def to_s_tree(*args, &block)
105
+ Inspector.tree(self, *args, &block)
106
+ end
107
+ end
108
+ end
109
+
110
+ if $0 == __FILE__
111
+ $LOAD_PATH << ".."
112
+ require "tree_support"
113
+ puts TreeSupport.example.to_s_tree(take: 0)
114
+ puts TreeSupport.example.to_s_tree(take: 1)
115
+ puts TreeSupport.example.to_s_tree(take: 2)
116
+ end
@@ -0,0 +1,124 @@
1
+ require 'active_support/core_ext/module/delegation' # for Module#delegate.
2
+
3
+ module TreeSupport
4
+ # Simple node (because it is troublesome to bother to make it on the application side when only wooden structure information is wanted)
5
+ class Node
6
+ include Treeable
7
+ include Stringify
8
+
9
+ attr_accessor :attributes, :parent, :children
10
+
11
+ alias_method :name, :attributes
12
+ alias_method :key, :attributes
13
+
14
+ delegate :[], :[]=, :to_h, to: :attributes
15
+
16
+ def initialize(attributes = nil, &block)
17
+ @attributes = attributes
18
+ @children = []
19
+ if block_given?
20
+ instance_eval(&block)
21
+ end
22
+ end
23
+
24
+ def add(*args, &block)
25
+ tap do
26
+ children << self.class.new(*args, &block).tap do |v|
27
+ v.parent = self
28
+ end
29
+ end
30
+ end
31
+ end
32
+
33
+ def self.example
34
+ Node.new("*root*") do
35
+ add "Battle" do
36
+ add "Attack" do
37
+ add "Shake the sword"
38
+ add "Attack magic" do
39
+ add "Summoned Beast X"
40
+ add "Summoned Beast Y"
41
+ end
42
+ add "Repel sword in length"
43
+ end
44
+ add "Defense"
45
+ end
46
+ add "Withdraw" do
47
+ add "To stop" do
48
+ add "Place a trap"
49
+ add "Shoot a bow and arrow"
50
+ end
51
+ add "To escape"
52
+ end
53
+ add "Break" do
54
+ add "Stop"
55
+ add "Recover" do
56
+ add "Recovery magic"
57
+ add "Drink recovery medicine"
58
+ end
59
+ end
60
+ end
61
+ end
62
+
63
+ # Methods for easily making trees from data like CSV
64
+ class << self
65
+ # Array -> Tree
66
+ #
67
+ # records = [
68
+ # {key: :a, parent: nil},
69
+ # {key: :b, parent: :a},
70
+ # {key: :c, parent: :b},
71
+ # ]
72
+ #
73
+ # puts TreeSupport.records_to_tree(records).to_s_tree
74
+ # a
75
+ # └─b
76
+ # └─c
77
+ # └─d
78
+ #
79
+ # Be sure to have one route
80
+ #
81
+ def records_to_tree(records, key: :key, parent_key: :parent, root_key: nil)
82
+ # Once hashed
83
+ source_hash = records.inject({}) { |a, e| a.merge(e[key] => e) }
84
+ # The node also makes it a hash of only the node having the key
85
+ node_hash = records.inject({}) { |a, e| a.merge(e[key] => Node.new(e[key])) }
86
+ # Link nodes
87
+ node_hash.each_value do |node|
88
+ if parent = source_hash[node.key][parent_key]
89
+ parent_node = node_hash[parent]
90
+ node.parent = parent_node
91
+ parent_node.children << node
92
+ end
93
+ end
94
+
95
+ # If the node whose parent was not set is the root (s)
96
+ roots = node_hash.each_value.find_all {|e| e.parent.nil? }
97
+
98
+ # Specify root_key when there are multiple routes and you are in trouble. Then create a new route and hang it.
99
+ if root_key
100
+ Node.new(root_key).tap do |root|
101
+ roots.each do |e|
102
+ e.parent = root
103
+ root.children << e
104
+ end
105
+ end
106
+ else
107
+ roots
108
+ end
109
+ end
110
+
111
+ # Tree -> Array
112
+ #
113
+ # p TreeSupport.tree_to_records(tree)
114
+ # [
115
+ # {key: :a, parent: nil},
116
+ # {key: :b, parent: :a},
117
+ # {key: :c, parent: :b},
118
+ # ]
119
+ #
120
+ def tree_to_records(root, key: :key, parent_key: :parent)
121
+ root.each_node.collect {|e| {key => e.key, parent_key => e.parent&.key} }
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,9 @@
1
+ module TreeSupport
2
+ class Railtie < Rails::Railtie
3
+ initializer 'tree_support.acts_as_tree.insert_into_active_record' do
4
+ ActiveSupport.on_load :active_record do
5
+ ActiveRecord::Base.include(ArTreeModel)
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,28 @@
1
+ # Tree structure visualization library
2
+ #
3
+ # root = TreeSupport::Node.new("ROOT") do
4
+ # add "A" do
5
+ # add "B" do
6
+ # add "C"
7
+ # end
8
+ # end
9
+ # end
10
+ #
11
+ # puts TreeSupport.tree(root)
12
+ # > ROOT
13
+ # > └─A
14
+ # > └─B
15
+ # > └─C
16
+
17
+ require "tree_support/treeable"
18
+ require "tree_support/inspector"
19
+ require "tree_support/node"
20
+ require "tree_support/ar_tree_model" if defined?(ActiveRecord)
21
+ require "tree_support/railtie" if defined?(Rails)
22
+
23
+ # Do not put gviz when you do not use it to touch Object
24
+ begin
25
+ require "gviz"
26
+ require "tree_support/graphviz_builder"
27
+ rescue LoadError
28
+ end
@@ -0,0 +1,52 @@
1
+ # Definition of methods that are common in tree-structured interfaces
2
+ #
3
+ # All you need is parent and children methods
4
+ #
5
+
6
+ require "active_support/core_ext/module/concerning"
7
+
8
+ module TreeSupport
9
+ concern :Treeable do
10
+ def root
11
+ parent ? parent.root : self
12
+ end
13
+
14
+ def root?
15
+ !parent
16
+ end
17
+
18
+ def leaf?
19
+ children.empty?
20
+ end
21
+
22
+ def each_node(&block)
23
+ return enum_for(__method__) unless block_given?
24
+ yield self
25
+ children.each { |e| e.each_node(&block) }
26
+ end
27
+
28
+ def descendants
29
+ children.flat_map { |e| [e] + e.descendants }
30
+ end
31
+
32
+ def self_and_descendants
33
+ [self] + descendants
34
+ end
35
+
36
+ def ancestors
37
+ self_and_ancestors - [self]
38
+ end
39
+
40
+ def self_and_ancestors
41
+ [self] + (parent ? parent.self_and_ancestors : [])
42
+ end
43
+
44
+ def siblings
45
+ self_and_siblings - [self]
46
+ end
47
+
48
+ def self_and_siblings
49
+ parent ? parent.children : []
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,3 @@
1
+ module TreeSupport
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,2 @@
1
+ require "tree_support/version"
2
+ require "tree_support/tree_support"
@@ -0,0 +1,82 @@
1
+ require "spec_helper"
2
+
3
+ require "rails"
4
+ require "tree_support/ar_tree_model"
5
+ require "tree_support/railtie"
6
+ require "active_record"
7
+
8
+ Class.new(Rails::Application){config.eager_load = true}.initialize!
9
+
10
+ RSpec.describe "ArTreeModel" do
11
+ before do
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 order: "name"
24
+
25
+ def add(name, &block)
26
+ tap do
27
+ child = children.create!(name: name)
28
+ if block_given?
29
+ child.instance_eval(&block)
30
+ end
31
+ end
32
+ end
33
+ end
34
+
35
+ @node = Node.create!(name: "*root*").tap do |n|
36
+ n.instance_eval do
37
+ add "Battle" do
38
+ add "Attack" do
39
+ add "Shake the sword"
40
+ add "Attack magic" do
41
+ add "Summoned Beast X"
42
+ add "Summoned Beast Y"
43
+ end
44
+ add "Repel sword in length"
45
+ end
46
+ add "Defense"
47
+ end
48
+ add "Withdraw" do
49
+ add "To stop" do
50
+ add "Place a trap"
51
+ add "Shoot a bow and arrow"
52
+ end
53
+ add "To escape"
54
+ end
55
+ add "Break" do
56
+ add "Stop"
57
+ add "Recover" do
58
+ add "Recovery magic"
59
+ add "Drink recovery medicine"
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+
66
+ it "roots" do
67
+ Node.roots.should == [@node]
68
+ end
69
+
70
+ it "root" do
71
+ Node.root.should == @node
72
+ end
73
+
74
+ it "to_s_tree" do
75
+ @node.to_s_tree
76
+ end
77
+
78
+ it "safe_destroy_all" do
79
+ Node.safe_destroy_all
80
+ Node.count.should == 0
81
+ end
82
+ end
data/spec/node_spec.rb ADDED
@@ -0,0 +1,52 @@
1
+ require "spec_helper"
2
+
3
+ RSpec.describe "Node" do
4
+ before do
5
+ @records = [
6
+ {key: :a0, parent: nil},
7
+ {key: :a1, parent: :a0},
8
+ {key: :a2, parent: :a1},
9
+ {key: :b0, parent: nil},
10
+ {key: :b1, parent: :b0},
11
+ {key: :b2, parent: :b1},
12
+ ]
13
+ end
14
+
15
+ it "Array -> Tree(s)" do
16
+ TreeSupport.records_to_tree(@records).collect(&:to_s_tree).join.should == <<-EOT
17
+ a0
18
+ └─a1
19
+ └─a2
20
+ b0
21
+ └─b1
22
+ └─b2
23
+ EOT
24
+
25
+ end
26
+
27
+ it "Array -> Tree(1)" do
28
+ TreeSupport.records_to_tree(@records, root_key: :root).to_s_tree.should == <<-EOT
29
+ root
30
+ ├─a0
31
+ │ └─a1
32
+ │ └─a2
33
+ └─b0
34
+ └─b1
35
+ └─b2
36
+ EOT
37
+ end
38
+
39
+ it "Tree -> Array" do
40
+ root = TreeSupport.records_to_tree(@records, root_key: :root)
41
+ records = TreeSupport.tree_to_records(root)
42
+ records.should == [
43
+ {key: :root, parent: nil },
44
+ {key: :a0, parent: :root },
45
+ {key: :a1, parent: :a0 },
46
+ {key: :a2, parent: :a1 },
47
+ {key: :b0, parent: :root },
48
+ {key: :b1, parent: :b0 },
49
+ {key: :b2, parent: :b1 },
50
+ ]
51
+ end
52
+ end
@@ -0,0 +1,8 @@
1
+ $LOAD_PATH.unshift File.expand_path("../../lib", __FILE__)
2
+ require "tree_support"
3
+
4
+ RSpec.configure do |config|
5
+ config.expect_with :rspec do |expectations|
6
+ expectations.syntax = [:should, :expect]
7
+ end
8
+ end
@@ -0,0 +1,28 @@
1
+ require "spec_helper"
2
+
3
+ RSpec.describe "TreeSupport" do
4
+ it "tree" do
5
+ expected = <<-EOT
6
+ *root*
7
+ ├─Battle
8
+ │ ├─Attack
9
+ │ │ ├─Shake the sword
10
+ │ │ ├─Attack magic
11
+ │ │ │ ├─Summoned Beast X
12
+ │ │ │ └─Summoned Beast Y
13
+ │ │ └─Repel sword in length
14
+ │ └─Defense
15
+ ├─Withdraw
16
+ │ ├─To stop
17
+ │ │ ├─Place a trap
18
+ │ │ └─Shoot a bow and arrow
19
+ │ └─To escape
20
+ └─Break
21
+ ├─Stop
22
+ └─Recover
23
+ ├─Recovery magic
24
+ └─Drink recovery medicine
25
+ EOT
26
+ TreeSupport.example.to_s_tree.should == expected
27
+ end
28
+ end
@@ -0,0 +1,59 @@
1
+ require "spec_helper"
2
+
3
+ RSpec.describe "Treeable" do
4
+ before do
5
+ @root = TreeSupport::Node.new("*root*") do
6
+ add "a" do
7
+ add "a1"
8
+ add "a2" do
9
+ add "x"
10
+ end
11
+ add "a3"
12
+ end
13
+ end
14
+ @a2 = @root.each_node.find {|e| e.name == "a2"}
15
+ @leaf = @root.each_node.find {|e| e.name == "x"}
16
+ end
17
+
18
+ it "root" do
19
+ @a2.root.name.should == "*root*"
20
+ end
21
+
22
+ it "root?" do
23
+ @root.root?.should == true
24
+ @leaf.root?.should == false
25
+ end
26
+
27
+ it "leaf?" do
28
+ @root.leaf?.should == false
29
+ @leaf.leaf?.should == true
30
+ end
31
+
32
+ it "each_node" do
33
+ @root.each_node.collect(&:name).should == ["*root*", "a", "a1", "a2", "x", "a3"]
34
+ end
35
+
36
+ it "descendants" do
37
+ @root.descendants.collect(&:name).should == ["a", "a1", "a2", "x", "a3"]
38
+ end
39
+
40
+ it "self_and_descendants" do
41
+ @root.self_and_descendants.collect(&:name).should == ["*root*", "a", "a1", "a2", "x", "a3"]
42
+ end
43
+
44
+ it "ancestors" do
45
+ @a2.ancestors.collect(&:name).should == ["a", "*root*"]
46
+ end
47
+
48
+ it "self_and_ancestors" do
49
+ @a2.self_and_ancestors.collect(&:name).should == ["a2", "a", "*root*"]
50
+ end
51
+
52
+ it "siblings" do
53
+ @a2.siblings.collect(&:name).should == ["a1", "a3"]
54
+ end
55
+
56
+ it "self_and_siblings" do
57
+ @a2.self_and_siblings.collect(&:name).should == ["a1", "a2", "a3"]
58
+ end
59
+ end
@@ -0,0 +1,30 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'tree_support/version'
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = "tree_support"
8
+ s.version = TreeSupport::VERSION
9
+ s.author = "akicho8"
10
+ s.email = "akicho8@gmail.com"
11
+ s.homepage = "https://github.com/akicho8/tree_support"
12
+ s.summary = "Tree structure visualization function library"
13
+ s.description = "Tree structure visualization function library"
14
+ s.platform = Gem::Platform::RUBY
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.require_paths = ["lib"]
20
+ s.rdoc_options = ["--line-numbers", "--inline-source", "--charset=UTF-8", "--diagram", "--image-format=jpg"]
21
+
22
+ s.add_dependency "activesupport"
23
+
24
+ s.add_development_dependency "rake"
25
+ s.add_development_dependency "rspec"
26
+ s.add_development_dependency "rails"
27
+ s.add_development_dependency "activerecord"
28
+ s.add_development_dependency "sqlite3"
29
+ s.add_development_dependency "gviz"
30
+ end