traversal 0.0.1

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.
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
+