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.
@@ -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, v| add(k,v) }
16
+ hsh.each {|k,v| add(k,v) }
17
+ self
6
18
  end
7
19
 
8
- def merge(hsh)
9
- dup.merge!(hsh)
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
+