action_tree 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +6 -0
- data/.rspec +3 -0
- data/.yardopts +14 -0
- data/LICENSE +20 -0
- data/MANUAL.md +277 -0
- data/README.md +88 -0
- data/Rakefile +45 -0
- data/TODO +32 -0
- data/VERSION +1 -0
- data/lib/action_tree/basic/match.rb +132 -0
- data/lib/action_tree/basic/node.rb +180 -0
- data/lib/action_tree/capture_hash.rb +19 -0
- data/lib/action_tree/dialect_helper.rb +12 -0
- data/lib/action_tree/eval_scope.rb +23 -0
- data/lib/action_tree/plugins/tilt.rb +65 -0
- data/lib/action_tree.rb +47 -0
- data/spec/01_support_lib_spec.rb +41 -0
- data/spec/02_node_spec.rb +149 -0
- data/spec/03_match_spec.rb +120 -0
- data/spec/04_integration_spec.rb +140 -0
- data/spec/fixtures/test.haml +2 -0
- data/spec/fixtures/test_layout.haml +1 -0
- data/spec/log +12 -0
- data/spec/p01_tilt_spec.rb +60 -0
- metadata +105 -0
data/lib/action_tree.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
|
2
|
+
require 'set'
|
3
|
+
require 'backports'
|
4
|
+
|
5
|
+
module ActionTree
|
6
|
+
|
7
|
+
require 'action_tree/eval_scope'
|
8
|
+
require 'action_tree/capture_hash'
|
9
|
+
require 'action_tree/dialect_helper'
|
10
|
+
|
11
|
+
module Plugins
|
12
|
+
require 'action_tree/plugins/tilt'
|
13
|
+
end
|
14
|
+
|
15
|
+
|
16
|
+
module Dialect
|
17
|
+
def new(*prms, &blk)
|
18
|
+
self::Node.new(*prms, &blk)
|
19
|
+
end
|
20
|
+
def apply(mod)
|
21
|
+
self::Node.send(:include, mod::NodeMixin)
|
22
|
+
self::Match.send(:include, mod::MatchMixin)
|
23
|
+
self::Match::DEFAULT_HELPERS << mod::Helpers
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# load basic dialect
|
28
|
+
module Basic
|
29
|
+
extend Dialect
|
30
|
+
require "action_tree/basic/node"
|
31
|
+
require "action_tree/basic/match"
|
32
|
+
end
|
33
|
+
|
34
|
+
# shorthand
|
35
|
+
def self.new(&blk)
|
36
|
+
Basic.new(&blk)
|
37
|
+
end
|
38
|
+
|
39
|
+
# layer proc wrapper
|
40
|
+
class Layer < Proc; end
|
41
|
+
def layer(*prms, &blk)
|
42
|
+
Layer.new(*prms, &blk)
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
|
@@ -0,0 +1,41 @@
|
|
1
|
+
|
2
|
+
require 'action_tree'
|
3
|
+
|
4
|
+
|
5
|
+
|
6
|
+
describe ActionTree::EvalScope do
|
7
|
+
it 'has access to local variables' do
|
8
|
+
@say = 'tra la la a ring a ding ding'
|
9
|
+
ActionTree::EvalScope.new(self).instance_eval do
|
10
|
+
@say
|
11
|
+
end.should == 'tra la la a ring a ding ding'
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'extends modules' do
|
15
|
+
module Hooga
|
16
|
+
def words_of_wisdom
|
17
|
+
'touch my tra la la my ding ding dong'
|
18
|
+
end
|
19
|
+
end
|
20
|
+
ActionTree::EvalScope.new(Hooga).words_of_wisdom.should ==
|
21
|
+
'touch my tra la la my ding ding dong'
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'overwrites sooner with later extensions' do
|
25
|
+
prepare = Proc.new { @greeting = 'Hello' }
|
26
|
+
greet = Proc.new { "#{@greeting} #{get_name}" }
|
27
|
+
module Booga
|
28
|
+
def get_name
|
29
|
+
'Melchior'
|
30
|
+
end
|
31
|
+
end
|
32
|
+
module Shooga
|
33
|
+
def get_name
|
34
|
+
'Baltazar'
|
35
|
+
end
|
36
|
+
end
|
37
|
+
scope = ActionTree::EvalScope.new(Booga, Shooga, self)
|
38
|
+
scope.instance_eval(&prepare)
|
39
|
+
scope.instance_eval(&greet).should == 'Hello Baltazar'
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,149 @@
|
|
1
|
+
|
2
|
+
require 'action_tree'
|
3
|
+
|
4
|
+
describe ActionTree::Basic::Node do
|
5
|
+
|
6
|
+
|
7
|
+
# PATH PARSING AND DESCENDING
|
8
|
+
|
9
|
+
describe 'descend' do
|
10
|
+
it 'returns self when descending to nil, "", "/" or []' do
|
11
|
+
[nil, '', '/', []].each do |location|
|
12
|
+
subject.descend(location).should == subject
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'returns a new node when descending one level' do
|
17
|
+
n = subject.descend('/the')
|
18
|
+
n.should be_a(ActionTree::Basic::Node)
|
19
|
+
n.should_not == subject
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'returns a new node when descending two levels' do
|
23
|
+
n = subject.descend('/the/world')
|
24
|
+
n.should be_a(ActionTree::Basic::Node)
|
25
|
+
n.should_not == subject
|
26
|
+
n.should_not == subject.descend('/the')
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'gives same node when descending two times, one level' do
|
30
|
+
subject.descend('/the').should == subject.descend('/the')
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'gives same node when descending two times, three levels' do
|
34
|
+
subject.descend('/the/world/is').should == subject.descend('/the/world/is')
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'gives birth recursively and selectively when descending' do
|
38
|
+
n = subject.descend('/the/world/is/good')
|
39
|
+
n.token.should == 'good'
|
40
|
+
n.should == subject.descend('the').descend('world/is').descend('good')
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
|
45
|
+
|
46
|
+
# TOKEN DIFFERENTIATION
|
47
|
+
|
48
|
+
describe 'with string token' do
|
49
|
+
subject { ActionTree::Basic::Node.new('noodles') }
|
50
|
+
|
51
|
+
it 'matches the same token' do
|
52
|
+
subject.match?('noodles').should be_true
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'does not match another token' do
|
56
|
+
subject.match?('naddles').should be_false
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'names no captures' do
|
60
|
+
subject.capture_names.should == []
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
|
65
|
+
describe 'with string token with four/five captures' do
|
66
|
+
subject do
|
67
|
+
ActionTree::Basic::Node.new(':year-:month-:day-:word-and-:word')
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'matches a valid match' do
|
71
|
+
subject.match?('2007-08-20-widgets-and-gadgets').should be_true
|
72
|
+
end
|
73
|
+
|
74
|
+
it 'does not match invalid match' do
|
75
|
+
subject.match?('2007-08-20-widgets-gadgets').should be_false
|
76
|
+
end
|
77
|
+
|
78
|
+
it 'names the four captures' do
|
79
|
+
subject.capture_names.should == %w{year month day word word}
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
|
84
|
+
describe 'with regexp capture' do
|
85
|
+
subject { ActionTree::Basic::Node.new(/[0-9]+\.[0-9]+/) }
|
86
|
+
|
87
|
+
it 'matches the same token' do
|
88
|
+
subject.match?('65.2').should be_true
|
89
|
+
end
|
90
|
+
|
91
|
+
it 'does not match another token' do
|
92
|
+
subject.match?('beef').should be_false
|
93
|
+
subject.match?('abba.beatles').should be_false
|
94
|
+
end
|
95
|
+
|
96
|
+
it 'captures to "match"' do
|
97
|
+
subject.capture_names.should == ['match']
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
101
|
+
|
102
|
+
describe 'with grouped regexp capture' do
|
103
|
+
subject { ActionTree::Basic::Node.new(/([0-9]+)\.([0-9]+)/) }
|
104
|
+
|
105
|
+
it 'matches the same token' do
|
106
|
+
subject.match?('65.2').should be_true
|
107
|
+
end
|
108
|
+
|
109
|
+
it 'does not match another token' do
|
110
|
+
subject.match?('beef').should be_false
|
111
|
+
subject.match?('abba.beatles').should be_false
|
112
|
+
end
|
113
|
+
|
114
|
+
it 'captures to "match"' do
|
115
|
+
subject.capture_names.should == ['match']
|
116
|
+
end
|
117
|
+
|
118
|
+
end
|
119
|
+
|
120
|
+
|
121
|
+
describe 'with symbol capture' do
|
122
|
+
subject { ActionTree::Basic::Node.new(:hometown) }
|
123
|
+
|
124
|
+
it 'matches any word' do
|
125
|
+
%w{Osaka Oslo Baden-Baden}.each do |town|
|
126
|
+
subject.match?(town).should be_true
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
it 'names the capture' do
|
131
|
+
subject.capture_names.should == ['hometown']
|
132
|
+
end
|
133
|
+
|
134
|
+
end
|
135
|
+
|
136
|
+
describe 'dsl' do
|
137
|
+
it 'mounts' do
|
138
|
+
pending
|
139
|
+
end
|
140
|
+
|
141
|
+
it 'defines helpers' do
|
142
|
+
subject.helpers { def guru; 'Ramana'; end }
|
143
|
+
o = Object.new
|
144
|
+
o.extend(subject.helper_scope)
|
145
|
+
o.guru.should == 'Ramana'
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
require 'action_tree'
|
4
|
+
|
5
|
+
|
6
|
+
|
7
|
+
|
8
|
+
describe ActionTree::Basic::Match do
|
9
|
+
|
10
|
+
describe 'captures' do
|
11
|
+
|
12
|
+
subject do
|
13
|
+
ActionTree.new do
|
14
|
+
r :genus do
|
15
|
+
a { @genus }
|
16
|
+
r ':species-:subspecies' do
|
17
|
+
a { "#{@genus} #{@species} #{@subspecies}" }
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'no values when no match' do
|
24
|
+
subject.match('/one/two/three/bobsleigh').run.should be_nil
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'one value' do
|
28
|
+
subject.match('/Homo').run.should == 'Homo'
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'values and GET variables' do
|
32
|
+
subject.match('/Homo/Sapiens-Sapiens').run.should ==
|
33
|
+
'Homo Sapiens Sapiens'
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
|
39
|
+
|
40
|
+
|
41
|
+
|
42
|
+
|
43
|
+
|
44
|
+
describe 'universe integration test' do
|
45
|
+
|
46
|
+
subject do
|
47
|
+
ActionTree::Basic::Node.new do
|
48
|
+
before { @altitude = :astronomical }
|
49
|
+
after { @boring = false }
|
50
|
+
action { "no sound at #{@altitude} altitudes" }
|
51
|
+
helpers { def infinite?; true; end }
|
52
|
+
not_found { 'nix nada zip' }
|
53
|
+
route :world do
|
54
|
+
before { @status = :threatened }
|
55
|
+
helpers { def infinite?; false; end }
|
56
|
+
route :continent do
|
57
|
+
before { @order = 'political' }
|
58
|
+
route :country do
|
59
|
+
before { @context = 'national' }
|
60
|
+
after { @context = 'life' }
|
61
|
+
helpers { def violent?; false; end }
|
62
|
+
action('show') { 'not much to see' }
|
63
|
+
route :municipality do
|
64
|
+
before { @boring = true }
|
65
|
+
helpers { def boring?; true; end }
|
66
|
+
action { [@world, @continent, @country, @municipality] if boring? }
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
it 'runs root action witin same scope as root before filter' do
|
75
|
+
subject.match('/').run.should == 'no sound at astronomical altitudes'
|
76
|
+
end
|
77
|
+
|
78
|
+
it 'allows overwriting root actions' do
|
79
|
+
subject.action { 'Zarathustra' }
|
80
|
+
subject.match('').run.should == 'Zarathustra'
|
81
|
+
end
|
82
|
+
|
83
|
+
it 'root has access to root helper' do
|
84
|
+
subject.action { "Infinite!" if infinite? }
|
85
|
+
subject.match('/').run.should == 'Infinite!'
|
86
|
+
end
|
87
|
+
|
88
|
+
it 'root invokes root not found' do
|
89
|
+
subject.match('/earth/europe/norway/akershus/super-cheese').run.should == 'nix nada zip'
|
90
|
+
end
|
91
|
+
|
92
|
+
it 'sets instance variables from captures' do
|
93
|
+
subject.match('/earth/europe/norway/akershus').run.should ==
|
94
|
+
%w{earth europe norway akershus}
|
95
|
+
end
|
96
|
+
|
97
|
+
# self
|
98
|
+
|
99
|
+
|
100
|
+
|
101
|
+
|
102
|
+
|
103
|
+
# inherited
|
104
|
+
|
105
|
+
it 'runs ancestors before filters' do
|
106
|
+
end
|
107
|
+
|
108
|
+
it 'runs ancestors after filters' do
|
109
|
+
end
|
110
|
+
|
111
|
+
it 'has access to ancestors helpers' do
|
112
|
+
end
|
113
|
+
|
114
|
+
it 'invokes ancestors not_found handler' do
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
end
|
119
|
+
|
120
|
+
|
@@ -0,0 +1,140 @@
|
|
1
|
+
|
2
|
+
require 'action_tree'
|
3
|
+
require 'fileutils'
|
4
|
+
|
5
|
+
|
6
|
+
|
7
|
+
describe ActionTree do
|
8
|
+
|
9
|
+
describe 'integration' do
|
10
|
+
|
11
|
+
subject do
|
12
|
+
ActionTree.new do
|
13
|
+
helpers { def h1; 'bob'; end }
|
14
|
+
helpers { def h1; '1'; end }
|
15
|
+
helpers { def h2; h1; end }
|
16
|
+
helpers { def h4; '6'; end }
|
17
|
+
before { @v = h2 + '2' }
|
18
|
+
before { @v = @v + '3' }
|
19
|
+
before { @v2 = 'sleigh' }
|
20
|
+
a { @v }
|
21
|
+
helpers('/a') {def h3; @v2; end}
|
22
|
+
before('/a') { @v2 = '4' }
|
23
|
+
before('/a') { @v3 = @v + h3 }
|
24
|
+
a('/a') { @v3 }
|
25
|
+
before('/a/b') { @v4 = @v3 + '5' }
|
26
|
+
before('/a/b') { @v5 = @v4 }
|
27
|
+
a('/a/b') { @v5 }
|
28
|
+
not_found('a/b') { @v5 + h4 }
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'works, if only just a bit' do
|
33
|
+
subject.match('/').run.should == '123'
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'works' do
|
37
|
+
subject.match('a/b').run.should == '12345'
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'works with not_found' do
|
41
|
+
subject.match('a/b/y').run.should ==
|
42
|
+
'123456'
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
|
47
|
+
|
48
|
+
|
49
|
+
describe 'integration scenario' do
|
50
|
+
|
51
|
+
subject do
|
52
|
+
|
53
|
+
ActionTree.new do
|
54
|
+
before { @msg = 'root hook ok' }
|
55
|
+
with('houses') do
|
56
|
+
action { 'house index' }
|
57
|
+
action('create') {'newly built house'}
|
58
|
+
with(:id) do
|
59
|
+
action { "showing house #{@id}" }
|
60
|
+
action('edit') { "editing house #{@id}" }
|
61
|
+
action('destroy') { "destroying house #{@id}" }
|
62
|
+
action(/[0-9]+/) {'number match'}
|
63
|
+
end
|
64
|
+
end
|
65
|
+
before('houses/:id') { @msg = 'house hook ok' }
|
66
|
+
action('long/action/weird/url') { 'long' }
|
67
|
+
action(/[0-9]+/) { 'root number match' }
|
68
|
+
end
|
69
|
+
|
70
|
+
|
71
|
+
end
|
72
|
+
|
73
|
+
|
74
|
+
it 'matches "" => self' do
|
75
|
+
subject.match('').node.should == subject
|
76
|
+
end
|
77
|
+
|
78
|
+
it 'matches "/" => self' do
|
79
|
+
subject.match('/').node.should == subject
|
80
|
+
end
|
81
|
+
|
82
|
+
it 'returns nil for nonexistant level one node' do
|
83
|
+
subject.match('/nothing').found?.should be_false
|
84
|
+
end
|
85
|
+
|
86
|
+
it 'returns nil for nonexistant level two node' do
|
87
|
+
subject.match('/long/nothing').found?.should be_false
|
88
|
+
end
|
89
|
+
|
90
|
+
it 'matches a first level request' do
|
91
|
+
subject.match('/houses').node.should == subject.descend('houses')
|
92
|
+
end
|
93
|
+
|
94
|
+
it 'matches a second level request' do
|
95
|
+
subject.match('/houses/create').node.should == subject.descend('houses/create')
|
96
|
+
end
|
97
|
+
|
98
|
+
it 'matches a second level capture' do
|
99
|
+
subject.match('/houses/4').node.should == subject.descend('houses/:id')
|
100
|
+
end
|
101
|
+
|
102
|
+
it 'matches beneath second level capture' do
|
103
|
+
subject.match('/houses/4/edit').node.should == subject.descend('houses/:id/edit')
|
104
|
+
end
|
105
|
+
|
106
|
+
it 'returns nil for nonexistant node beneath capture' do
|
107
|
+
subject.match('/houses/4/nothing_here').found?.should be_false
|
108
|
+
end
|
109
|
+
|
110
|
+
it 'gets the path right' do
|
111
|
+
subject.match('/long/action/weird/url').path.should == '/long/action/weird/url'
|
112
|
+
end
|
113
|
+
|
114
|
+
it 'gets the path right with captures' do
|
115
|
+
subject.match('/houses/53/edit').path.should == '/houses/53/edit'
|
116
|
+
end
|
117
|
+
|
118
|
+
it 'ignores trailing slashes' do
|
119
|
+
subject.match('/long/action').node.should ==
|
120
|
+
subject.match('long/action/').node
|
121
|
+
end
|
122
|
+
|
123
|
+
it 'matches regexp in root scope' do
|
124
|
+
subject.match('/346').node.should == subject.descend([/[0-9]+/])
|
125
|
+
end
|
126
|
+
|
127
|
+
it 'fills in path for regexp in root scope' do
|
128
|
+
subject.match('/346').path.should == '/346'
|
129
|
+
end
|
130
|
+
|
131
|
+
it 'matches regexp in capture scope' do
|
132
|
+
subject.match('/houses/5/4').node.should == subject.descend(['houses', :id, /[0-9]+/])
|
133
|
+
end
|
134
|
+
|
135
|
+
it 'fills in path for regexp in capture scope' do
|
136
|
+
subject.match('/houses/5/4').path.should == '/houses/5/4'
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
end
|