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.
@@ -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
+