action_tree 0.1.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/.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
|
+
|