traversal 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -31,8 +31,8 @@ module Traversal
31
31
  # t = TreeNode.new
32
32
  # t.traverse # equivalent to Traversal::Description.new.traverse(t)
33
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 }
34
+ def traverse(*relations)
35
+ Traversal::Description.new.traverse(self).tap { |desc| desc.follow(*relations) unless relations.empty? }
36
36
  end
37
37
  end
38
38
  end
@@ -1,25 +1,49 @@
1
1
  # coding: utf-8
2
+
3
+ require "forwardable"
4
+
2
5
  module Traversal
3
6
  # Traversal description
4
7
  class Description
8
+ class EmptyArgument; end
9
+
10
+ extend Forwardable
5
11
  include Enumerable
6
12
 
7
13
  DEPTH_FIRST = 0
8
14
  BREADTH_FIRST = 1
9
15
 
10
- attr_reader :start_node, :relation
16
+ attr_reader :start_node, :relations
17
+ def_delegators :each, :[], :at, :empty?,
18
+ :fetch, :find_index, :index,
19
+ :last, :reverse, :values_at
11
20
 
12
21
  # Create blank traversal description
13
22
  def initialize
14
- @exclude = []
15
- @prune = []
16
- @stop_before = []
17
- @stop_after = []
23
+ @exclude = []
24
+ @include_only = []
25
+
26
+ @prune = []
27
+ @expand_only = []
28
+ @stop_before = []
29
+ @stop_after = []
18
30
 
19
- @start_node = nil
20
- @relation = nil
31
+ @start_node = nil
32
+ @relations = []
21
33
 
22
- @order = DEPTH_FIRST
34
+ @order = DEPTH_FIRST
35
+ @uniq = true
36
+ end
37
+
38
+ # Tests equality of traversal descriptions
39
+ def ==(other)
40
+ return super unless other.is_a?(Description)
41
+
42
+ [:@start_node, :@relations, :@include_only,
43
+ :@exclude, :@prune, :@stop_before, :@uniq,
44
+ :@stop_after, :@order, :@expand_only].all? do |sym|
45
+ instance_variable_get(sym) == other.instance_variable_get(sym)
46
+ end
23
47
  end
24
48
 
25
49
  # Declare a traversal start point. From which node you want to follow relations?
@@ -35,14 +59,34 @@ module Traversal
35
59
  # == Example
36
60
  # traversal.follow(:children) # for each node will call node#children method
37
61
  # traversal.follow { |node| node.children } # same effect
38
- def follow(relation = nil, &blk)
39
- tap { @relation = condition("Relation", relation, &blk) }
62
+ def follow(*relations, &blk)
63
+ raise ArgumentError, 'arguments or block expected' if relations.empty? && !block_given?
64
+
65
+ tap do
66
+ relations << blk if block_given?
67
+
68
+ relations.each do |relation|
69
+ @relations << condition(relation)
70
+ end
71
+ #@relations = condition(*relations, &blk)
72
+ end
40
73
  end
41
74
 
42
75
  # Declare exclude condition. Which nodes you want to
43
76
  # exclude (ignore them but not their relations) from your traversal?
44
- def exclude(cond = nil, &blk)
45
- tap { @exclude << condition("Exclude condition", cond, &blk) }
77
+ def exclude(*nodes, &blk)
78
+ tap { @exclude << condition(*nodes, &blk) }
79
+ end
80
+
81
+ # Declare inverted exclude condition. Which nodes you want to keep?
82
+ def include_only(*nodes, &blk)
83
+ tap { @include_only << condition(*nodes, &blk) }
84
+ end
85
+ alias exclude_unless include_only
86
+
87
+ # Declare which nodes you want to expand. Others will be pruned.
88
+ def expand_only(*nodes, &blk)
89
+ tap { @expand_only << condition(*nodes, &blk) }
46
90
  end
47
91
 
48
92
  # Declare prune condition. Which nodes relations you want to ignore?
@@ -50,28 +94,28 @@ module Traversal
50
94
  # Example:
51
95
  # traversal.follow(:children).
52
96
  # 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) }
97
+ def prune(*nodes, &blk)
98
+ tap { @prune << condition(*nodes, &blk) }
55
99
  end
56
100
 
57
101
  # Declare exclude AND prune condition.
58
102
  # 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)
103
+ def exclude_and_prune(*nodes, &blk)
104
+ exclude(*nodes, &blk)
105
+ prune(*nodes, &blk)
62
106
  end
63
107
  alias prune_and_exclude exclude_and_prune
64
108
 
65
109
  # Declare +stop pre-condition+.
66
110
  # 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) }
111
+ def stop_before(*nodes, &blk)
112
+ tap { @stop_before << condition(*nodes, &blk) }
69
113
  end
70
114
 
71
115
  # Declare +stop post-condition+.
72
116
  # 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) }
117
+ def stop_after(*nodes, &blk)
118
+ tap { @stop_after << condition(*nodes, &blk) }
75
119
  end
76
120
 
77
121
  # Declare traversal order strategy as +depth first+
@@ -84,7 +128,14 @@ module Traversal
84
128
  tap { @order = BREADTH_FIRST }
85
129
  end
86
130
 
87
- def each
131
+ # Set uniqueness behaviour
132
+ # By default it is set to +true+
133
+ def uniq(v = true)
134
+ tap { @uniq = !!v }
135
+ end
136
+
137
+ # Iterate through nodes defined by DSL and optionally execute given +block+ for each node.
138
+ def each # :yields: node
88
139
  assert_complete_description
89
140
 
90
141
  iter = Traversal::Iterator.new(self)
@@ -105,29 +156,60 @@ module Traversal
105
156
  (type == :after ? @stop_after : @stop_before).any? { |cond| cond[node] }
106
157
  end
107
158
 
159
+ def include?(node) #:nodoc:
160
+ @include_only.all? { |cond| cond[node] } &&
161
+ @exclude.none? { |cond| cond[node] }
162
+ end
163
+
108
164
  def exclude?(node) #:nodoc:
109
- @exclude.any? { |cond| cond[node] }
165
+ !include?(node)
166
+ end
167
+
168
+ def expand?(node) #:nodoc:
169
+ @expand_only.all? { |cond| cond[node] } &&
170
+ @prune.none? { |cond| cond[node] }
110
171
  end
111
172
 
112
173
  def prune?(node) #:nodoc:
113
- @prune.any? { |cond| cond[node] }
174
+ !expand?(node)
114
175
  end
115
176
 
116
177
  def breadth_first? #:nodoc:
117
178
  @order == BREADTH_FIRST
118
179
  end
119
180
 
181
+ def uniq? #:nodoc:
182
+ @uniq
183
+ end
184
+
120
185
  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)
186
+ def condition(*args, &blk) #:nodoc:
187
+ # on empty argument use given block
188
+ args << blk if block_given?
189
+ raise ArgumentError, 'arguments or block expected' if args.empty?
124
190
 
125
- arg.to_proc
191
+ args.length == 1 ? arg_to_proc(args.first) : args_to_proc(args)
126
192
  end
127
193
 
128
194
  def assert_complete_description #:nodoc:
129
195
  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
196
+ raise IncompleteDescription, "Traversal description should contain relation(s). Use #follow method" if @relations.empty?
197
+ end
198
+
199
+ def args_to_proc(args)
200
+ procs = args.map { |arg| arg_to_proc(arg) }
201
+ proc { |node| procs.any? { |pr| pr[node] } }
202
+ end
203
+
204
+ # convert argument to callable proc
205
+ def arg_to_proc(arg) #:nodoc:
206
+ return arg.to_proc if arg.respond_to?(:to_proc)
207
+
208
+ [:===, :==, :eql?].each do |meth|
209
+ return arg.method(meth) if arg.respond_to?(meth)
210
+ end
211
+
212
+ raise TypeError, 'argument must respond to one of the following method: #to_proc, #===, #==, #eql?'
131
213
  end
132
214
  end
133
215
  end
@@ -1,10 +1,17 @@
1
1
  # coding: utf-8
2
2
 
3
3
  require "enumerator"
4
+ require "forwardable"
4
5
 
5
6
  module Traversal
6
7
  # Traversal iterator.
7
- class Iterator < Enumerator
8
+ class Iterator < Enumerator #:nodoc: all
9
+ extend Forwardable
10
+
11
+ def_delegators :to_ary!, :[], :at, :empty?,
12
+ :fetch, :find_index, :index,
13
+ :last, :reverse, :values_at
14
+
8
15
  # Create new traversal iterator from traversal description
9
16
  def initialize(description)
10
17
  raise TypeError,
@@ -14,23 +21,33 @@ module Traversal
14
21
  @description = description
15
22
  start_node = @description.start_node
16
23
 
17
- # Create Enumerator
18
- super() do |yielder|
24
+ # Map of visited nodes
25
+ @visited = {}
26
+
27
+ # Create underlying Enumerator
28
+ @enumerator = Enumerator.new do |yielder|
19
29
  @yielder = yielder
20
30
 
21
31
  begin
22
32
  yield_node(start_node)
23
33
 
24
- expand_node(start_node)
34
+ expand_node(start_node) if @description.expand?(start_node)
25
35
  rescue StopIteration
26
36
  # ignore
27
37
  end
28
38
  end
39
+
40
+ # Wrap underlying enumerator
41
+ super() do |y|
42
+ @enumerator.each { |e| y << e }
43
+ end
29
44
  end
30
45
 
31
46
  private
32
- def push(*args) #:nodoc:
33
- @yielder.yield(*args)
47
+ def push(node) #:nodoc:
48
+ @visited[node] = true if @description.uniq? # memo visited node
49
+
50
+ @yielder.yield(node)
34
51
  end
35
52
 
36
53
  def yield_node(node) #:nodoc:
@@ -38,7 +55,7 @@ module Traversal
38
55
  raise StopIteration if @description.stop?(node, :before)
39
56
 
40
57
  # do yield
41
- push(node) unless @description.exclude?(node)
58
+ push(node) unless @description.exclude?(node) || visited?(node)
42
59
 
43
60
  # check stop post-condition
44
61
  raise StopIteration if @description.stop?(node, :after)
@@ -82,9 +99,28 @@ module Traversal
82
99
 
83
100
  # Expand relations for node
84
101
  def relations_for(node) #:nodoc:
85
- relation = @description.relation[node]
102
+ Enumerator.new do |yielder|
103
+ @description.relations.each do |relation_accessor|
104
+ begin
105
+ relations = relation_accessor[node]
106
+ enumerable = relations.is_a?(Enumerable) ? relations : [relations].compact
107
+
108
+ enumerable.each { |e| yielder << e unless visited?(e) }
109
+ rescue NoMethodError
110
+ # ignore errors on relation_accessor[node]
111
+ end
112
+ end
113
+ end
114
+ end
115
+
116
+ def visited?(node) #:nodoc:
117
+ @description.uniq? && @visited.key?(node)
118
+ end
86
119
 
87
- relation.is_a?(Enumerable) ? relation : []
120
+ # convert underlying enumerator to array
121
+ def to_ary! #:nodoc:
122
+ @enumerator = @enumerator.to_a unless @enumerator.is_a?(Array)
123
+ @enumerator
88
124
  end
89
- end
90
- end
125
+ end # module Iterator
126
+ end # module Traversal
@@ -1,3 +1,3 @@
1
1
  module Traversal
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2"
3
3
  end
metadata CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
5
5
  segments:
6
6
  - 0
7
7
  - 0
8
- - 1
9
- version: 0.0.1
8
+ - 2
9
+ version: 0.0.2
10
10
  platform: ruby
11
11
  authors:
12
12
  - Alexey Mikhaylov
@@ -14,7 +14,7 @@ autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
16
 
17
- date: 2012-01-22 00:00:00 +06:00
17
+ date: 2012-01-26 00:00:00 +06:00
18
18
  default_executable:
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
@@ -53,18 +53,11 @@ extensions: []
53
53
  extra_rdoc_files: []
54
54
 
55
55
  files:
56
- - .gitignore
57
- - Gemfile
58
- - README.rdoc
59
- - Rakefile
60
56
  - lib/traversal.rb
61
57
  - lib/traversal/acts_as_traversable.rb
62
58
  - lib/traversal/description.rb
63
59
  - lib/traversal/iterator.rb
64
60
  - lib/traversal/version.rb
65
- - spec/test_helper.rb
66
- - spec/traversal_spec.rb
67
- - traversal.gemspec
68
61
  has_rdoc: true
69
62
  homepage: https://github.com/take-five/traversal
70
63
  licenses: []
data/.gitignore DELETED
@@ -1,6 +0,0 @@
1
- *.gem
2
- .bundle
3
- .idea
4
- Gemfile.lock
5
- pkg/*
6
- coverage/*
data/Gemfile DELETED
@@ -1,9 +0,0 @@
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 DELETED
@@ -1,96 +0,0 @@
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 DELETED
@@ -1 +0,0 @@
1
- require "bundler/gem_tasks"
data/spec/test_helper.rb DELETED
@@ -1,8 +0,0 @@
1
- require "rubygems"
2
- require "bundler"
3
- Bundler.setup
4
-
5
- require 'simplecov'
6
-
7
- SimpleCov.start
8
- require "traversal"
@@ -1,151 +0,0 @@
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 DELETED
@@ -1,24 +0,0 @@
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