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/TODO
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
|
4
|
+
|
5
|
+
dialect mixins
|
6
|
+
templating
|
7
|
+
|
8
|
+
rewrite tests
|
9
|
+
tests for:
|
10
|
+
variable scope copy
|
11
|
+
after hooks
|
12
|
+
action namespaces
|
13
|
+
running different action layers
|
14
|
+
get, put, post, delete
|
15
|
+
postprocessors
|
16
|
+
mounting
|
17
|
+
layers & application
|
18
|
+
|
19
|
+
|
20
|
+
documentation for:
|
21
|
+
postprocessors
|
22
|
+
action namespaces
|
23
|
+
new mounting
|
24
|
+
layer application
|
25
|
+
ActionTree.layer
|
26
|
+
Node.apply(layer)
|
27
|
+
mounting versus layer application
|
28
|
+
get, put, post, and update helpers
|
29
|
+
DSL shorthands/aliases
|
30
|
+
applying plugins
|
31
|
+
|
32
|
+
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
@@ -0,0 +1,132 @@
|
|
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
|
+
|
@@ -0,0 +1,180 @@
|
|
1
|
+
|
2
|
+
class ActionTree::Basic::Node
|
3
|
+
|
4
|
+
include ::ActionTree::DialectHelper
|
5
|
+
|
6
|
+
attr_reader :token, :children, :helper_scope,
|
7
|
+
:action_namespaces, :before_hooks,
|
8
|
+
:after_hooks, :postprocessors
|
9
|
+
attr_accessor :not_found_handler
|
10
|
+
|
11
|
+
|
12
|
+
def initialize(path=[], &blk)
|
13
|
+
path = parse_path(path)
|
14
|
+
@token = path.shift
|
15
|
+
@children = Set.new
|
16
|
+
@helper_scope = Module.new
|
17
|
+
@before_hooks = []
|
18
|
+
@after_hooks = []
|
19
|
+
@postprocessors = []
|
20
|
+
@action_namespaces = {}
|
21
|
+
@not_found_handler = nil
|
22
|
+
route(path, &blk) if block_given?
|
23
|
+
end
|
24
|
+
|
25
|
+
# INSPECTION
|
26
|
+
|
27
|
+
def inspect
|
28
|
+
"#<#{self.class}:#{self.object_id.to_s(16)} #{display_name} >"
|
29
|
+
end
|
30
|
+
|
31
|
+
def display_name
|
32
|
+
case @token
|
33
|
+
when String then @token
|
34
|
+
when Symbol then @token.inspect
|
35
|
+
when Regexp then @token.source
|
36
|
+
when nil then '(nil)'
|
37
|
+
end + ' ' + [
|
38
|
+
@actions.size.to_s,
|
39
|
+
@before_hooks.size,
|
40
|
+
@after_hooks.size
|
41
|
+
].join(', ')
|
42
|
+
end
|
43
|
+
|
44
|
+
def printout(stack='')
|
45
|
+
stack + display_name + "\n" +
|
46
|
+
@children.map {|c| c.printout(stack + ' ') }.join
|
47
|
+
end
|
48
|
+
|
49
|
+
# MATCHING
|
50
|
+
|
51
|
+
def regexp
|
52
|
+
@regexp ||=
|
53
|
+
Regexp.new('^' + case @token
|
54
|
+
when Regexp then "(#{@token.source})"
|
55
|
+
when Symbol then '(.+)'
|
56
|
+
when String then @token.gsub(/:\w+/, '(.+)')
|
57
|
+
#when nil then '\/*'
|
58
|
+
end + '$')
|
59
|
+
end
|
60
|
+
|
61
|
+
def match?(path_fragment)
|
62
|
+
!!path_fragment.match(regexp)
|
63
|
+
end
|
64
|
+
|
65
|
+
def capture_names
|
66
|
+
case @token
|
67
|
+
when Regexp then ['match']
|
68
|
+
when Symbol then [@token.to_s]
|
69
|
+
when String
|
70
|
+
@token.scan(/:\w+/).map {|m| m[1..-1] }
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
|
75
|
+
# DSL
|
76
|
+
|
77
|
+
def route(location=nil, &blk)
|
78
|
+
descend(location).instance_eval(&blk) if blk
|
79
|
+
self
|
80
|
+
end
|
81
|
+
alias :with :route
|
82
|
+
alias :w :route
|
83
|
+
alias :r :route
|
84
|
+
alias :_ :route
|
85
|
+
def apply(layer); route(&layer); end
|
86
|
+
|
87
|
+
def action(location=nil, namespace=:default, &blk)
|
88
|
+
descend(location).action_namespaces[namespace] = blk
|
89
|
+
self
|
90
|
+
end
|
91
|
+
alias :a :action
|
92
|
+
alias :o :action
|
93
|
+
def get( loc=nil, &blk); action(loc, :get, &blk); end
|
94
|
+
def put( loc=nil, &blk); action(loc, :put, &blk); end
|
95
|
+
def post( loc=nil, &blk); action(loc, :post, &blk); end
|
96
|
+
def delete(loc=nil, &blk); action(loc, :delete, &blk); end
|
97
|
+
|
98
|
+
def helpers(location=nil, &blk)
|
99
|
+
descend(location).helper_scope.module_eval(&blk)
|
100
|
+
end
|
101
|
+
|
102
|
+
def before(location=nil, &blk)
|
103
|
+
descend(location).before_hooks << blk if blk
|
104
|
+
end
|
105
|
+
alias :b :before
|
106
|
+
|
107
|
+
def after(location=nil, &blk)
|
108
|
+
descend(location).after_hooks << blk if blk
|
109
|
+
end
|
110
|
+
|
111
|
+
def not_found(location=nil, &blk)
|
112
|
+
descend(location).not_found_handler = blk if blk
|
113
|
+
end
|
114
|
+
alias :x :not_found
|
115
|
+
|
116
|
+
def postprocessor(location=nil, &blk)
|
117
|
+
descend(location).postprocessors << blk if blk
|
118
|
+
end
|
119
|
+
alias :p :postprocessor
|
120
|
+
|
121
|
+
def mount(node, location=nil)
|
122
|
+
if node.token
|
123
|
+
descend(location).children < node
|
124
|
+
else
|
125
|
+
raise 'root nodes can not be mounted. apply a layer instead.'
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
|
130
|
+
# LOOKUPS
|
131
|
+
|
132
|
+
def match(path=[])
|
133
|
+
dialect::Match.new(self, nil, nil).match(path)
|
134
|
+
end
|
135
|
+
|
136
|
+
def descend(path)
|
137
|
+
loc = parse_path(path)
|
138
|
+
if loc.empty? then self else
|
139
|
+
token = loc.shift
|
140
|
+
(get_child(token) || make_child(token)).descend(loc)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
|
145
|
+
private
|
146
|
+
|
147
|
+
def get_child(token)
|
148
|
+
@children.each do |child|
|
149
|
+
return child if child.token == token
|
150
|
+
end; nil
|
151
|
+
end
|
152
|
+
|
153
|
+
def make_child(token)
|
154
|
+
child = self.class.new(token)
|
155
|
+
@children << child
|
156
|
+
child
|
157
|
+
end
|
158
|
+
|
159
|
+
# convert any valid path syntax to array syntax
|
160
|
+
def parse_path(path)
|
161
|
+
case path
|
162
|
+
when nil then []
|
163
|
+
when Array then path
|
164
|
+
when Regexp, Symbol then [path]
|
165
|
+
when String then parse_string_path(path)
|
166
|
+
else
|
167
|
+
raise "invalid path #{path}"
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
# convert a string to array path syntax
|
172
|
+
def parse_string_path(path)
|
173
|
+
path.gsub(/(^\/)|(\/$)/, ''). # remove trailing slashes
|
174
|
+
split('/').map do |str| # split tokens
|
175
|
+
str.match(/^:\w+$/) ? # replace ':c' with :c
|
176
|
+
str[1..-1].to_sym : str
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
@@ -0,0 +1,19 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
class ActionTree::CaptureHash < Hash
|
4
|
+
def merge!(hsh)
|
5
|
+
hsh.each {|k, v| add(k,v) }
|
6
|
+
end
|
7
|
+
|
8
|
+
def merge(hsh)
|
9
|
+
dup.merge!(hsh)
|
10
|
+
end
|
11
|
+
|
12
|
+
def add(key, value)
|
13
|
+
case self[key]
|
14
|
+
when nil then self[key] = value
|
15
|
+
when Array then self[key] << value
|
16
|
+
else self[key] = [self[key], value]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
|
2
|
+
# The scope in which matched actions are run. Used to copy instance variables, define instance variables from captures, and extend helper method modules.
|
3
|
+
|
4
|
+
class ActionTree::EvalScope
|
5
|
+
def initialize(*ingredients)
|
6
|
+
ingredients.each do |ing|
|
7
|
+
case ing
|
8
|
+
when Hash
|
9
|
+
ing.each do |name, value|
|
10
|
+
instance_variable_set(:"@#{name}", value)
|
11
|
+
end
|
12
|
+
when Module
|
13
|
+
extend ing
|
14
|
+
else
|
15
|
+
ing.instance_variables.each do |name|
|
16
|
+
self.instance_variable_set(name,
|
17
|
+
ing.instance_variable_get(name)
|
18
|
+
)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,65 @@
|
|
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
|
+
|