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/.document
ADDED
data/.rspec
ADDED
data/.yardopts
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2010-2011 Jostein Berre Eliassen
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/MANUAL.md
ADDED
@@ -0,0 +1,277 @@
|
|
1
|
+
|
2
|
+
ActionTree
|
3
|
+
==========
|
4
|
+
|
5
|
+
**Manual pages**
|
6
|
+
|
7
|
+
<pre>
|
8
|
+
\/ | |/
|
9
|
+
\/ / \||/ /_/___/_
|
10
|
+
\/ |/ \/
|
11
|
+
_\__\_\ | /_____/_
|
12
|
+
\ | / /
|
13
|
+
__ _-----` |{,-----------~
|
14
|
+
\ }{
|
15
|
+
}{{ ACTION TREE:
|
16
|
+
}}{
|
17
|
+
{{} a dry request router/controller
|
18
|
+
, -=-~{ .-^- _
|
19
|
+
ejm `}
|
20
|
+
{
|
21
|
+
</pre>
|
22
|
+
|
23
|
+
This document covers:
|
24
|
+
|
25
|
+
1. Architecture
|
26
|
+
2. Usage
|
27
|
+
1. Defining routes
|
28
|
+
2. Looking up requests
|
29
|
+
3. Running match results
|
30
|
+
3. Dialects
|
31
|
+
4. Advanced usage
|
32
|
+
1. Modularity
|
33
|
+
2. Recursive routes
|
34
|
+
|
35
|
+
|
36
|
+
|
37
|
+
|
38
|
+
Architecture
|
39
|
+
------------
|
40
|
+
|
41
|
+
ActionTree is a [DRY](http://en.wikipedia.org/wiki/Don't_repeat_yourself) request router/controller framweork suitable for web apps. Given a request, such as `/houses/21/parties`, it runs the relevant pieces of code in the right order, with the right variables. It is a bit similar to [Sinatra](http://www.sinatrarb.com/)'s routing, in that it maps URLs to procs, but is much more powerful.
|
42
|
+
|
43
|
+
The routes are kept as a tree of nodes. Request paths are looked up through the tree in the same way that file paths match a file tree. Unlike file trees, one node can match several, and not just one path fragment. In other words, looking up '/house/32/info' first looks for a child node that matches 'house', then from there a child node matching '32', and from there child node matching 'info'.
|
44
|
+
|
45
|
+
Each node has:
|
46
|
+
|
47
|
+
* A match token
|
48
|
+
* N child nodes
|
49
|
+
|
50
|
+
Each node also has
|
51
|
+
|
52
|
+
* One optional (namespaced) action
|
53
|
+
* One optional not_found handler
|
54
|
+
* N ordered before filters
|
55
|
+
* N ordered after filters
|
56
|
+
* N ordered postprocessors
|
57
|
+
|
58
|
+
All of these are procs. Everything except the action is inherited by all descending nodes.
|
59
|
+
|
60
|
+
Finally, each node has
|
61
|
+
|
62
|
+
* A helper scope with n methods, stored as a module.
|
63
|
+
|
64
|
+
These helpers are also inherited by all descending nodes.
|
65
|
+
|
66
|
+
---
|
67
|
+
|
68
|
+
Since all routes are trees, all routes can easily be reused or mounted in several locations. Since inheritance happens per request, the same route mounted several places can work differently in each context.
|
69
|
+
|
70
|
+
This enables DRY and concise controllers and routes.
|
71
|
+
|
72
|
+
Actually, the routes are not even really trees, but graphs, which means you can build infinite, recursive routes, such as `/me/dad/mom/mom/dad/dad/dad/...` -- although that would be a strange thing to do.
|
73
|
+
|
74
|
+
|
75
|
+
### Paths and locations
|
76
|
+
|
77
|
+
I will differentiate between request *paths* and route *locations*.
|
78
|
+
|
79
|
+
**Paths**, like `/houses/21`...
|
80
|
+
|
81
|
+
* Express route lookups.
|
82
|
+
* Are divided into fragments, like `"21"`, `"bobsleigh"` or `"Tirol"`.
|
83
|
+
|
84
|
+
**Locations**, like `houses/:number`...
|
85
|
+
|
86
|
+
* Express route definitions.
|
87
|
+
* Are divided into tokens, like `:number`, `'about'` or `/[a-z]+/`.
|
88
|
+
* Can be
|
89
|
+
* plain: `'about'`
|
90
|
+
* captures:
|
91
|
+
* `:number`,
|
92
|
+
* `':embedded-:symbol'`
|
93
|
+
* `/regex[p]?/`.
|
94
|
+
|
95
|
+
|
96
|
+
|
97
|
+
|
98
|
+
Defining routes
|
99
|
+
---
|
100
|
+
|
101
|
+
### DSL Methods
|
102
|
+
|
103
|
+
Each of the following DSL methods take an optional location argument and a block parameter. The location format is specified later in this document. If omitted, the location will be the same as the current context.
|
104
|
+
|
105
|
+
**route**(location=nil, &blk)
|
106
|
+
: Also known as `with`, `r`, `w`, `_`
|
107
|
+
: Evaluates the code in `blk` in context of the specified `location`.
|
108
|
+
|
109
|
+
**action**(location=nil, layer=nil, &blk)
|
110
|
+
: Also known as `a`, `o`
|
111
|
+
: Attaches an action. Any number of actions can be attached to every node, and they will run in the same order later. `layer` specifies a layer name, to allow storing different actions for different contexts in the same node.
|
112
|
+
|
113
|
+
**get**, **put**, **post** and **delete**
|
114
|
+
: Shortcuts to layering actions.
|
115
|
+
: These methods are shortcuts to the action method above, to simplify layering of http verbs.
|
116
|
+
|
117
|
+
|
118
|
+
**before**(location=nil, &blk)
|
119
|
+
: Also known as `b`
|
120
|
+
: Attaches another before hook to be run for this action and all descendants.
|
121
|
+
|
122
|
+
**after**(location=nil, &blk)
|
123
|
+
: Attaches another after hook to be run for this action and all descendants.
|
124
|
+
|
125
|
+
**helpers**(location=nil, &blk)
|
126
|
+
: Use def inside the block to write helper methods that will be available to all descendants.
|
127
|
+
|
128
|
+
**not_found**(location=nil, &blk)
|
129
|
+
: Also known as `x`
|
130
|
+
: todo
|
131
|
+
|
132
|
+
**mount**(node)
|
133
|
+
: todo
|
134
|
+
|
135
|
+
**mount**(location, node)
|
136
|
+
: todo
|
137
|
+
|
138
|
+
|
139
|
+
### Location format specification
|
140
|
+
|
141
|
+
Locations can be expressed in four ways:
|
142
|
+
|
143
|
+
* A single token, such as `"bobsleigh"`, `:variety` or `/[0-9]+/`
|
144
|
+
* A path string, such as `"/book/chapter/:page/:from-:to"`
|
145
|
+
* An array of tokens, like `["ideas", :id, "search", /[a-z]+/]`
|
146
|
+
* Omitting the path parameter, resulting in a reference to the current scope.
|
147
|
+
|
148
|
+
There are four different types of tokens:
|
149
|
+
|
150
|
+
* `"bobsleigh"` matches the exact path segment "bobsleigh".
|
151
|
+
* `:variety` matches any path segment and keeps it as @variety.
|
152
|
+
* `/[0-9]+/` matches the regexp, in this case numbers.
|
153
|
+
* `":year-:month-:date"` matches "anything-like-this", keeping it as `@year`, `@month` and `@date`
|
154
|
+
|
155
|
+
All these can be used with DSL methods, like this:
|
156
|
+
|
157
|
+
before('bobsleigh') { polish_ice }
|
158
|
+
action(:variety) { "Thank you for choosing #{@variety}" }
|
159
|
+
after(/[0-9]+/) { log_number(@match.to_i) }
|
160
|
+
helper(':id-:name') { Cow.get(@id) }
|
161
|
+
route [/[0-9]+/, /[0-9]+/, /[0-9]+/] do
|
162
|
+
"You visited #{match.join('/')}"
|
163
|
+
end
|
164
|
+
|
165
|
+
|
166
|
+
### Utility methods
|
167
|
+
|
168
|
+
#### descend
|
169
|
+
|
170
|
+
descend(location) #=> #<ActionTree::Node>
|
171
|
+
|
172
|
+
house_routes = routes.descend('houses')
|
173
|
+
|
174
|
+
Returns the node at `location` relative to the current context.
|
175
|
+
|
176
|
+
|
177
|
+
|
178
|
+
|
179
|
+
|
180
|
+
Looking up requests
|
181
|
+
-------------------
|
182
|
+
|
183
|
+
- matching
|
184
|
+
- was it found?
|
185
|
+
- on to run
|
186
|
+
- match chain
|
187
|
+
|
188
|
+
|
189
|
+
|
190
|
+
|
191
|
+
|
192
|
+
Running match results
|
193
|
+
---------------------
|
194
|
+
|
195
|
+
- running
|
196
|
+
- parameters: scope and action layer.
|
197
|
+
- scope is created
|
198
|
+
- precedence: captures overwrite source scope.
|
199
|
+
- variable copy
|
200
|
+
- captures
|
201
|
+
- accumulation
|
202
|
+
- regexps accumulate in @match
|
203
|
+
- esoteric variables, such as @__match
|
204
|
+
- dialect variables, such as @get and @post
|
205
|
+
- actions are run
|
206
|
+
- order
|
207
|
+
each node with before hooks -> run in order of attachment
|
208
|
+
actions of current node run in order of attachment
|
209
|
+
each node with after filters -> run in order of attachment
|
210
|
+
|
211
|
+
|
212
|
+
|
213
|
+
|
214
|
+
|
215
|
+
|
216
|
+
|
217
|
+
|
218
|
+
|
219
|
+
|
220
|
+
Appendix
|
221
|
+
--------
|
222
|
+
|
223
|
+
### Re-using routes
|
224
|
+
|
225
|
+
Let's say we want to make a reusable authentication API:
|
226
|
+
|
227
|
+
simple_auth = ActionTree.new do
|
228
|
+
before { authenticate }
|
229
|
+
action('login') { ... }
|
230
|
+
action('logout') { ... }
|
231
|
+
end
|
232
|
+
|
233
|
+
Now this can be mounted anywhere
|
234
|
+
|
235
|
+
some_app = ActionTree.new do
|
236
|
+
# tada:
|
237
|
+
mount simple_auth
|
238
|
+
|
239
|
+
with('cars') do
|
240
|
+
# or within a scope
|
241
|
+
mount instant_storefront
|
242
|
+
end
|
243
|
+
|
244
|
+
# or with a path:
|
245
|
+
mount 'docs/api', magic_api_doc_generator
|
246
|
+
end
|
247
|
+
|
248
|
+
If you want to pick out a subsection from routes you have already built, you can use the `descend` method:
|
249
|
+
|
250
|
+
car_routes = some_app.descend('cars')
|
251
|
+
|
252
|
+
Now we can mount just the car routes in another ActionTree, or even somewhere else within the same one. We can even make endless circular route constructs...
|
253
|
+
|
254
|
+
family = ActionTree.new do
|
255
|
+
before('father') { @person = @person.father }
|
256
|
+
before('mother') { @person = @person.mother }
|
257
|
+
action('show') { @person.render }
|
258
|
+
end
|
259
|
+
|
260
|
+
family.mount('father', family)
|
261
|
+
family.mount('mother', family)
|
262
|
+
|
263
|
+
me = ActionTree.new do
|
264
|
+
before { @person = ME }
|
265
|
+
mount family
|
266
|
+
end
|
267
|
+
|
268
|
+
me.match('mother/father/mother/father/father/father').run # => great-great-grand-whatnot
|
269
|
+
|
270
|
+
|
271
|
+
|
272
|
+
|
273
|
+
### DSL variants ###
|
274
|
+
|
275
|
+
### Mounting and advanced routes ###
|
276
|
+
|
277
|
+
|
data/README.md
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
[**CLICK HERE TO READ THE MANUAL**](http://rdoc.info/github/jbe/action_tree/master/file/MANUAL.md)
|
2
|
+
*ALPHA VERSION. Documentation may be out of date.*
|
3
|
+
<pre>
|
4
|
+
\/ | |/
|
5
|
+
\/ / \||/ /_/___/_
|
6
|
+
\/ |/ \/
|
7
|
+
_\__\_\ | /_____/_
|
8
|
+
\ | / /
|
9
|
+
__ _-----` |{,-----------~
|
10
|
+
\ }{
|
11
|
+
}{{ ACTION TREE:
|
12
|
+
}}{
|
13
|
+
{{} a dry request router/controller
|
14
|
+
, -=-~{ .-^- _
|
15
|
+
ejm `}
|
16
|
+
{
|
17
|
+
</pre>
|
18
|
+
|
19
|
+
$ gem install action_tree
|
20
|
+
|
21
|
+
Quickstart
|
22
|
+
----------
|
23
|
+
|
24
|
+
|
25
|
+
The Local Office of Alligators, division of information, subdivision of web2.0 and social media, have drawn a (naive) map for their new alligator web API:
|
26
|
+
|
27
|
+
|
28
|
+
welcome
|
29
|
+
|-- alligators
|
30
|
+
| |-- list
|
31
|
+
| |-- create
|
32
|
+
| `-- (with an id)
|
33
|
+
| |-- edit
|
34
|
+
| `-- hug
|
35
|
+
|
36
|
+
A map like this not only shows paths, but also relationships between actions. For instance, `edit` and `hug` retrieve a specific alligator (with an id) before doing anything else. Similarly, every request beneath `/alligators/` should be counted, to see if we're on Digg yet.
|
37
|
+
|
38
|
+
So let's put those two in `before` hooks in an ActionTree:
|
39
|
+
|
40
|
+
routes = ActionTree.new do
|
41
|
+
action { "Welcome to the alligator manager #{@name}!" }
|
42
|
+
|
43
|
+
with 'alligators' do
|
44
|
+
before { @count = (@count || 0) + 1 }
|
45
|
+
|
46
|
+
action { Alligator.all }
|
47
|
+
action('create') { Alligator.create }
|
48
|
+
|
49
|
+
with :id do
|
50
|
+
before { @alligator = Alligator.get(@id) }
|
51
|
+
action { 'this is ' + @alligator }
|
52
|
+
action('edit') { "editing #{@alligator}" }
|
53
|
+
action('hug') { "#{@alligator} is pleased." }
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
We can then match and run actions:
|
59
|
+
|
60
|
+
routes.match('/') #=> #<ActionTree::Match >
|
61
|
+
routes.match('/').found? #=> true
|
62
|
+
|
63
|
+
@name = 'Zap'
|
64
|
+
routes.match('/').run(self) # => "Welcome to the alligator manager, Zap."
|
65
|
+
|
66
|
+
routes.match('/alligators/4').run # => "this is #<Alligator 4>"
|
67
|
+
router.match('/alligators/8/hug').run # => "<#Alligator 8> is pleased."
|
68
|
+
|
69
|
+
routes.match('nonexistant/route') # => #<ActionTree::NotFound>
|
70
|
+
routes.match('nonexistant/route').found? #=> false
|
71
|
+
|
72
|
+
|
73
|
+
|
74
|
+
|
75
|
+
|
76
|
+
Available dialects
|
77
|
+
------------------
|
78
|
+
|
79
|
+
[**at-rack**](https://github.com/jbe/at-rack) turns ActionTree into a Sinatra-like framework, and allows you to mount trees as rack apps.
|
80
|
+
|
81
|
+
**at-websocket** is planned to make writing websocket servers easier.
|
82
|
+
|
83
|
+
|
84
|
+
|
85
|
+
---
|
86
|
+
|
87
|
+
Copyright (c) 2010-2011 Jostein Berre Eliassen. See [LICENSE](http://rdoc.info/github/jbe/action_tree/master/file/LICENSE) for details.
|
88
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "action_tree"
|
8
|
+
gem.summary = %Q{Versatile routing dry enough to kill cacti.}
|
9
|
+
gem.description = %Q{ActionTree is a router. It provides a compact DSL for defining routes that map paths to actions. It was designed as a router for modern ruby mini-frameworks.}
|
10
|
+
gem.email = "post@jostein.be"
|
11
|
+
gem.homepage = "http://github.com/jbe/action_tree"
|
12
|
+
gem.authors = ["jbe"]
|
13
|
+
|
14
|
+
#gem.required_ruby_version = '>= 1.8.7'
|
15
|
+
gem.add_dependency 'backports'
|
16
|
+
|
17
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
18
|
+
end
|
19
|
+
Jeweler::GemcutterTasks.new
|
20
|
+
rescue LoadError
|
21
|
+
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
|
22
|
+
end
|
23
|
+
|
24
|
+
require 'rake/testtask'
|
25
|
+
Rake::TestTask.new(:test) do |test|
|
26
|
+
test.libs << 'lib' << 'test'
|
27
|
+
test.pattern = 'test/**/test_*.rb'
|
28
|
+
test.verbose = true
|
29
|
+
end
|
30
|
+
|
31
|
+
require 'rspec/core/rake_task'
|
32
|
+
RSpec::Core::RakeTask.new(:spec)
|
33
|
+
|
34
|
+
|
35
|
+
task :spec => :check_dependencies
|
36
|
+
task :default => :spec
|
37
|
+
|
38
|
+
|
39
|
+
|
40
|
+
#namespace :test do
|
41
|
+
# desc 'open testing shell'
|
42
|
+
# task :shell => [:build, :install] do
|
43
|
+
# sh 'irb -r ./test/shell.rb'
|
44
|
+
# end
|
45
|
+
#end
|