action_tree 0.1.0
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/.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
|