action_tree 0.1.1 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.yardopts +3 -5
- data/README.md +6 -77
- data/Rakefile +16 -3
- data/TODO +84 -26
- data/VERSION +1 -1
- data/lib/action_tree.rb +46 -27
- data/lib/action_tree/basic.rb +36 -0
- data/lib/action_tree/basic/helpers.rb +20 -0
- data/lib/action_tree/basic/node.rb +294 -103
- data/lib/action_tree/basic/query.rb +211 -0
- data/lib/action_tree/basic/shared.rb +25 -0
- data/lib/action_tree/capture_hash.rb +28 -3
- data/lib/action_tree/components/rack.rb +52 -0
- data/lib/action_tree/components/rack/request.rb +59 -0
- data/lib/action_tree/components/rack/response.rb +10 -0
- data/lib/action_tree/components/tilt.rb +65 -0
- data/lib/action_tree/errors.rb +12 -0
- data/lib/action_tree/eval_scope.rb +18 -16
- data/spec/00_foundations/capture_hash_spec.rb +47 -0
- data/spec/00_foundations/eval_scope_spec.rb +80 -0
- data/spec/{02_node_spec.rb → 01_node/02_node_spec.rb} +8 -13
- data/spec/{03_match_spec.rb → 02_query/query_spec.rb} +5 -4
- data/spec/{04_integration_spec.rb → 03_action_tree_basic/04_integration_spec.rb} +10 -8
- data/spec/{p01_tilt_spec.rb → 04_components/template_spec.rb} +5 -9
- metadata +66 -20
- data/MANUAL.md +0 -277
- data/lib/action_tree/basic/match.rb +0 -132
- data/lib/action_tree/dialect_helper.rb +0 -12
- data/lib/action_tree/plugins/tilt.rb +0 -65
- data/spec/01_support_lib_spec.rb +0 -41
data/MANUAL.md
DELETED
@@ -1,277 +0,0 @@
|
|
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
|
-
|
@@ -1,132 +0,0 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
class ActionTree::Basic::Match
|
6
|
-
|
7
|
-
include ::ActionTree::DialectHelper
|
8
|
-
|
9
|
-
DEFAULT_HELPERS = []
|
10
|
-
|
11
|
-
attr_reader :node, :selected_scope, :parent, :fragment
|
12
|
-
|
13
|
-
def initialize(node, parent, fragment)
|
14
|
-
@node, @parent, @fragment = node, parent, fragment
|
15
|
-
end
|
16
|
-
|
17
|
-
def found?; @node; end
|
18
|
-
|
19
|
-
def captures
|
20
|
-
@captures ||= read_captures
|
21
|
-
end
|
22
|
-
|
23
|
-
def match_chain
|
24
|
-
@match_chain ||= @parent ? @parent.match_chain << self : [self]
|
25
|
-
end
|
26
|
-
|
27
|
-
def valid_match_chain
|
28
|
-
@valid_match_chain ||= match_chain.select(&:node)
|
29
|
-
end
|
30
|
-
|
31
|
-
def node_chain
|
32
|
-
@node_chain ||= valid_match_chain.map(&:node)
|
33
|
-
end
|
34
|
-
|
35
|
-
def path
|
36
|
-
match_chain.map(&:fragment).join('/')
|
37
|
-
end
|
38
|
-
|
39
|
-
# Run the action; first the before filters within the match chain, then the actions, then the postprocessors, then the after filters, returning the return value of the last evaluated action.
|
40
|
-
def run(namespace=:default, *scope_sources)
|
41
|
-
|
42
|
-
eval_scope = ::ActionTree::EvalScope.new(
|
43
|
-
*helper_mixins, *scope_sources, captures,
|
44
|
-
*DEFAULT_HELPERS,
|
45
|
-
{:__match => self}
|
46
|
-
)
|
47
|
-
|
48
|
-
hooks(:before_hooks).each {|prc| eval_scope.instance_eval(&prc) }
|
49
|
-
result = eval_scope.instance_eval(&main_action(namespace))
|
50
|
-
hooks(:after_hooks).each {|prc| eval_scope.instance_eval(&prc) }
|
51
|
-
|
52
|
-
postprocessors.inject(result) do |result, postprocessor|
|
53
|
-
eval_scope.instance_exec(result, &postprocessor)
|
54
|
-
end
|
55
|
-
end
|
56
|
-
|
57
|
-
|
58
|
-
# Return @self@ or a descendant matching @path@, or a {NotFound} if not found.
|
59
|
-
def match(path)
|
60
|
-
path = parse_path(path)
|
61
|
-
return self if path.empty?
|
62
|
-
return self unless found?
|
63
|
-
|
64
|
-
fragment = path.shift
|
65
|
-
@node.children.each do |child|
|
66
|
-
if child.match?(fragment)
|
67
|
-
return self.class.new(
|
68
|
-
child, self, fragment).match(path)
|
69
|
-
end
|
70
|
-
end
|
71
|
-
|
72
|
-
self.class.new(nil, self, fragment) # not found
|
73
|
-
end
|
74
|
-
|
75
|
-
private
|
76
|
-
def parse_path(path)
|
77
|
-
case path
|
78
|
-
when Array then path
|
79
|
-
else path.to_s.gsub(/(^\/)|(\/$)/, '').split('/')
|
80
|
-
end
|
81
|
-
end
|
82
|
-
|
83
|
-
def not_found_handler
|
84
|
-
node_chain.reverse.each do |node|
|
85
|
-
return node.not_found_handler if node.not_found_handler
|
86
|
-
end; nil
|
87
|
-
end
|
88
|
-
|
89
|
-
def read_captures
|
90
|
-
hsh = ::ActionTree::CaptureHash.new
|
91
|
-
|
92
|
-
match_chain[1..-1].each do |m|
|
93
|
-
if m.found? && m.node.capture_names.any?
|
94
|
-
raw_captures = m.fragment.match(m.node.regexp).captures
|
95
|
-
m.node.capture_names.each_with_index do |name, i|
|
96
|
-
hsh.add(name, raw_captures[i])
|
97
|
-
end
|
98
|
-
end
|
99
|
-
end
|
100
|
-
hsh
|
101
|
-
end
|
102
|
-
|
103
|
-
def main_action(namespace)
|
104
|
-
(found? ?
|
105
|
-
@node.action_namespaces[namespace] :
|
106
|
-
not_found_handler
|
107
|
-
) || Proc.new { nil }
|
108
|
-
end
|
109
|
-
|
110
|
-
def helper_mixins
|
111
|
-
node_chain.map(&:helper_scope)
|
112
|
-
end
|
113
|
-
|
114
|
-
def hooks(type)
|
115
|
-
node_chain.map {|n| n.send(type) }.flatten
|
116
|
-
end
|
117
|
-
|
118
|
-
def postprocessors
|
119
|
-
@node ? @node.postprocessors : []
|
120
|
-
end
|
121
|
-
|
122
|
-
def inherit_via_node_chain(&blk)
|
123
|
-
n = node_chain.reverse.detect(&blk)
|
124
|
-
n ? blk.call(n) : nil
|
125
|
-
end
|
126
|
-
|
127
|
-
end
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
@@ -1,65 +0,0 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
require 'tilt'
|
5
|
-
|
6
|
-
module ActionTree::Plugins::Tilt
|
7
|
-
|
8
|
-
CACHE = Tilt::Cache.new
|
9
|
-
|
10
|
-
module NodeMixin
|
11
|
-
# sets a default, optionally namespaced, layout for this and
|
12
|
-
# descending nodes
|
13
|
-
def layout(path, namespace=nil)
|
14
|
-
layout_namespaces[namespace] = path
|
15
|
-
end
|
16
|
-
|
17
|
-
def layout_namespaces
|
18
|
-
@layout_namespaces ||= {}
|
19
|
-
end
|
20
|
-
|
21
|
-
# sets a default view folder path for this and descending nodes.
|
22
|
-
def view_path(path=nil)
|
23
|
-
path ? @view_path = path : @view_path
|
24
|
-
end
|
25
|
-
end
|
26
|
-
|
27
|
-
module MatchMixin
|
28
|
-
def view_path
|
29
|
-
inherit_via_node_chain(&:view_path) || '.'
|
30
|
-
end
|
31
|
-
|
32
|
-
def layout(namespace=nil)
|
33
|
-
inherit_via_node_chain do |node|
|
34
|
-
node.layout_namespaces[namespace]
|
35
|
-
end
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
|
-
|
40
|
-
module Helpers
|
41
|
-
|
42
|
-
def render(path, layout_namespace=nil, &blk)
|
43
|
-
path = ::File.join(@__match.view_path, path)
|
44
|
-
|
45
|
-
template = ::ActionTree::Plugins::Tilt::CACHE.fetch(
|
46
|
-
path) { Tilt.new(path, nil, {}) }
|
47
|
-
|
48
|
-
if respond_to?(:content_type)
|
49
|
-
content_type(template.class.default_mime_type)
|
50
|
-
end
|
51
|
-
|
52
|
-
result = template.render(self, &blk)
|
53
|
-
|
54
|
-
layout = @__match.layout(layout_namespace)
|
55
|
-
if layout && !%w{sass scss less coffee}.include?(File.extname(path))
|
56
|
-
render(layout, :AbSoLuTeLy_No_LaYoUt) { result }
|
57
|
-
else
|
58
|
-
result
|
59
|
-
end
|
60
|
-
end
|
61
|
-
|
62
|
-
end
|
63
|
-
|
64
|
-
end
|
65
|
-
|