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/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
- &nbsp;
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
- &nbsp;
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
- &nbsp;
179
-
180
- Looking up requests
181
- -------------------
182
-
183
- - matching
184
- - was it found?
185
- - on to run
186
- - match chain
187
-
188
-
189
-
190
- &nbsp;
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
- &nbsp;
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,12 +0,0 @@
1
-
2
- module ActionTree::DialectHelper
3
-
4
- # access to the dialect namespace
5
- def dialect
6
- modules = self.class.name.split('::')
7
- modules.pop
8
- modules.inject(Kernel) do |current, next_const|
9
- current.const_get(next_const)
10
- end
11
- end
12
- end
@@ -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
-