tree_support 0.1.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.
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