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/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,12 @@
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
@@ -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
+