manveru-innate 2009.02.25 → 2009.03.24
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +383 -0
- data/MANIFEST +17 -8
- data/README.md +222 -136
- data/Rakefile +7 -2
- data/example/provides.rb +28 -0
- data/innate.gemspec +19 -10
- data/lib/innate/action.rb +21 -47
- data/lib/innate/adapter.rb +65 -56
- data/lib/innate/cache.rb +16 -8
- data/lib/innate/dynamap.rb +39 -29
- data/lib/innate/helper/aspect.rb +60 -0
- data/lib/innate/helper/cgi.rb +2 -0
- data/lib/innate/helper/link.rb +43 -11
- data/lib/innate/helper/partial.rb +6 -5
- data/lib/innate/helper.rb +10 -13
- data/lib/innate/log/hub.rb +1 -0
- data/lib/innate/log.rb +3 -6
- data/lib/{rack → innate}/middleware_compiler.rb +19 -12
- data/lib/innate/mock.rb +3 -2
- data/lib/innate/node.rb +573 -179
- data/lib/innate/options/dsl.rb +46 -6
- data/lib/innate/options/stub.rb +7 -0
- data/lib/innate/options.rb +14 -93
- data/lib/innate/request.rb +21 -7
- data/lib/innate/response.rb +12 -0
- data/lib/innate/route.rb +2 -3
- data/lib/innate/session.rb +37 -20
- data/lib/innate/spec.rb +4 -0
- data/lib/innate/state/fiber.rb +14 -7
- data/lib/innate/state/thread.rb +10 -2
- data/lib/innate/state.rb +8 -11
- data/lib/innate/traited.rb +14 -6
- data/lib/innate/trinity.rb +0 -4
- data/lib/innate/version.rb +1 -1
- data/lib/innate/view/erb.rb +4 -2
- data/lib/innate/view/none.rb +2 -2
- data/lib/innate/view.rb +14 -21
- data/lib/innate.rb +49 -30
- data/spec/helper.rb +8 -0
- data/spec/innate/action/layout.rb +9 -6
- data/spec/innate/cache/common.rb +3 -3
- data/spec/innate/helper/aspect.rb +3 -5
- data/spec/innate/helper/flash.rb +1 -1
- data/spec/innate/helper/link.rb +45 -2
- data/spec/innate/helper/partial.rb +34 -10
- data/spec/innate/helper/view/loop.erb +1 -1
- data/spec/innate/helper/view/recursive.erb +1 -1
- data/spec/innate/node/mapping.rb +37 -0
- data/spec/innate/node/node.rb +142 -0
- data/spec/innate/node/resolve.rb +82 -0
- data/spec/innate/node/{another_layout → view/another_layout}/another_layout.erb +0 -0
- data/spec/innate/node/{bar.html → view/bar.erb} +0 -0
- data/spec/innate/node/{foo.html.erb → view/foo.html.erb} +0 -0
- data/spec/innate/node/{only_view.html → view/only_view.erb} +0 -0
- data/spec/innate/node/view/with_layout.erb +1 -0
- data/spec/innate/node/wrap_action_call.rb +83 -0
- data/spec/innate/options.rb +28 -6
- data/spec/innate/provides/list.html.erb +1 -0
- data/spec/innate/provides/list.txt.erb +1 -0
- data/spec/innate/provides.rb +99 -0
- data/spec/innate/request.rb +23 -10
- data/spec/innate/route.rb +2 -4
- data/spec/innate/session.rb +1 -1
- data/spec/innate/state/fiber.rb +57 -0
- data/spec/innate/state/thread.rb +40 -0
- metadata +20 -11
- data/lib/rack/reloader.rb +0 -192
- data/spec/innate/node/with_layout.erb +0 -3
- data/spec/innate/node.rb +0 -224
data/lib/innate/node.rb
CHANGED
@@ -22,24 +22,36 @@ module Innate
|
|
22
22
|
# {Innate::Session} instances, and all the standard helper methods as well as
|
23
23
|
# the ability to simply add other helpers.
|
24
24
|
#
|
25
|
-
#
|
26
|
-
#
|
27
|
-
#
|
28
|
-
# Those methods, and all other methods occurring in the ancestors after
|
29
|
-
# {Innate::Node} will not be considered valid action methods and will be
|
30
|
-
# ignored.
|
31
|
-
# * This also means that method_missing will not see any of the requests
|
32
|
-
# coming in.
|
33
|
-
# * If you want an action to act as a catch-all, use `def index(*args)`.
|
34
|
-
|
25
|
+
# Please note that method_missing will _not_ be considered when building an
|
26
|
+
# {Action}. There might be future demand for this, but for now you can simply
|
27
|
+
# use `def index(*args); end` to make a catch-all action.
|
35
28
|
module Node
|
36
29
|
include Traited
|
37
30
|
|
38
31
|
DEFAULT_HELPERS = %w[aspect cgi flash link partial redirect send_file]
|
39
32
|
NODE_LIST = Set.new
|
40
33
|
|
41
|
-
|
42
|
-
|
34
|
+
# These traits are inherited into ancestors, changing a trait in an
|
35
|
+
# ancestor doesn't affect the higher ones.
|
36
|
+
#
|
37
|
+
# class Foo; include Innate::Node; end
|
38
|
+
# class Bar < Foo; end
|
39
|
+
#
|
40
|
+
# Foo.trait[:wrap] == Bar.trait[:wrap] # => true
|
41
|
+
# Bar.trait(:wrap => [:cache_wrap])
|
42
|
+
# Foo.trait[:wrap] == Bar.trait[:wrap] # => false
|
43
|
+
|
44
|
+
trait :views => []
|
45
|
+
trait :layouts => []
|
46
|
+
trait :layout => nil
|
47
|
+
trait :alias_view => {}
|
48
|
+
trait :provide => {}
|
49
|
+
|
50
|
+
# @see wrap_action_call
|
51
|
+
trait :wrap => SortedSet.new
|
52
|
+
trait :provide_set => false
|
53
|
+
trait :needs_method => false
|
54
|
+
trait :skip_node_map => false
|
43
55
|
|
44
56
|
# Upon inclusion we make ourselves comfortable.
|
45
57
|
def self.included(into)
|
@@ -50,90 +62,172 @@ module Innate
|
|
50
62
|
|
51
63
|
NODE_LIST << into
|
52
64
|
|
53
|
-
return if into.
|
54
|
-
into.provide(:html
|
65
|
+
return if into.provide_set?
|
66
|
+
into.provide(:html, :ERB)
|
55
67
|
into.trait(:provide_set => false)
|
56
68
|
end
|
57
69
|
|
70
|
+
# node mapping procedure
|
71
|
+
#
|
72
|
+
# when Node is included into an object, it's added to NODE_LIST
|
73
|
+
# when object::map(location) is sent, it maps the object into DynaMap
|
74
|
+
# when Innate.start is issued, it calls Node::setup
|
75
|
+
# Node::setup iterates NODE_LIST and maps all objects not in DynaMap by
|
76
|
+
# using Node::generate_mapping(object.name) as location
|
77
|
+
#
|
78
|
+
# when object::map(nil) is sent, the object will be skipped in Node::setup
|
79
|
+
|
58
80
|
def self.setup
|
59
|
-
NODE_LIST.each{|node|
|
60
|
-
|
81
|
+
NODE_LIST.each{|node|
|
82
|
+
node.map(generate_mapping(node.name)) unless node.trait[:skip_node_map]
|
83
|
+
}
|
84
|
+
# Log.debug("Mapped Nodes: %p" % DynaMap.to_hash) unless NODE_LIST.empty?
|
85
|
+
end
|
86
|
+
|
87
|
+
def self.generate_mapping(object_name = self.name)
|
88
|
+
return '/' if NODE_LIST.size == 1
|
89
|
+
parts = object_name.split('::').map{|part|
|
90
|
+
part.gsub(/^[A-Z]+/){|sub| sub.downcase }.gsub(/[A-Z]+[^A-Z]/, '_\&')
|
91
|
+
}
|
92
|
+
'/' << parts.join('/').downcase
|
61
93
|
end
|
62
94
|
|
95
|
+
# Tries to find the relative url that this {Node} is mapped to.
|
96
|
+
# If it cannot find one it will instead generate one based on the
|
97
|
+
# snake_cased name of itself.
|
98
|
+
#
|
99
|
+
# @example Usage:
|
100
|
+
#
|
101
|
+
# class FooBar
|
102
|
+
# include Innate::Node
|
103
|
+
# end
|
104
|
+
# FooBar.mapping # => '/foo_bar'
|
105
|
+
#
|
63
106
|
# @return [String] the relative path to the node
|
107
|
+
#
|
108
|
+
# @api external
|
109
|
+
# @see Innate::SingletonMethods#to
|
110
|
+
# @author manveru
|
64
111
|
def mapping
|
65
|
-
|
66
|
-
return mapped if mapped
|
67
|
-
return '/' if NODE_LIST.size == 1
|
68
|
-
"/" << self.name.gsub(/\B[A-Z][^A-Z]/, '_\&').downcase
|
112
|
+
Innate.to(self)
|
69
113
|
end
|
70
114
|
|
71
|
-
# Shortcut to map or remap this Node
|
115
|
+
# Shortcut to map or remap this Node.
|
116
|
+
#
|
117
|
+
# @example Usage for explicit mapping:
|
118
|
+
#
|
119
|
+
# class FooBar
|
120
|
+
# include Innate::Node
|
121
|
+
# map '/foo_bar'
|
122
|
+
# end
|
123
|
+
#
|
124
|
+
# Innate.to(FooBar) # => '/foo_bar'
|
125
|
+
#
|
126
|
+
# @example Usage for automatic mapping:
|
127
|
+
#
|
128
|
+
# class FooBar
|
129
|
+
# include Innate::Node
|
130
|
+
# map mapping
|
131
|
+
# end
|
132
|
+
#
|
133
|
+
# Innate.to(FooBar) # => '/foo_bar'
|
134
|
+
#
|
72
135
|
# @param [#to_s] location
|
136
|
+
#
|
137
|
+
# @api external
|
138
|
+
# @see Innate::SingletonMethods::map
|
139
|
+
# @author manveru
|
73
140
|
def map(location)
|
74
|
-
|
141
|
+
trait :skip_node_map => true
|
142
|
+
Innate.map(location, self) if location
|
75
143
|
end
|
76
144
|
|
77
|
-
#
|
78
|
-
# content from a single action.
|
145
|
+
# Specify which way contents are provided and processed.
|
79
146
|
#
|
80
|
-
#
|
147
|
+
# Use this to set a templating engine, custom Content-Type, or pass a block
|
148
|
+
# to take over the processing of the {Action} and template yourself.
|
81
149
|
#
|
82
|
-
#
|
83
|
-
# include Innate::Node
|
84
|
-
# map '/feed'
|
150
|
+
# Provides set via this method will be inherited into subclasses.
|
85
151
|
#
|
86
|
-
#
|
152
|
+
# The +format+ is extracted from the PATH_INFO, it simply represents the
|
153
|
+
# last extension name in the path.
|
87
154
|
#
|
88
|
-
#
|
89
|
-
#
|
90
|
-
#
|
91
|
-
#
|
155
|
+
# The provide also has influence on the chosen templates for the {Action}.
|
156
|
+
#
|
157
|
+
# @example providing RSS with ERB templating
|
158
|
+
#
|
159
|
+
# provide :rss, :engine => :ERB
|
160
|
+
#
|
161
|
+
# Given a request to `/list.rss` the template lookup first tries to find
|
162
|
+
# `list.rss.erb`, if that fails it falls back to `list.erb`.
|
163
|
+
# If neither of these are available it will try to use the return value of
|
164
|
+
# the method in the {Action} as template.
|
165
|
+
#
|
166
|
+
# A request to `/list.yaml` would match the format 'yaml'
|
92
167
|
#
|
93
|
-
#
|
168
|
+
# @example providing a yaml version of actions
|
94
169
|
#
|
95
|
-
#
|
96
|
-
#
|
97
|
-
# /
|
170
|
+
# class Articles
|
171
|
+
# include Innate::Node
|
172
|
+
# map '/article'
|
98
173
|
#
|
99
|
-
#
|
174
|
+
# provide(:yaml, :type => 'text/yaml'){|action, value| value.to_yaml }
|
100
175
|
#
|
101
|
-
#
|
176
|
+
# def list
|
177
|
+
# @articles = Article.list
|
178
|
+
# end
|
179
|
+
# end
|
102
180
|
#
|
103
|
-
#
|
104
|
-
# back to the one specified for html.
|
181
|
+
# @example providing plain text inspect version
|
105
182
|
#
|
106
|
-
#
|
107
|
-
#
|
183
|
+
# class Articles
|
184
|
+
# include Innate::Node
|
185
|
+
# map '/article'
|
108
186
|
#
|
109
|
-
#
|
110
|
-
# engine, use :none like:
|
187
|
+
# provide(:txt, :type => 'text/plain'){|action, value| value.inspect }
|
111
188
|
#
|
112
|
-
#
|
189
|
+
# def list
|
190
|
+
# @articles = Article.list
|
191
|
+
# end
|
192
|
+
# end
|
113
193
|
#
|
114
|
-
#
|
194
|
+
# @param [Proc] block
|
195
|
+
# upon calling the action, [action, value] will be passed to it and its
|
196
|
+
# return value becomes the response body.
|
115
197
|
#
|
116
|
-
#
|
198
|
+
# @option param :engine [Symbol String]
|
199
|
+
# Name of an engine for View::get
|
200
|
+
# @option param :type [String]
|
201
|
+
# default Content-Type if none was set in Response
|
117
202
|
#
|
118
|
-
#
|
203
|
+
# @raise [ArgumentError] if neither a block nor an engine was given
|
119
204
|
#
|
120
|
-
#
|
121
|
-
#
|
122
|
-
#
|
123
|
-
# /view/index.erb
|
124
|
-
# /view/feed.rss.erb
|
205
|
+
# @api external
|
206
|
+
# @see View::get Node#provides
|
207
|
+
# @author manveru
|
125
208
|
#
|
126
|
-
#
|
127
|
-
#
|
209
|
+
# @todo
|
210
|
+
# The comment of this method may be too short for the effects it has on
|
211
|
+
# the rest of Innate, if you feel something is missing please let me
|
212
|
+
# know.
|
128
213
|
|
129
|
-
def provide(
|
130
|
-
|
214
|
+
def provide(format, param = {}, &block)
|
215
|
+
if param.respond_to?(:to_hash)
|
216
|
+
param = param.to_hash
|
217
|
+
handler = block || View.get(param[:engine])
|
218
|
+
content_type = param[:type]
|
219
|
+
else
|
220
|
+
handler = View.get(param)
|
221
|
+
end
|
222
|
+
|
223
|
+
raise(ArgumentError, "Need an engine or block") unless handler
|
131
224
|
|
132
|
-
|
133
|
-
|
134
|
-
|
225
|
+
trait("#{format}_handler" => handler, :provide_set => true)
|
226
|
+
trait("#{format}_content_type" => content_type) if content_type
|
227
|
+
end
|
135
228
|
|
136
|
-
|
229
|
+
def provides
|
230
|
+
ancestral_trait.reject{|k,v| k !~ /_handler$/ }
|
137
231
|
end
|
138
232
|
|
139
233
|
# This makes the Node a valid application for Rack.
|
@@ -150,12 +244,18 @@ module Innate
|
|
150
244
|
# We do however log errors at some vital points in order to provide you
|
151
245
|
# with feedback in your logs.
|
152
246
|
#
|
153
|
-
#
|
154
|
-
#
|
155
|
-
#
|
156
|
-
#
|
157
|
-
#
|
158
|
-
#
|
247
|
+
# A lot of functionality in here relies on the fact that call is executed
|
248
|
+
# within Innate::STATE.wrap which populates the variables used by Trinity.
|
249
|
+
# So if you use the Node directly as a middleware make sure that you #use
|
250
|
+
# Innate::Current as a middleware before it.
|
251
|
+
#
|
252
|
+
# @param [Hash] env
|
253
|
+
#
|
254
|
+
# @return [Array]
|
255
|
+
#
|
256
|
+
# @api external
|
257
|
+
# @see Response#reset Node#try_resolve Session#flush
|
258
|
+
# @author manveru
|
159
259
|
|
160
260
|
def call(env)
|
161
261
|
path = env['PATH_INFO']
|
@@ -163,7 +263,6 @@ module Innate
|
|
163
263
|
|
164
264
|
response.reset
|
165
265
|
response = try_resolve(path)
|
166
|
-
response['Content-Type'] ||= 'text/html'
|
167
266
|
|
168
267
|
Current.session.flush(response)
|
169
268
|
|
@@ -171,29 +270,42 @@ module Innate
|
|
171
270
|
end
|
172
271
|
|
173
272
|
# Let's try to find some valid action for given +path+.
|
174
|
-
# Otherwise we dispatch to action_missing
|
273
|
+
# Otherwise we dispatch to {action_missing}.
|
274
|
+
#
|
275
|
+
# @param [String] path from env['PATH_INFO']
|
175
276
|
#
|
176
|
-
# @
|
277
|
+
# @return [Response]
|
278
|
+
#
|
279
|
+
# @api external
|
280
|
+
# @see Node#resolve Node#action_found Node#action_missing
|
281
|
+
# @author manveru
|
177
282
|
def try_resolve(path)
|
178
283
|
action = resolve(path)
|
179
284
|
action ? action_found(action) : action_missing(path)
|
180
285
|
end
|
181
286
|
|
182
|
-
# Executed once an Action has been found.
|
287
|
+
# Executed once an {Action} has been found.
|
288
|
+
#
|
183
289
|
# Reset the {Innate::Response} instance, catch :respond and :redirect.
|
184
290
|
# {Action#call} has to return a String.
|
185
291
|
#
|
186
|
-
# @param [
|
292
|
+
# @param [Action] action
|
293
|
+
#
|
187
294
|
# @return [Innate::Response]
|
295
|
+
#
|
296
|
+
# @api external
|
297
|
+
# @see Action#call Innate::Response
|
298
|
+
# @author manveru
|
188
299
|
def action_found(action)
|
189
|
-
|
300
|
+
response = catch(:respond){ catch(:redirect){ action.call }}
|
190
301
|
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
Current.response.write(result)
|
195
|
-
return Current.response
|
302
|
+
unless response.respond_to?(:finish)
|
303
|
+
self.response.write(response)
|
304
|
+
response = self.response
|
196
305
|
end
|
306
|
+
|
307
|
+
response['Content-Type'] ||= action.options[:content_type]
|
308
|
+
response
|
197
309
|
end
|
198
310
|
|
199
311
|
# The default handler in case no action was found, kind of method_missing.
|
@@ -227,7 +339,10 @@ module Innate
|
|
227
339
|
# end
|
228
340
|
#
|
229
341
|
# @param [String] path
|
230
|
-
#
|
342
|
+
#
|
343
|
+
# @api external
|
344
|
+
# @see Innate::Response Node#try_resolve
|
345
|
+
# @author manveru
|
231
346
|
def action_missing(path)
|
232
347
|
response.status = 404
|
233
348
|
response['Content-Type'] = 'text/plain'
|
@@ -241,57 +356,93 @@ module Innate
|
|
241
356
|
# html.
|
242
357
|
#
|
243
358
|
# @param [String] path
|
244
|
-
#
|
359
|
+
#
|
360
|
+
# @return [nil, Action]
|
361
|
+
#
|
362
|
+
# @api external
|
245
363
|
# @see Node::find_provide Node::update_method_arities Node::find_action
|
246
364
|
# @author manveru
|
247
365
|
def resolve(path)
|
248
|
-
name, wish = find_provide(path)
|
366
|
+
name, wish, engine = find_provide(path)
|
367
|
+
action = Action.create(:node => self, :wish => wish, :engine => engine)
|
368
|
+
|
369
|
+
if content_type = ancestral_trait["#{wish}_content_type"]
|
370
|
+
action.options = {:content_type => content_type}
|
371
|
+
end
|
372
|
+
|
249
373
|
update_method_arities
|
250
|
-
|
374
|
+
fill_action(action, name)
|
251
375
|
end
|
252
376
|
|
377
|
+
# Resolve possible provides for the given +path+ from {provides}.
|
378
|
+
#
|
253
379
|
# @param [String] path
|
254
|
-
#
|
255
|
-
# @
|
380
|
+
#
|
381
|
+
# @return [Array] with name, wish, engine
|
382
|
+
#
|
383
|
+
# @api internal
|
384
|
+
# @see Node::provide Node::provides
|
256
385
|
# @author manveru
|
257
386
|
def find_provide(path)
|
258
|
-
|
387
|
+
pr = provides
|
388
|
+
|
389
|
+
name, wish, engine = path, 'html', pr['html_handler']
|
259
390
|
|
260
|
-
|
391
|
+
pr.find do |key, value|
|
392
|
+
key = key[/(.*)_handler$/, 1]
|
261
393
|
next unless path =~ /^(.+)\.#{key}$/i
|
262
|
-
name, wish = $1, key
|
394
|
+
name, wish, engine = $1, key, value
|
263
395
|
end
|
264
396
|
|
265
|
-
return name, wish
|
397
|
+
return name, wish, engine
|
266
398
|
end
|
267
399
|
|
268
|
-
# Now we're talking Action, we try to find a matching template and
|
269
|
-
# if we can't find either we go to the next pattern, otherwise we
|
270
|
-
# with an Action with everything we know so far about the demands
|
271
|
-
# client.
|
400
|
+
# Now we're talking {Action}, we try to find a matching template and
|
401
|
+
# method, if we can't find either we go to the next pattern, otherwise we
|
402
|
+
# answer with an {Action} with everything we know so far about the demands
|
403
|
+
# of the client.
|
272
404
|
#
|
273
405
|
# @param [String] given_name the name extracted from REQUEST_PATH
|
274
406
|
# @param [String] wish
|
407
|
+
#
|
408
|
+
# @return [Action, nil]
|
409
|
+
#
|
410
|
+
# @api internal
|
411
|
+
# @see Node#find_method Node#find_view Node#find_layout Node#patterns_for
|
412
|
+
# Action#wish Action#merge!
|
275
413
|
# @author manveru
|
276
|
-
def
|
277
|
-
needs_method =
|
414
|
+
def fill_action(action, given_name)
|
415
|
+
needs_method = self.needs_method?
|
416
|
+
wish = action.wish
|
278
417
|
|
279
418
|
patterns_for(given_name) do |name, params|
|
280
419
|
method = find_method(name, params)
|
281
|
-
view = find_view(name, wish)
|
282
420
|
|
283
|
-
next unless view or method
|
284
421
|
next unless method if needs_method
|
285
422
|
next unless method if params.any?
|
423
|
+
next unless (view = find_view(name, wish)) or method
|
286
424
|
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
Action.create(:method => method, :params => params, :layout => layout,
|
291
|
-
:node => self, :view => view, :wish => wish)
|
425
|
+
action.merge!(:method => method, :view => view, :params => params,
|
426
|
+
:layout => find_layout(name, wish))
|
292
427
|
end
|
293
428
|
end
|
294
429
|
|
430
|
+
# Try to find a suitable value for the layout. This may be a template or
|
431
|
+
# the name of a method.
|
432
|
+
#
|
433
|
+
# If a layout could be found, an Array with two elements is returned, the
|
434
|
+
# first indicating the kind of layout (:layout|:view|:method), the second
|
435
|
+
# the found value, which may be a String or Symbol.
|
436
|
+
#
|
437
|
+
# @param [String] name
|
438
|
+
# @param [String] wish
|
439
|
+
#
|
440
|
+
# @return [Array, nil]
|
441
|
+
#
|
442
|
+
# @api external
|
443
|
+
# @see Node#to_layout Node#find_method Node#find_view
|
444
|
+
# @author manveru
|
445
|
+
#
|
295
446
|
# @todo allow layouts combined of method and view... hairy :)
|
296
447
|
def find_layout(name, wish)
|
297
448
|
return unless layout = ancestral_trait[:layout]
|
@@ -306,8 +457,8 @@ module Innate
|
|
306
457
|
end
|
307
458
|
end
|
308
459
|
|
309
|
-
#
|
310
|
-
#
|
460
|
+
# We check arity if possible, but will happily dispatch to any method that
|
461
|
+
# has default parameters.
|
311
462
|
# If you don't want your method to be responsible for messing up a request
|
312
463
|
# you should think twice about the arguments you specify due to limitations
|
313
464
|
# in Ruby.
|
@@ -336,10 +487,19 @@ module Innate
|
|
336
487
|
# def index(a = :a, b = :b) # => -1
|
337
488
|
# def index(a = :a, b = :b, *r) # => -1
|
338
489
|
#
|
490
|
+
# @param [String, Symbol] name
|
491
|
+
# @param [Array] params
|
492
|
+
#
|
493
|
+
# @return [String, Symbol]
|
494
|
+
#
|
495
|
+
# @api external
|
496
|
+
# @see Node#fill_action Node#find_layout
|
497
|
+
# @author manveru
|
498
|
+
#
|
339
499
|
# @todo Once 1.9 is mainstream we can use Method#parameters to do accurate
|
340
500
|
# prediction
|
341
501
|
def find_method(name, params)
|
342
|
-
return unless arity =
|
502
|
+
return unless arity = method_arities[name]
|
343
503
|
name if arity == params.size or arity < 0
|
344
504
|
end
|
345
505
|
|
@@ -356,40 +516,61 @@ module Innate
|
|
356
516
|
# Hi.update_method_arities
|
357
517
|
# # => {'index' => 0, 'foo' => -1, 'bar => 2}
|
358
518
|
#
|
359
|
-
# @
|
519
|
+
# @api internal
|
520
|
+
# @see Node#resolve
|
360
521
|
# @return [Hash] mapping the name of the methods to their arity
|
361
522
|
def update_method_arities
|
362
|
-
|
363
|
-
trait(:method_arities => arities)
|
523
|
+
@method_arities = {}
|
364
524
|
|
365
525
|
exposed = ancestors & Helper::EXPOSE.to_a
|
366
526
|
higher = ancestors.select{|a| a < Innate::Node }
|
367
527
|
|
368
528
|
(higher + exposed).reverse_each do |ancestor|
|
369
529
|
ancestor.public_instance_methods(false).each do |im|
|
370
|
-
|
530
|
+
@method_arities[im.to_s] = ancestor.instance_method(im).arity
|
371
531
|
end
|
372
532
|
end
|
373
533
|
|
374
|
-
|
534
|
+
@method_arities
|
375
535
|
end
|
376
536
|
|
377
|
-
|
378
|
-
|
537
|
+
attr_reader :method_arities
|
538
|
+
|
539
|
+
# Try to find the best template for the given basename and wish and respect
|
540
|
+
# aliased views.
|
541
|
+
#
|
542
|
+
# @param [#to_s] file
|
543
|
+
# @param [#to_s] wish
|
544
|
+
#
|
545
|
+
# @return [String, nil] depending whether a template could be found
|
546
|
+
#
|
547
|
+
# @api external
|
548
|
+
# @see Node#to_template Node#find_aliased_view
|
549
|
+
# @author manveru
|
379
550
|
def find_view(file, wish)
|
380
551
|
aliased = find_aliased_view(file, wish)
|
381
552
|
return aliased if aliased
|
382
553
|
|
383
|
-
|
384
|
-
to_template(path, wish)
|
554
|
+
to_view(file, wish)
|
385
555
|
end
|
386
556
|
|
387
|
-
#
|
388
|
-
#
|
389
|
-
#
|
390
|
-
|
391
|
-
|
392
|
-
|
557
|
+
# Try to find the best template for the given basename and wish.
|
558
|
+
#
|
559
|
+
# This method is mostly here for symetry with {to_layout} and to allow you
|
560
|
+
# overriding the template lookup easily.
|
561
|
+
#
|
562
|
+
# @param [#to_s] file
|
563
|
+
# @param [#to_s] wish
|
564
|
+
#
|
565
|
+
# @return [String, nil] depending whether a template could be found
|
566
|
+
#
|
567
|
+
# @api external
|
568
|
+
# @see {Node#find_view} {Node#to_template} {Node#root_mappings}
|
569
|
+
# {Node#view_mappings} {Node#to_template}
|
570
|
+
# @author manveru
|
571
|
+
def to_view(file, wish)
|
572
|
+
path = root_mappings.concat(view_mappings) << file
|
573
|
+
to_template(path, wish)
|
393
574
|
end
|
394
575
|
|
395
576
|
# Aliasing one view from another.
|
@@ -417,7 +598,9 @@ module Innate
|
|
417
598
|
#
|
418
599
|
# @param [#to_s] to view that should be replaced
|
419
600
|
# @param [#to_s] from view to use or Node.
|
420
|
-
# @param [#nil
|
601
|
+
# @param [#nil?, Node] node optionally obtain view from this Node
|
602
|
+
#
|
603
|
+
# @api external
|
421
604
|
# @see Node::find_aliased_view
|
422
605
|
# @author manveru
|
423
606
|
def alias_view(to, from, node = nil)
|
@@ -425,9 +608,14 @@ module Innate
|
|
425
608
|
trait[:alias_view][to.to_s] = node ? [from.to_s, node] : from.to_s
|
426
609
|
end
|
427
610
|
|
611
|
+
# Resolve one level of aliasing for the given +file+ and +wish+.
|
612
|
+
#
|
428
613
|
# @param [String] file
|
429
614
|
# @param [String] wish
|
430
|
-
#
|
615
|
+
#
|
616
|
+
# @return [nil, String] the absolute path to the aliased template or nil
|
617
|
+
#
|
618
|
+
# @api internal
|
431
619
|
# @see Node::alias_view Node::find_view
|
432
620
|
# @author manveru
|
433
621
|
def find_aliased_view(file, wish)
|
@@ -438,53 +626,55 @@ module Innate
|
|
438
626
|
|
439
627
|
# Find the best matching file for the layout, if any.
|
440
628
|
#
|
629
|
+
# This is mostly an abstract method that you might find handy if you want
|
630
|
+
# to do vastly different layout lookup.
|
631
|
+
#
|
441
632
|
# @param [String] file
|
442
633
|
# @param [String] wish
|
443
|
-
#
|
444
|
-
# @
|
634
|
+
#
|
635
|
+
# @return [nil, String] the absolute path to the template or nil
|
636
|
+
#
|
637
|
+
# @api external
|
638
|
+
# @see {Node#to_template} {Node#root_mappings} {Node#layout_mappings}
|
445
639
|
# @author manveru
|
446
640
|
def to_layout(file, wish)
|
447
|
-
path =
|
641
|
+
path = root_mappings.concat(layout_mappings) << file
|
448
642
|
to_template(path, wish)
|
449
643
|
end
|
450
644
|
|
451
|
-
# @param [String] file
|
452
|
-
# @param [String] wish
|
453
|
-
# @return [nil String] the absolute path to the template or nil
|
454
|
-
# @see Node::find_view Node::to_layout Node::find_aliased_view
|
455
|
-
# @author manveru
|
456
|
-
def to_template(path, wish)
|
457
|
-
return unless path.all?
|
458
|
-
|
459
|
-
path = File.join(*path.map{|pa| pa.to_s.split('__') }.flatten)
|
460
|
-
exts = (Array[provide[wish]] + provide.keys).flatten.compact.uniq.join(',')
|
461
|
-
glob = "#{path}.{#{wish}.,#{wish},}{#{exts},}"
|
462
|
-
found = Dir[glob].uniq
|
463
|
-
|
464
|
-
if found.size > 1
|
465
|
-
Log.warn("%d views found for %p | %p" % [found.size, path, wish])
|
466
|
-
end
|
467
|
-
|
468
|
-
found.first
|
469
|
-
end
|
470
|
-
|
471
645
|
# Define a layout to use on this Node.
|
472
646
|
#
|
473
|
-
#
|
474
|
-
#
|
475
|
-
# @return [Proc String] The assigned name or block
|
647
|
+
# A Node can only have one layout, although the template being chosen can
|
648
|
+
# depend on {provides}.
|
476
649
|
#
|
477
|
-
# @
|
478
|
-
#
|
479
|
-
#
|
480
|
-
#
|
650
|
+
# @param [String, #to_s] name basename without extension of the layout to use
|
651
|
+
# @param [Proc, #call] block called on every dispatch if no name given
|
652
|
+
#
|
653
|
+
# @return [Proc, String] The assigned name or block
|
654
|
+
#
|
655
|
+
# @api external
|
656
|
+
# @see Node#find_layout Node#layout_paths Node#to_layout Node#app_layout
|
657
|
+
# @author manveru
|
658
|
+
#
|
659
|
+
# NOTE:
|
660
|
+
# The behaviour of Node#layout changed significantly from Ramaze, instead
|
661
|
+
# of multitudes of obscure options and methods like deny_layout we simply
|
662
|
+
# take a block and use the returned value as the name for the layout. No
|
663
|
+
# layout will be used if the block returns nil.
|
481
664
|
def layout(name = nil, &block)
|
482
665
|
if name and block
|
666
|
+
# default name, but still check with block
|
483
667
|
trait(:layout => lambda{|n, w| name if block.call(n, w) })
|
484
668
|
elsif name
|
669
|
+
# name of a method or template
|
485
670
|
trait(:layout => name.to_s)
|
486
671
|
elsif block
|
672
|
+
# call block every request with name and wish, returned value is name
|
673
|
+
# of layout template or method
|
487
674
|
trait(:layout => block)
|
675
|
+
else
|
676
|
+
# remove layout for this node
|
677
|
+
trait(:layout => nil)
|
488
678
|
end
|
489
679
|
|
490
680
|
return ancestral_trait[:layout]
|
@@ -503,7 +693,8 @@ module Innate
|
|
503
693
|
# The last fallback will always be the index action with all of the path
|
504
694
|
# turned into parameters.
|
505
695
|
#
|
506
|
-
# @
|
696
|
+
# @example yielding possible combinations of action names and params
|
697
|
+
#
|
507
698
|
# class Foo; include Innate::Node; map '/'; end
|
508
699
|
#
|
509
700
|
# Foo.patterns_for('/'){|action, params| p action => params }
|
@@ -519,6 +710,14 @@ module Innate
|
|
519
710
|
# # => {"foo__bar"=>["baz"]}
|
520
711
|
# # => {"foo"=>["bar", "baz"]}
|
521
712
|
# # => {"index"=>["foo", "bar", "baz"]}
|
713
|
+
#
|
714
|
+
# @param [String, #split] path usually the PATH_INFO
|
715
|
+
#
|
716
|
+
# @return [Action] it actually returns the first non-nil/false result of yield
|
717
|
+
#
|
718
|
+
# @api internal
|
719
|
+
# @see Node#fill_action
|
720
|
+
# @author manveru
|
522
721
|
def patterns_for(path)
|
523
722
|
atoms = path.split('/')
|
524
723
|
atoms.delete('')
|
@@ -527,7 +726,7 @@ module Innate
|
|
527
726
|
atoms.size.downto(0) do |len|
|
528
727
|
action = atoms[0...len].join('__')
|
529
728
|
params = atoms[len..-1]
|
530
|
-
action = 'index' if action.empty?
|
729
|
+
action = 'index' if action.empty? and params != ['index']
|
531
730
|
|
532
731
|
return result if result = yield(action, params)
|
533
732
|
end
|
@@ -535,24 +734,93 @@ module Innate
|
|
535
734
|
return nil
|
536
735
|
end
|
537
736
|
|
538
|
-
#
|
539
|
-
#
|
540
|
-
#
|
541
|
-
#
|
737
|
+
# Try to find a template at the given +path+ for +wish+.
|
738
|
+
#
|
739
|
+
# Since Innate supports multiple paths to templates the +path+ has to be an
|
740
|
+
# Array that may be nested one level.
|
741
|
+
# The +path+ is then translated by {Node#path_glob} and the +wish+ by
|
742
|
+
# {Node#ext_glob}.
|
743
|
+
#
|
744
|
+
# @example Usage to find available templates
|
745
|
+
#
|
746
|
+
# # This assumes following files:
|
747
|
+
# # view/foo.erb
|
748
|
+
# # view/bar.erb
|
749
|
+
# # view/bar.rss.erb
|
750
|
+
# # view/bar.yaml.erb
|
751
|
+
#
|
752
|
+
# class FooBar
|
753
|
+
# Innate.node('/')
|
754
|
+
# end
|
755
|
+
#
|
756
|
+
# FooBar.to_template(['.', 'view', '/', 'foo'], 'html')
|
757
|
+
# # => "./view/foo.erb"
|
758
|
+
# FooBar.to_template(['.', 'view', '/', 'foo'], 'yaml')
|
759
|
+
# # => "./view/foo.erb"
|
760
|
+
# FooBar.to_template(['.', 'view', '/', 'foo'], 'rss')
|
761
|
+
# # => "./view/foo.erb"
|
762
|
+
#
|
763
|
+
# FooBar.to_template(['.', 'view', '/', 'bar'], 'html')
|
764
|
+
# # => "./view/bar.erb"
|
765
|
+
# FooBar.to_template(['.', 'view', '/', 'bar'], 'yaml')
|
766
|
+
# # => "./view/bar.yaml.erb"
|
767
|
+
# FooBar.to_template(['.', 'view', '/', 'bar'], 'rss')
|
768
|
+
# # => "./view/bar.rss.erb"
|
769
|
+
#
|
770
|
+
# @param [Array<Array<String>>, Array<String>] path
|
771
|
+
# array containing strings and nested (1 level) arrays containing strings
|
772
|
+
# @param [String] wish
|
773
|
+
#
|
774
|
+
# @return [nil, String] relative path to the first template found
|
775
|
+
#
|
776
|
+
# @api external
|
777
|
+
# @see Node#find_view Node#to_layout Node#find_aliased_view
|
778
|
+
# Node#path_glob Node#ext_glob
|
779
|
+
# @author manveru
|
780
|
+
def to_template(path, wish)
|
781
|
+
return unless exts = ext_glob(wish)
|
782
|
+
glob = "#{path_glob(*path)}.#{exts}"
|
783
|
+
found = Dir[glob].uniq
|
784
|
+
|
785
|
+
count = found.size
|
786
|
+
Log.warn("%d views found for %p" % [count, glob]) if count > 1
|
787
|
+
|
788
|
+
found.first
|
789
|
+
end
|
790
|
+
|
791
|
+
# Produce a glob that can be processed by Dir::[] matching the possible
|
792
|
+
# paths to the given +elements+.
|
793
|
+
#
|
794
|
+
# The +elements+ are an Array that may be nested one level, take care to
|
795
|
+
# splat if you try to pass an existing Array.
|
796
|
+
#
|
797
|
+
# @return [String] glob matching possible paths to the given +elements+
|
798
|
+
#
|
799
|
+
# @api internal
|
800
|
+
# @see Node#to_template
|
801
|
+
# @author manveru
|
802
|
+
def path_glob(*elements)
|
803
|
+
File.join(elements.map{|element|
|
804
|
+
"{%s}" % [*element].map{|e| e.to_s.gsub('__', '/') }.join(',')
|
805
|
+
}).gsub(/\/\{\/?\}\//, '/')
|
806
|
+
end
|
807
|
+
|
808
|
+
# Produce a glob that can be processed by Dir::[] matching the extensions
|
809
|
+
# associated with the given +wish+.
|
810
|
+
#
|
811
|
+
# @param [#to_s] wish the extension (no leading '.')
|
542
812
|
#
|
543
|
-
#
|
544
|
-
# authentication, etc...
|
813
|
+
# @return [String] glob matching the valid exts for the given +wish+
|
545
814
|
#
|
546
|
-
# @
|
547
|
-
# @
|
548
|
-
# @see Action#render
|
815
|
+
# @api internal
|
816
|
+
# @see Node#to_template View::exts_of Node#provides
|
549
817
|
# @author manveru
|
550
|
-
def
|
551
|
-
|
552
|
-
|
553
|
-
|
554
|
-
|
555
|
-
|
818
|
+
def ext_glob(wish)
|
819
|
+
pr = provides
|
820
|
+
return unless engine = pr["#{wish}_handler"]
|
821
|
+
engine_exts = View.exts_of(engine).join(',')
|
822
|
+
represented = [*wish].map{|k| "#{k}." }.join(',')
|
823
|
+
"{%s,}{%s}" % [represented, engine_exts]
|
556
824
|
end
|
557
825
|
|
558
826
|
# For compatibility with new Kernel#binding behaviour in 1.9
|
@@ -560,7 +828,125 @@ module Innate
|
|
560
828
|
# @return [Binding] binding of the instance being rendered.
|
561
829
|
# @see Action#binding
|
562
830
|
# @author manveru
|
563
|
-
def binding; super
|
831
|
+
def binding; super end
|
832
|
+
|
833
|
+
# make sure this is an Array and a new instance so modification on the
|
834
|
+
# wrapping array doesn't affect the original option.
|
835
|
+
# [*arr].object_id == arr.object_id if arr is an Array
|
836
|
+
#
|
837
|
+
# @return [Array] list of root directories
|
838
|
+
#
|
839
|
+
# @api external
|
840
|
+
# @author manveru
|
841
|
+
def root_mappings
|
842
|
+
[*options.roots].dup
|
843
|
+
end
|
844
|
+
|
845
|
+
# Set the paths for lookup below the Innate.options.views paths.
|
846
|
+
#
|
847
|
+
# @param [String, Array<String>] locations
|
848
|
+
# Any number of strings indicating the paths where view templates may be
|
849
|
+
# located, relative to Innate.options.roots/Innate.options.views
|
850
|
+
#
|
851
|
+
# @return [Node] self
|
852
|
+
#
|
853
|
+
# @api external
|
854
|
+
# @see {Node#view_mappings}
|
855
|
+
# @author manveru
|
856
|
+
def map_views(*locations)
|
857
|
+
trait :views => locations.flatten.uniq
|
858
|
+
self
|
859
|
+
end
|
860
|
+
|
861
|
+
# Combine Innate.options.views with either the `ancestral_trait[:views]`
|
862
|
+
# or the {Node#mapping} if the trait yields an empty Array.
|
863
|
+
#
|
864
|
+
# @return [Array<String>, Array<Array<String>>]
|
865
|
+
#
|
866
|
+
# @api external
|
867
|
+
# @see {Node#map_views}
|
868
|
+
# @author manveru
|
869
|
+
def view_mappings
|
870
|
+
paths = [*ancestral_trait[:views]]
|
871
|
+
paths = [mapping] if paths.empty?
|
872
|
+
|
873
|
+
[*options.views] + paths
|
874
|
+
end
|
875
|
+
|
876
|
+
# Set the paths for lookup below the Innate.options.layouts paths.
|
877
|
+
#
|
878
|
+
# @param [String, Array<String>] locations
|
879
|
+
# Any number of strings indicating the paths where layout templates may
|
880
|
+
# be located, relative to Innate.options.roots/Innate.options.layouts
|
881
|
+
#
|
882
|
+
# @return [Node] self
|
883
|
+
#
|
884
|
+
# @api external
|
885
|
+
# @see {Node#layout_mappings}
|
886
|
+
# @author manveru
|
887
|
+
def map_layouts(*locations)
|
888
|
+
trait :layouts => locations.flatten.uniq
|
889
|
+
self
|
890
|
+
end
|
891
|
+
|
892
|
+
# Combine Innate.options.layouts with either the `ancestral_trait[:layouts]`
|
893
|
+
# or the {Node#mapping} if the trait yields an empty Array.
|
894
|
+
#
|
895
|
+
# @return [Array<String>, Array<Array<String>>]
|
896
|
+
#
|
897
|
+
# @api external
|
898
|
+
# @see {Node#map_layouts}
|
899
|
+
# @author manveru
|
900
|
+
def layout_mappings
|
901
|
+
paths = [*ancestral_trait[:layouts]]
|
902
|
+
paths = [mapping] if paths.empty?
|
903
|
+
|
904
|
+
[*options.layouts] + paths
|
905
|
+
end
|
906
|
+
|
907
|
+
def options
|
908
|
+
Innate.options
|
909
|
+
end
|
910
|
+
|
911
|
+
# Whether an {Action} can be built without a method.
|
912
|
+
#
|
913
|
+
# The default is to allow actions that use only a view template, but you
|
914
|
+
# might want to turn this on, for example if you have partials in your view
|
915
|
+
# directories.
|
916
|
+
#
|
917
|
+
# @example turning needs_method? on
|
918
|
+
#
|
919
|
+
# class Foo
|
920
|
+
# Innate.node('/')
|
921
|
+
# end
|
922
|
+
#
|
923
|
+
# Foo.needs_method? # => true
|
924
|
+
# Foo.trait :needs_method => false
|
925
|
+
# Foo.needs_method? # => false
|
926
|
+
#
|
927
|
+
# @return [true, false] (false)
|
928
|
+
#
|
929
|
+
# @api external
|
930
|
+
# @see {Node#fill_action}
|
931
|
+
# @author manveru
|
932
|
+
def needs_method?
|
933
|
+
ancestral_trait[:needs_method]
|
934
|
+
end
|
935
|
+
|
936
|
+
# This will return true if the only provides set are by {Node::included}.
|
937
|
+
#
|
938
|
+
# The reasoning behind this is to determine whether the user has touched
|
939
|
+
# the provides at all, in which case we will not override the provides in
|
940
|
+
# subclasses.
|
941
|
+
#
|
942
|
+
# @return [true, false] (false)
|
943
|
+
#
|
944
|
+
# @api internal
|
945
|
+
# @see {Node::included}
|
946
|
+
# @author manveru
|
947
|
+
def provide_set?
|
948
|
+
ancestral_trait[:provide_set]
|
949
|
+
end
|
564
950
|
end
|
565
951
|
|
566
952
|
module SingletonMethods
|
@@ -568,10 +954,14 @@ module Innate
|
|
568
954
|
# +location+.
|
569
955
|
#
|
570
956
|
# @param [#to_s] location where the node is mapped to
|
571
|
-
# @param [Node nil] node the class that will be a node, will try to
|
572
|
-
# up if not given
|
573
|
-
#
|
574
|
-
# @
|
957
|
+
# @param [Node, nil] node the class that will be a node, will try to
|
958
|
+
# look it up if not given
|
959
|
+
#
|
960
|
+
# @return [Class, Module] the node argument or detected class will be
|
961
|
+
# returned
|
962
|
+
#
|
963
|
+
# @api external
|
964
|
+
# @see SingletonMethods::node_from_backtrace
|
575
965
|
# @author manveru
|
576
966
|
def node(location, node = nil)
|
577
967
|
node ||= node_from_backtrace(caller)
|
@@ -588,13 +978,17 @@ module Innate
|
|
588
978
|
# If there are any problems with this (filenames containing ':' or
|
589
979
|
# metaprogramming) just pass the node parameter explicitly to Innate::node
|
590
980
|
#
|
591
|
-
# @param [Array #[]] backtrace
|
592
|
-
#
|
981
|
+
# @param [Array<String>, #[]] backtrace
|
982
|
+
#
|
983
|
+
# @return [Class, Module]
|
984
|
+
#
|
985
|
+
# @api internal
|
986
|
+
# @see SingletonMethods::node
|
593
987
|
# @author manveru
|
594
988
|
def node_from_backtrace(backtrace)
|
595
|
-
|
596
|
-
|
597
|
-
File.readlines(
|
989
|
+
filename, lineno = backtrace[0].split(':', 2)
|
990
|
+
regexp = /^\s*class\s+(\S+)/
|
991
|
+
File.readlines(filename)[0..lineno.to_i].reverse.find{|l| l =~ regexp }
|
598
992
|
const_get($1)
|
599
993
|
end
|
600
994
|
end
|