traversal 0.0.1 → 0.0.2

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