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
@@ -0,0 +1,211 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
# When a request is matched against an action tree,
|
4
|
+
# (or rather, its root node), a chain of query objects
|
5
|
+
# is built, and the last one returned.
|
6
|
+
#
|
7
|
+
# Each query object wraps a node, and the query path is the
|
8
|
+
# list of queries leading from the root down to the item matched.
|
9
|
+
#
|
10
|
+
# When no node matches, a query object is still returned,
|
11
|
+
# with different behaviour, and #found? => false
|
12
|
+
|
13
|
+
class ActionTree::Basic::Query
|
14
|
+
|
15
|
+
include ActionTree::Basic::Shared
|
16
|
+
include ActionTree::Errors
|
17
|
+
|
18
|
+
# The {Node} wrapped by this query.
|
19
|
+
# @return [Node]
|
20
|
+
attr_reader :node
|
21
|
+
|
22
|
+
# The parent `Query` in the chain, or `nil` if first.
|
23
|
+
# @return [Query]
|
24
|
+
attr_reader :parent
|
25
|
+
|
26
|
+
# The path fragment matched by this single `Query`.
|
27
|
+
# @return [String]
|
28
|
+
attr_reader :fragment
|
29
|
+
|
30
|
+
# The action namespace specified for this `Query`.
|
31
|
+
# @return [String]
|
32
|
+
attr_reader :namespace
|
33
|
+
|
34
|
+
# ==
|
35
|
+
|
36
|
+
# Queries should be created by calling Node#match
|
37
|
+
#
|
38
|
+
# @api private
|
39
|
+
#
|
40
|
+
def initialize(node, parent, fragment, namespace)
|
41
|
+
@node = node
|
42
|
+
@parent = parent
|
43
|
+
@fragment = fragment
|
44
|
+
@namespace = namespace
|
45
|
+
end
|
46
|
+
|
47
|
+
# Wether the request was successfully matched.
|
48
|
+
# @return [Boolean]
|
49
|
+
def found?
|
50
|
+
@node && @node.actions[@namespace]
|
51
|
+
end
|
52
|
+
|
53
|
+
# Inverse of {Query#found?}
|
54
|
+
# @return [Boolean]
|
55
|
+
def not_found?; !found?; end
|
56
|
+
|
57
|
+
# The chain of `Query` objects wrapping the matched nodes
|
58
|
+
# along the requested path.
|
59
|
+
# @return [Array]
|
60
|
+
def query_path
|
61
|
+
@query_path ||= @parent ? @parent.query_path << self : [self]
|
62
|
+
end
|
63
|
+
|
64
|
+
# Same as {Query#query_path}, but only include {Query} objects that
|
65
|
+
# map to a real node.
|
66
|
+
# @return [Array]
|
67
|
+
def match_path
|
68
|
+
@match_path ||= query_path.select(&:node)
|
69
|
+
end
|
70
|
+
|
71
|
+
# The nodes along the {Query#match_path}
|
72
|
+
# @return [Array]
|
73
|
+
def node_path
|
74
|
+
@node_path ||= match_path.map(&:node)
|
75
|
+
end
|
76
|
+
|
77
|
+
# The matched request path
|
78
|
+
# @return [String]
|
79
|
+
def path
|
80
|
+
query_path.map(&:fragment).join('/')
|
81
|
+
end
|
82
|
+
|
83
|
+
# Do an additional lookup beneath this `Query`.
|
84
|
+
# @return [Query]
|
85
|
+
def match(path)
|
86
|
+
path = parse_path(path)
|
87
|
+
path.empty? ? self : match_one(path.shift).match(path)
|
88
|
+
end
|
89
|
+
alias :query :match
|
90
|
+
|
91
|
+
# Run the matched action along with hooks and processors
|
92
|
+
#
|
93
|
+
# @param [Hash] vars Instance variables to be set in the action
|
94
|
+
# evaluation scope.
|
95
|
+
#
|
96
|
+
# @return [Object] The (processed) result of running the action.
|
97
|
+
def run(vars={})
|
98
|
+
scope = ActionTree::EvalScope.new(
|
99
|
+
vars, default_vars, captures, helper_mixins)
|
100
|
+
begin
|
101
|
+
raise NotFound unless found?
|
102
|
+
run_everything(scope, vars)
|
103
|
+
rescue Exception => err
|
104
|
+
handler = handler_for(err)
|
105
|
+
handler ? scope.instance_exec(err, &handler) : raise
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
|
110
|
+
# The configuration inherited through the matched nodes,
|
111
|
+
# made available in the scope through @_conf.
|
112
|
+
def config
|
113
|
+
@config ||= node_path.inject(dialect::CONFIG) do |conf, node|
|
114
|
+
conf.merge(node.config)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
# A hash of values captured by the query
|
119
|
+
def captures
|
120
|
+
return parent.captures unless @node
|
121
|
+
|
122
|
+
@captures ||= @parent ?
|
123
|
+
@node.read_captures(@fragment, @parent.captures.dup) :
|
124
|
+
ActionTree::CaptureHash.new
|
125
|
+
end
|
126
|
+
|
127
|
+
private
|
128
|
+
|
129
|
+
# Variables for the execution scope
|
130
|
+
def default_vars
|
131
|
+
{
|
132
|
+
:_conf => config,
|
133
|
+
:_query => self
|
134
|
+
}
|
135
|
+
end
|
136
|
+
|
137
|
+
# the helpers mixed into the action scope
|
138
|
+
def helper_mixins
|
139
|
+
[dialect::Helpers] + node_path.map(&:helper_scope)
|
140
|
+
end
|
141
|
+
|
142
|
+
def parse_path(path)
|
143
|
+
case path
|
144
|
+
when Array then path
|
145
|
+
else path.to_s.gsub(/(^\/)|(\/$)/, '').split('/')
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def inherit_via_nodes(&blk)
|
150
|
+
n = node_path.reverse.detect(&blk)
|
151
|
+
blk.call(n) if n
|
152
|
+
end
|
153
|
+
|
154
|
+
### Helpers for matching during queries
|
155
|
+
|
156
|
+
# Return a child {Query} wrapping the {Node} matching `fragment`
|
157
|
+
def match_one(fragment)
|
158
|
+
self.class.new(
|
159
|
+
get_child(fragment),
|
160
|
+
self, fragment, @namespace
|
161
|
+
)
|
162
|
+
end
|
163
|
+
|
164
|
+
# Return the child {Node} matching `fragment`
|
165
|
+
def get_child(fragment)
|
166
|
+
@node.children.find {|n| n.match?(fragment) } if @node
|
167
|
+
end
|
168
|
+
|
169
|
+
### Helpers for running actions
|
170
|
+
|
171
|
+
# Run hooks, action and processors; the whole shebang.
|
172
|
+
def run_everything(scope, vars)
|
173
|
+
apply scope, :before_hooks
|
174
|
+
result = scope.instance_eval(&@node.actions[@namespace])
|
175
|
+
result = process(scope, result)
|
176
|
+
apply scope, :after_hooks
|
177
|
+
result
|
178
|
+
end
|
179
|
+
|
180
|
+
# Run all hooks of `hook_type` in an {EvalScope}
|
181
|
+
def apply(scope, hook_type)
|
182
|
+
node_path.each do |node|
|
183
|
+
node.send(hook_type).each do |prc|
|
184
|
+
scope.instance_eval(&prc)
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
# Postprocess result by running it through the chain
|
190
|
+
# of defined processors.
|
191
|
+
# @see Query#processors
|
192
|
+
def process(scope, result)
|
193
|
+
processors.inject(result) do |result, processor|
|
194
|
+
scope.instance_exec(result, &processor)
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
# The chain of processors, defined using {Node#processor} and
|
199
|
+
# {Node#deep_processor}.
|
200
|
+
def processors
|
201
|
+
@processors = (@node ? @node.processors : []) +
|
202
|
+
node_path.reverse.map(&:deep_processors).flatten
|
203
|
+
end
|
204
|
+
|
205
|
+
# The proc defined to handle `err` using {Node#handle}, or `nil`.
|
206
|
+
def handler_for(err)
|
207
|
+
err = err.class unless err.is_a?(Class)
|
208
|
+
inherit_via_nodes {|n| n.exception_handlers[err] }
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
@@ -0,0 +1,25 @@
|
|
1
|
+
|
2
|
+
# Conatins instance methods shared between
|
3
|
+
# {Basic::Node} and {Basic::Query}. Part of {ActionTree::Basic} and therefore available to all ActionTree dialects.
|
4
|
+
|
5
|
+
module ActionTree::Basic::Shared
|
6
|
+
|
7
|
+
# The dialect that this {Node} or {Query} belongs to.
|
8
|
+
# The dialect is the module namespace containing the
|
9
|
+
# {Node} and {Query} classes.
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
# a = ActionTree::Basic.new
|
13
|
+
# a.is_a?(ActionTree::Basic::Node) #=> true
|
14
|
+
# a.dialect #=> ActionTree::Basic
|
15
|
+
#
|
16
|
+
# @see ActionTree::Basic
|
17
|
+
#
|
18
|
+
# @return [Module] the dialect of the current node or query
|
19
|
+
def dialect
|
20
|
+
self.class.name.split('::')[0..-2].
|
21
|
+
inject(Kernel) do |current, next_const|
|
22
|
+
current.const_get(next_const)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -1,19 +1,44 @@
|
|
1
1
|
|
2
|
+
# Accumulates captured variables during a query match.
|
3
|
+
# Subclasses `Hash`, providing a non-destructive
|
4
|
+
# {CaptureHash#add} method, and merging.
|
5
|
+
#
|
6
|
+
# @see ActionTree::Basic::Query#captures
|
7
|
+
# @api private
|
2
8
|
|
3
9
|
class ActionTree::CaptureHash < Hash
|
10
|
+
|
11
|
+
# {CaptureHash#add} each key and value in the given Hash.
|
12
|
+
#
|
13
|
+
# @return [CaptureHash] self, after merging
|
14
|
+
#
|
4
15
|
def merge!(hsh)
|
5
|
-
hsh.each {|k,
|
16
|
+
hsh.each {|k,v| add(k,v) }
|
17
|
+
self
|
6
18
|
end
|
7
19
|
|
8
|
-
|
9
|
-
|
20
|
+
# Nondestructive version of {CaptureHash#merge!}
|
21
|
+
#
|
22
|
+
# @return [CaptureHash]
|
23
|
+
def merge(*args)
|
24
|
+
dup.merge!(*args)
|
10
25
|
end
|
11
26
|
|
27
|
+
# Add a specified value to a specified key.
|
28
|
+
#
|
29
|
+
# Depending on the value already at the specified key:
|
30
|
+
#
|
31
|
+
# * `Array`: append the new value
|
32
|
+
# * `nil`: replace with new value
|
33
|
+
# * else: wrap existing value followed by new value in an Array
|
34
|
+
#
|
35
|
+
# @return [CaptureHash] self, after adding
|
12
36
|
def add(key, value)
|
13
37
|
case self[key]
|
14
38
|
when nil then self[key] = value
|
15
39
|
when Array then self[key] << value
|
16
40
|
else self[key] = [self[key], value]
|
17
41
|
end
|
42
|
+
self
|
18
43
|
end
|
19
44
|
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
|
2
|
+
require 'rack'
|
3
|
+
|
4
|
+
|
5
|
+
|
6
|
+
|
7
|
+
|
8
|
+
module ActionTree::Components::Rack
|
9
|
+
|
10
|
+
require 'action_tree/components/rack/request.rb'
|
11
|
+
require 'action_tree/components/rack/response.rb'
|
12
|
+
|
13
|
+
CONFIG = {}
|
14
|
+
|
15
|
+
module NodeMethods
|
16
|
+
|
17
|
+
def call(env)
|
18
|
+
request = Request.new(env)
|
19
|
+
response = Response.new
|
20
|
+
|
21
|
+
response.finish do
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
response.write match(
|
26
|
+
Rack::Utils.unescape(request.path_info)
|
27
|
+
).run(request.request_method.to_sym, {
|
28
|
+
:request => request,
|
29
|
+
:response => response,
|
30
|
+
:get => request.GET,
|
31
|
+
:post => request.POST
|
32
|
+
})
|
33
|
+
|
34
|
+
response.finish
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
module QueryMethods
|
39
|
+
end
|
40
|
+
|
41
|
+
module Helpers
|
42
|
+
# FROM SINATRA:
|
43
|
+
# redirect
|
44
|
+
# content_type
|
45
|
+
def get( loc=nil, &blk); action(loc, :get, &blk); end
|
46
|
+
def put( loc=nil, &blk); action(loc, :put, &blk); end
|
47
|
+
def post( loc=nil, &blk); action(loc, :post, &blk); end
|
48
|
+
def delete(loc=nil, &blk); action(loc, :delete, &blk); end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
|
@@ -0,0 +1,59 @@
|
|
1
|
+
|
2
|
+
# Subclass of Rack::Request. Copied from [Sinatra](http://sinatrarb.com).
|
3
|
+
#
|
4
|
+
# @see http://rack.rubyforge.org/doc/classes/Rack/Request.html Rack::Request
|
5
|
+
|
6
|
+
|
7
|
+
class ActionTree::Component::Rack::Request < Rack::Request
|
8
|
+
|
9
|
+
def self.new(env)
|
10
|
+
env['actiontree.request'] ||= super
|
11
|
+
end
|
12
|
+
|
13
|
+
# Returns an array of acceptable media types for the response
|
14
|
+
def accept
|
15
|
+
@env['actiontree.accept'] ||= begin
|
16
|
+
entries = @env['HTTP_ACCEPT'].to_s.split(',')
|
17
|
+
entries.map { |e| accept_entry(e) }.sort_by(&:last).map(&:first)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def preferred_type(*types)
|
22
|
+
return accept.first if types.empty?
|
23
|
+
types.flatten!
|
24
|
+
accept.detect do |pattern|
|
25
|
+
type = types.detect { |t| File.fnmatch(pattern, t) }
|
26
|
+
return type if type
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
alias accept? preferred_type
|
31
|
+
alias secure? ssl?
|
32
|
+
|
33
|
+
def forwarded?
|
34
|
+
@env.include? "HTTP_X_FORWARDED_HOST"
|
35
|
+
end
|
36
|
+
|
37
|
+
def route
|
38
|
+
@route ||= begin
|
39
|
+
path = Rack::Utils.unescape(path_info)
|
40
|
+
path.empty? ? "/" : path
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def path_info=(value)
|
45
|
+
@route = nil
|
46
|
+
super
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def accept_entry(entry)
|
52
|
+
type, *options = entry.gsub(/\s/, '').split(';')
|
53
|
+
quality = 0 # we sort smalles first
|
54
|
+
options.delete_if { |e| quality = 1 - e[2..-1].to_f if e.start_with? 'q=' }
|
55
|
+
[type, [quality, type.count('*'), 1 - options.size]]
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
|
@@ -0,0 +1,10 @@
|
|
1
|
+
|
2
|
+
# The response object. Subclass of `Rack::Response`.
|
3
|
+
#
|
4
|
+
# @see http://rack.rubyforge.org/doc/classes/Rack/Response.html Rack::Response
|
5
|
+
# @see http://rack.rubyforge.org/doc/classes/Rack/Response/Helpers.html Rack::ResponseHelpers
|
6
|
+
#
|
7
|
+
class Response < Rack::Response
|
8
|
+
|
9
|
+
# Nothing here yet.
|
10
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
|
2
|
+
require 'tilt'
|
3
|
+
|
4
|
+
module ActionTree::Components::Tilt
|
5
|
+
|
6
|
+
CONFIG = {:template_path => 'views'}
|
7
|
+
|
8
|
+
module NodeMethods
|
9
|
+
def templates
|
10
|
+
@templates ||= {}
|
11
|
+
end
|
12
|
+
|
13
|
+
def template(*args)
|
14
|
+
tilt = args.find {|a| a.is_a?(Tilt::Template) }
|
15
|
+
name = args.find {|a| a.is_a?(Symbol) }
|
16
|
+
source = args.find {|a| a.is_a?(String) }
|
17
|
+
options = args.find {|a| a.is_a?(Hash) }
|
18
|
+
|
19
|
+
unless tilt
|
20
|
+
path = File.join(@conf[:template_path], source)
|
21
|
+
tilt = Tilt.new(path, nil, options)
|
22
|
+
end
|
23
|
+
templates[name] = tilt
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
module QueryMethods
|
28
|
+
|
29
|
+
# Hash of templates like :name => [tmpl1, tmpl2, ...]
|
30
|
+
def templates
|
31
|
+
@templates ||= node_chain.inject({}) do |hsh, node|
|
32
|
+
hsh[name] ||= []
|
33
|
+
hsh[name] << node.templates[name]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
module Helpers
|
39
|
+
|
40
|
+
# Public: Renders a template
|
41
|
+
def render(*args, &blk)
|
42
|
+
tilt = args.find {|a| a.is_a?(Tilt::Template) }
|
43
|
+
name = args.find {|a| a.is_a?(Symbol) }
|
44
|
+
source = args.find {|a| a.is_a?(String) }
|
45
|
+
options = args.find {|a| a.is_a?(Hash) }
|
46
|
+
|
47
|
+
if tilt && source
|
48
|
+
path = File.join(config[:template_path], source)
|
49
|
+
tilt = Tilt.new(path, nil, options)
|
50
|
+
end
|
51
|
+
|
52
|
+
templates = @_match.templates[name]
|
53
|
+
|
54
|
+
default :type, templates.first.class.default_mime_type
|
55
|
+
|
56
|
+
result = tilt ? tilt.render(self, options, &blk) : nil
|
57
|
+
|
58
|
+
templates.reverse.inject(result) do |r, template|
|
59
|
+
template.render(self) { r }
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
|