action_tree 0.1.1 → 0.2.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/.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
|
+
|