traversal 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,6 @@
1
+ *.gem
2
+ .bundle
3
+ .idea
4
+ Gemfile.lock
5
+ pkg/*
6
+ coverage/*
data/Gemfile ADDED
@@ -0,0 +1,9 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in traversal.gemspec
4
+ gemspec
5
+
6
+ group :development do
7
+ gem 'rspec'
8
+ gem 'simplecov'
9
+ end
data/README.rdoc ADDED
@@ -0,0 +1,96 @@
1
+ == Synopsys
2
+ Simple traversal API for pure Ruby objects. Also it can be used it with ActiveRecord or DataMapper (or any other ORM).
3
+
4
+ == Installation
5
+ Install it via rubygems:
6
+ gem install traversal
7
+
8
+ In your Gemfile:
9
+ gem 'traversal'
10
+
11
+ == Usage
12
+ Imagine tree(-ish) structure:
13
+ plants:
14
+ vegetables:
15
+ - cucumber
16
+ - tomato
17
+ fruits:
18
+ - apple
19
+ - banana
20
+
21
+ In Ruby it'll look like this:
22
+ class Node
23
+ attr_reader :name, :children
24
+
25
+ def initialize(name)
26
+ @name = name
27
+ @children = []
28
+ end
29
+ end
30
+
31
+ # lets recreate tree structure from above
32
+ root = Node.new('plants')
33
+ veg = Node.new('vegetables')
34
+ fruits = Node.new('fruits')
35
+ cucumber = Node.new('cucumber')
36
+ tomato = Node.new('tomato')
37
+ apple = Node.new('apple')
38
+ banana = Node.new('banana')
39
+
40
+ root.children << veg << fruits
41
+ veg.children << cucumber << tomato
42
+ fruits.children << apple << banana
43
+
44
+ So, we have a simple tree with <code>root</code> element on the top of it.
45
+ Now let's create a <b>traversal description</b>.
46
+ require 'traversal'
47
+
48
+ traversal = Traversal::Description.new
49
+ traversal.traverse(root). # start from root node
50
+ follow(:children) # move forward via children relations
51
+
52
+ It's a minimal traversal description. It has <b>start node</b> and <b>relation</b> pointer (<code>children</code>, in this case).
53
+ Traversal description is <code>Enumerable</code> object. Let's examine our traverse:
54
+ traversal.map { |n| n.name } # should be equal to [root, vegetables, cucumber, tomato, fruits, apple, banana]
55
+
56
+ Let's look closer:
57
+ 1. We are starting from <code>root</code> node. It's first element.
58
+ 2. Traversal cursor moves to the first child of <code>root</code>: <code>vegetables</code>
59
+ 3. By default cursor moves deeper, to first child of <code>vegetables</code> node: <code>cucumber</code>
60
+ 4. <code>cucumber</code> has no children, traversal cursor moves to the next child of <code>vegetables</code>: <code>tomato</code>
61
+ 5. Traversal cursor moves to the next child of <code>root</code> - <code>fruits</code>
62
+ 6. Traversal cursor visits children of <code>fruits</code>: <code>apple</code> and <code>banana</code>
63
+ 7. All nodes are visited, cursor closed.
64
+
65
+ If you want the cursor to visit all children before visiting grandchildren, then you have to declare <code>breadth_first</code> traversal visiting strategy:
66
+ traversal.breadth_first # in opposite of traversal.depth_first
67
+
68
+ You can exclude nodes (but allow cursor to follow relations) from final result:
69
+ traversal.exclude { |node| node.children.length > 0 } # all nodes with children will be excluded from result
70
+
71
+ You can prune away any node children (but leave the node in final result):
72
+ traversal.prune { |node| node.name == "vegetables" }.
73
+ map(&:name) # will produce [root, vegetables, fruits, apple, banana]
74
+
75
+ Or, you can exclude node and prune away it's children:
76
+ traversal.prune_and_exclude { |node| node.name == "vegetables" }.
77
+ map(&:name) # will produce [root, fruits, apple, banana]
78
+
79
+ Also, you can mark any node as "loop terminator":
80
+ traversal.stop_before { |node| node.name == "vegetables" }.to_a # will produce only [root]
81
+ traversal.stop_after { |node| node.name == "vegetables" }.to_a # will produce [root, vegetables]
82
+
83
+ == Real world example
84
+ require "traversable"
85
+
86
+ class Page < ActiveRecord::Base
87
+ acts_as_tree
88
+ acts_as_traversable
89
+ end
90
+
91
+ lim = 15
92
+
93
+ Page.root.traverse(:children).
94
+ exclude(:root?).
95
+ exclude_and_prune(:is_deleted?).
96
+ stop_after { (lmt -= 1) == 0 }
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/lib/traversal.rb ADDED
@@ -0,0 +1,11 @@
1
+ require "traversal/version"
2
+
3
+ module Traversal
4
+ autoload :Description, "traversal/description"
5
+ autoload :Iterator, "traversal/iterator"
6
+ autoload :ActsAsTraversable, "traversal/acts_as_traversable"
7
+
8
+ class IncompleteDescription < Exception; end
9
+ end
10
+
11
+ Object.extend(Traversal::ActsAsTraversable)
@@ -0,0 +1,39 @@
1
+ module Traversal
2
+ module ActsAsTraversable
3
+ # == Synopsys
4
+ # Mix-in <tt>#traverse</tt> method
5
+ #
6
+ # == Example
7
+ # class TreeNode
8
+ # attr_accessor :siblings
9
+ #
10
+ # acts_as_traversable
11
+ # end
12
+ #
13
+ # t = TreeNode.new
14
+ # t.traverse # equivalent to Traversal::Description.new.traverse(t)
15
+ # t.traverse(:siblings) # equivalent to Traversal::Description.new.traverse(t).follow(:siblings)
16
+ def acts_as_traversable
17
+ include InstanceMethods
18
+ end
19
+ alias acts_like_traversable acts_as_traversable
20
+
21
+ module InstanceMethods
22
+ # Shortcut method, simplified interface to Traversal::Description
23
+ #
24
+ # == Example
25
+ # class TreeNode
26
+ # attr_accessor :siblings
27
+ #
28
+ # acts_as_traversable
29
+ # end
30
+ #
31
+ # t = TreeNode.new
32
+ # t.traverse # equivalent to Traversal::Description.new.traverse(t)
33
+ # t.traverse(:siblings) # equivalent to Traversal::Description.new.traverse(t).follow(:siblings)
34
+ def traverse(relation = nil)
35
+ Traversal::Description.new.traverse(self).tap { |desc| desc.follow(relation) if relation }
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,133 @@
1
+ # coding: utf-8
2
+ module Traversal
3
+ # Traversal description
4
+ class Description
5
+ include Enumerable
6
+
7
+ DEPTH_FIRST = 0
8
+ BREADTH_FIRST = 1
9
+
10
+ attr_reader :start_node, :relation
11
+
12
+ # Create blank traversal description
13
+ def initialize
14
+ @exclude = []
15
+ @prune = []
16
+ @stop_before = []
17
+ @stop_after = []
18
+
19
+ @start_node = nil
20
+ @relation = nil
21
+
22
+ @order = DEPTH_FIRST
23
+ end
24
+
25
+ # Declare a traversal start point. From which node you want to follow relations?
26
+ def traverse(start_node)
27
+ tap { @start_node = start_node }
28
+ end
29
+
30
+ # Declare which relation you want to follow in your traversal.
31
+ # It can be Symbol, Method, Proc or block.
32
+ #
33
+ # Relation should return something enumerable, otherwise it will be ignored in traversal.
34
+ #
35
+ # == Example
36
+ # traversal.follow(:children) # for each node will call node#children method
37
+ # traversal.follow { |node| node.children } # same effect
38
+ def follow(relation = nil, &blk)
39
+ tap { @relation = condition("Relation", relation, &blk) }
40
+ end
41
+
42
+ # Declare exclude condition. Which nodes you want to
43
+ # exclude (ignore them but not their relations) from your traversal?
44
+ def exclude(cond = nil, &blk)
45
+ tap { @exclude << condition("Exclude condition", cond, &blk) }
46
+ end
47
+
48
+ # Declare prune condition. Which nodes relations you want to ignore?
49
+ #
50
+ # Example:
51
+ # traversal.follow(:children).
52
+ # prune { |node| node.name == "A" } # node "A" will be included, but not its children
53
+ def prune(cond = nil, &blk)
54
+ tap { @prune << condition("Prune condition", cond, &blk) }
55
+ end
56
+
57
+ # Declare exclude AND prune condition.
58
+ # Matching node and its relations will be excluded from traversal.
59
+ def exclude_and_prune(cond = nil, &blk)
60
+ exclude(cond, &blk)
61
+ prune(cond, &blk)
62
+ end
63
+ alias prune_and_exclude exclude_and_prune
64
+
65
+ # Declare +stop pre-condition+.
66
+ # When met, matched node will be excluded from traversal and iteration will be stopped.
67
+ def stop_before(cond = nil, &blk)
68
+ tap { @stop_before << condition("Stop condition", cond, &blk) }
69
+ end
70
+
71
+ # Declare +stop post-condition+.
72
+ # When met, matched node will be included in traversal and iteration will be stopped.
73
+ def stop_after(cond = nil, &blk)
74
+ tap { @stop_after << condition("Stop condition", cond, &blk) }
75
+ end
76
+
77
+ # Declare traversal order strategy as +depth first+
78
+ def depth_first
79
+ tap { @order = DEPTH_FIRST }
80
+ end
81
+
82
+ # Declare traversal order strategy as +breadth first+
83
+ def breadth_first
84
+ tap { @order = BREADTH_FIRST }
85
+ end
86
+
87
+ def each
88
+ assert_complete_description
89
+
90
+ iter = Traversal::Iterator.new(self)
91
+
92
+ if iterator?
93
+ iter.each do |node|
94
+ yield node
95
+ end
96
+ else
97
+ iter
98
+ end
99
+ end
100
+
101
+ # Predicates section
102
+
103
+ # Does node matches one of stop conditions?
104
+ def stop?(node, type = :before) #:nodoc:
105
+ (type == :after ? @stop_after : @stop_before).any? { |cond| cond[node] }
106
+ end
107
+
108
+ def exclude?(node) #:nodoc:
109
+ @exclude.any? { |cond| cond[node] }
110
+ end
111
+
112
+ def prune?(node) #:nodoc:
113
+ @prune.any? { |cond| cond[node] }
114
+ end
115
+
116
+ def breadth_first? #:nodoc:
117
+ @order == BREADTH_FIRST
118
+ end
119
+
120
+ private
121
+ def condition(name, arg, &blk) #:nodoc:
122
+ raise TypeError, "#{name} must be Symbol, Method, Proc or block" unless
123
+ (arg ||= blk).respond_to?(:to_proc)
124
+
125
+ arg.to_proc
126
+ end
127
+
128
+ def assert_complete_description #:nodoc:
129
+ raise IncompleteDescription, "Traversal description should contain start node. Use #traverse method" unless @start_node
130
+ raise IncompleteDescription, "Traversal description should contain relation. Use #follow method" unless @relation
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,90 @@
1
+ # coding: utf-8
2
+
3
+ require "enumerator"
4
+
5
+ module Traversal
6
+ # Traversal iterator.
7
+ class Iterator < Enumerator
8
+ # Create new traversal iterator from traversal description
9
+ def initialize(description)
10
+ raise TypeError,
11
+ 'Traversal::Description expected, %s given' % description.class.name \
12
+ unless description.is_a?(Traversal::Description)
13
+
14
+ @description = description
15
+ start_node = @description.start_node
16
+
17
+ # Create Enumerator
18
+ super() do |yielder|
19
+ @yielder = yielder
20
+
21
+ begin
22
+ yield_node(start_node)
23
+
24
+ expand_node(start_node)
25
+ rescue StopIteration
26
+ # ignore
27
+ end
28
+ end
29
+ end
30
+
31
+ private
32
+ def push(*args) #:nodoc:
33
+ @yielder.yield(*args)
34
+ end
35
+
36
+ def yield_node(node) #:nodoc:
37
+ # check stop pre-condition
38
+ raise StopIteration if @description.stop?(node, :before)
39
+
40
+ # do yield
41
+ push(node) unless @description.exclude?(node)
42
+
43
+ # check stop post-condition
44
+ raise StopIteration if @description.stop?(node, :after)
45
+ end
46
+
47
+ # Expand node
48
+ def expand_node(node) #:nodoc:
49
+ if @description.breadth_first?
50
+ expand_breadth(node)
51
+ else
52
+ expand_depth(node)
53
+ end
54
+ end
55
+
56
+ # Expand node with DEPTH_FIRST strategy
57
+ def expand_depth(node) #:nodoc:
58
+ relations_for(node).each do |rel|
59
+ yield_node(rel)
60
+
61
+ expand_node(rel) unless @description.prune?(rel)
62
+ end
63
+ end
64
+
65
+ # Expand node with BREADTH_FIRST strategy
66
+ def expand_breadth(node) #:nodoc:
67
+ cached_relations = []
68
+
69
+ # 1. yield direct relations first
70
+ relations_for(node).each do |rel|
71
+ yield_node(rel)
72
+
73
+ # memoize relation for next iteration
74
+ cached_relations << rel unless @description.prune?(rel)
75
+ end
76
+
77
+ # 2. dig deeper
78
+ cached_relations.each do |rel|
79
+ expand_breadth(rel)
80
+ end
81
+ end
82
+
83
+ # Expand relations for node
84
+ def relations_for(node) #:nodoc:
85
+ relation = @description.relation[node]
86
+
87
+ relation.is_a?(Enumerable) ? relation : []
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,3 @@
1
+ module Traversal
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,8 @@
1
+ require "rubygems"
2
+ require "bundler"
3
+ Bundler.setup
4
+
5
+ require 'simplecov'
6
+
7
+ SimpleCov.start
8
+ require "traversal"
@@ -0,0 +1,151 @@
1
+ require File.expand_path('../test_helper', __FILE__)
2
+
3
+ # Tree structure:
4
+ #
5
+ # +root+
6
+ # / \
7
+ # +a+ +b+
8
+ # / \ \
9
+ # +c+ +d+ +f+
10
+ # / / \
11
+ # +e+ +g+ +h+
12
+
13
+ class Node
14
+ attr_accessor :children
15
+ attr_accessor :level
16
+ attr_accessor :id
17
+ acts_as_traversable
18
+
19
+ def initialize(id, level)
20
+ @id = id
21
+ @level = level
22
+ @children = []
23
+ end
24
+
25
+ def inspect
26
+ "#<Node: #{id.to_s}>"
27
+ end
28
+ end
29
+
30
+ # create tree
31
+ root = Node.new(0, 0)
32
+ a = Node.new(1, 1)
33
+ b = Node.new(2, 1)
34
+ c = Node.new(3, 2)
35
+ d = Node.new(4, 2)
36
+ e = Node.new(5, 3)
37
+ f = Node.new(6, 2)
38
+ g = Node.new(7, 3)
39
+ h = Node.new(8, 3)
40
+
41
+ # link nodes
42
+ root.children << a << b
43
+ a.children << c << d
44
+ c.children << e
45
+ b.children << f
46
+ f.children << g << h
47
+
48
+ describe Traversal do
49
+ let(:iter) { Traversal::Description.new }
50
+
51
+ it "should traverse all descendants" do
52
+ # traverse whole tree, with default strategy (depth first)
53
+ iter.follow(:children).
54
+ traverse(root).
55
+ to_a.
56
+ should eq([root, a, c, e, d, b, f, g, h])
57
+ end
58
+
59
+ it "should exclude some nodes" do
60
+ # this traverse will exclude only +c+ node, but not it's children
61
+ iter.traverse(root).
62
+ follow(:children).
63
+ exclude { |node| node == c }.
64
+ to_a.should eq([root, a, e, d, b, f, g, h])
65
+ end
66
+
67
+ it "should disjunct excludes" do
68
+ # this traverse will exclude +c+ and +d+ nodes, but not their children
69
+ iter.traverse(root).
70
+ follow(:children).
71
+ exclude { |node| node == c }.
72
+ exclude { |node| node == d }.
73
+ to_a.should eq([root, a, e, b, f, g, h])
74
+ end
75
+
76
+ it "should prune some nodes" do
77
+ # this traverse will exclude all +c+ children
78
+ iter.traverse(root).
79
+ follow(:children).
80
+ prune { |node| node == c }.
81
+ to_a.should eq([root, a, c, d, b, f, g, h])
82
+ end
83
+
84
+ it "should disjunct prunes" do
85
+ # this traverse will exclude all +c+ and +f+ children
86
+ iter.traverse(root).
87
+ follow(:children).
88
+ prune { |node| node == c }.
89
+ prune { |node| node == f }.
90
+ to_a.should eq([root, a, c, d, b, f])
91
+ end
92
+
93
+ it "should exclude and prune some nodes" do
94
+ # this traverse will exclude +c+ and its children
95
+ iter.traverse(root).
96
+ follow(:children).
97
+ exclude_and_prune { |node| node == c }.
98
+ to_a.should eq([root, a, d, b, f, g, h])
99
+ end
100
+
101
+ it "should stop traversal after some condition met" do
102
+ # this traversal will stop after visiting +d+ node
103
+ iter.traverse(root).
104
+ follow(:children).
105
+ stop_after { |node| node == d }.
106
+ to_a.should eq([root, a, c, e, d])
107
+ end
108
+
109
+ it "should stop traversal before some condition met" do
110
+ # this traversal will stop before visiting +d+ node
111
+ iter.traverse(root).
112
+ follow(:children).
113
+ stop_before { |node| node == d }.
114
+ to_a.should eq([root, a, c, e])
115
+ end
116
+
117
+ it "should traverse with depth_first strategy" do
118
+ iter.traverse(root).
119
+ follow(:children).
120
+ depth_first.
121
+ exclude { |node| node.level == 3 }.
122
+ to_a.should eq([root, a, c, d, b, f])
123
+ end
124
+
125
+ it "should traverse with breadth_first strategy" do
126
+ iter.traverse(root).
127
+ follow(:children).
128
+ breadth_first.
129
+ exclude { |node| node.level == 3 }.
130
+ to_a.should eq([root, a, b, c, d, f])
131
+ end
132
+
133
+ it "should raise exception when no start node given" do
134
+ lambda { iter.follow(:children).to_a }.should raise_error(Traversal::IncompleteDescription)
135
+ end
136
+
137
+ it "should raise exception when no relations given" do
138
+ lambda { iter.traverse(root).to_a }.should raise_error(Traversal::IncompleteDescription)
139
+ end
140
+
141
+ it "should have shortcut" do
142
+ root.should respond_to(:traverse)
143
+
144
+ root.traverse(:children).count.should eq(9)
145
+ end
146
+
147
+ it "should return Enumerable when called without block" do
148
+ iter.traverse(root).follow(:children).each.should be_a(Enumerable)
149
+
150
+ end
151
+ end
data/traversal.gemspec ADDED
@@ -0,0 +1,24 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "traversal/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "traversal"
7
+ s.version = Traversal::VERSION
8
+ s.authors = ["Alexey Mikhaylov"]
9
+ s.email = ["amikhailov83@gmail.com"]
10
+ s.homepage = "https://github.com/take-five/traversal"
11
+ s.summary = %q{Simple traversal API for pure Ruby objects}
12
+ s.date = "2012-01-22"
13
+
14
+ s.rubyforge_project = "traversal"
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.require_paths = ["lib"]
20
+
21
+ s.add_development_dependency "rspec"
22
+ s.add_development_dependency "simplecov"
23
+ # s.add_runtime_dependency "rest-client"
24
+ end
metadata ADDED
@@ -0,0 +1,101 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: traversal
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 0
8
+ - 1
9
+ version: 0.0.1
10
+ platform: ruby
11
+ authors:
12
+ - Alexey Mikhaylov
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2012-01-22 00:00:00 +06:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: rspec
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ segments:
29
+ - 0
30
+ version: "0"
31
+ type: :development
32
+ version_requirements: *id001
33
+ - !ruby/object:Gem::Dependency
34
+ name: simplecov
35
+ prerelease: false
36
+ requirement: &id002 !ruby/object:Gem::Requirement
37
+ none: false
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ segments:
42
+ - 0
43
+ version: "0"
44
+ type: :development
45
+ version_requirements: *id002
46
+ description:
47
+ email:
48
+ - amikhailov83@gmail.com
49
+ executables: []
50
+
51
+ extensions: []
52
+
53
+ extra_rdoc_files: []
54
+
55
+ files:
56
+ - .gitignore
57
+ - Gemfile
58
+ - README.rdoc
59
+ - Rakefile
60
+ - lib/traversal.rb
61
+ - lib/traversal/acts_as_traversable.rb
62
+ - lib/traversal/description.rb
63
+ - lib/traversal/iterator.rb
64
+ - lib/traversal/version.rb
65
+ - spec/test_helper.rb
66
+ - spec/traversal_spec.rb
67
+ - traversal.gemspec
68
+ has_rdoc: true
69
+ homepage: https://github.com/take-five/traversal
70
+ licenses: []
71
+
72
+ post_install_message:
73
+ rdoc_options: []
74
+
75
+ require_paths:
76
+ - lib
77
+ required_ruby_version: !ruby/object:Gem::Requirement
78
+ none: false
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ segments:
83
+ - 0
84
+ version: "0"
85
+ required_rubygems_version: !ruby/object:Gem::Requirement
86
+ none: false
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ segments:
91
+ - 0
92
+ version: "0"
93
+ requirements: []
94
+
95
+ rubyforge_project: traversal
96
+ rubygems_version: 1.3.7
97
+ signing_key:
98
+ specification_version: 3
99
+ summary: Simple traversal API for pure Ruby objects
100
+ test_files: []
101
+