tanuki 0.1.3
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +19 -0
- data/README.rdoc +19 -0
- data/app/tanuki/controller/controller.rb +3 -0
- data/app/tanuki/controller/default.thtml +5 -0
- data/app/tanuki/controller/index.thtml +1 -0
- data/app/tanuki/controller/link.thtml +7 -0
- data/app/tanuki/object/object.rb +3 -0
- data/app/tanuki/page/missing/default.thtml +2 -0
- data/app/tanuki/page/missing/missing.rb +9 -0
- data/app/user/page/index/default.thtml +121 -0
- data/app/user/page/index/index.rb +2 -0
- data/bin/tanuki +55 -0
- data/lib/tanuki/application.rb +111 -0
- data/lib/tanuki/argument/base.rb +27 -0
- data/lib/tanuki/argument/integer.rb +15 -0
- data/lib/tanuki/argument/integer_range.rb +22 -0
- data/lib/tanuki/argument/string.rb +15 -0
- data/lib/tanuki/argument.rb +41 -0
- data/lib/tanuki/configurator.rb +93 -0
- data/lib/tanuki/context.rb +36 -0
- data/lib/tanuki/controller_behavior.rb +324 -0
- data/lib/tanuki/i18n.rb +33 -0
- data/lib/tanuki/launcher.rb +20 -0
- data/lib/tanuki/loader.rb +131 -0
- data/lib/tanuki/module_extensions.rb +27 -0
- data/lib/tanuki/object_behavior.rb +32 -0
- data/lib/tanuki/template_compiler.rb +145 -0
- data/lib/tanuki/version.rb +6 -0
- data/lib/tanuki.rb +22 -0
- data/schema/tanuki/l10n/en/controller.yml +13 -0
- data/schema/tanuki/l10n/en/page.yml +31 -0
- data/schema/tanuki/l10n/ru/controller.yml +13 -0
- data/schema/tanuki/l10n/ru/page.yml +31 -0
- data/schema/tanuki/models/controller.yml +18 -0
- data/schema/tanuki/models/page.yml +48 -0
- metadata +168 -0
@@ -0,0 +1,324 @@
|
|
1
|
+
module Tanuki
|
2
|
+
|
3
|
+
# Tanuki::ControllerBehavior contains basic methods for a framework controller.
|
4
|
+
# In is included in the base controller class.
|
5
|
+
module ControllerBehavior
|
6
|
+
|
7
|
+
include Enumerable
|
8
|
+
|
9
|
+
internal_attr_reader :model, :logical_parent, :link
|
10
|
+
internal_attr_accessor :logical_child, :visual_child
|
11
|
+
|
12
|
+
# Create new controller with context ctx, logical_parent controller, route_part definitions and a model.
|
13
|
+
def initialize(ctx, logical_parent, route_part, model=nil)
|
14
|
+
@_configured = false
|
15
|
+
@_ctx = ctx
|
16
|
+
@_model = model
|
17
|
+
@_args = {}
|
18
|
+
if @_logical_parent = logical_parent
|
19
|
+
@_route = route_part[:route]
|
20
|
+
self.class.arg_defs.each_pair do |arg_name, arg_def|
|
21
|
+
route_part[:args][arg_def[:index]] = @_args[arg_name] = arg_def[:arg].to_value(route_part[:args][arg_def[:index]])
|
22
|
+
end
|
23
|
+
@_link = self.class.grow_link(@_logical_parent, {:route => @_route, :args => @_args}, self.class.arg_defs)
|
24
|
+
initialize_route(*route_part[:args])
|
25
|
+
else
|
26
|
+
@_link = '/'
|
27
|
+
@_route = nil
|
28
|
+
initialize_route
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Invoked with route args when current route is initialized.
|
33
|
+
def initialize_route(*args)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Returns controller context. Used internally by templates.
|
37
|
+
def _ctx(ctx)
|
38
|
+
@_ctx
|
39
|
+
end
|
40
|
+
|
41
|
+
# Initializes and retrieves child route object. Searches static, dynamic, and ghost routes (in that order).
|
42
|
+
def [](route, *args)
|
43
|
+
byname = (args.length == 1 and args[0].is_a? Hash)
|
44
|
+
ensure_configured!
|
45
|
+
key = [route, args.dup]
|
46
|
+
if cached = @_cache[key]
|
47
|
+
# Return form cache
|
48
|
+
return cached
|
49
|
+
elsif child_def = @_child_defs[route]
|
50
|
+
# Search static routes
|
51
|
+
klass = child_def[:class]
|
52
|
+
args = klass.extract_args(args[0]) if byname
|
53
|
+
child = klass.new(process_child_context(@_ctx, route), self, {:route => route, :args => args}, child_def[:model])
|
54
|
+
else
|
55
|
+
# Search dynamic routes
|
56
|
+
found = false
|
57
|
+
s = route.to_s
|
58
|
+
@_child_collection_defs.each do |collection_def|
|
59
|
+
if md = collection_def[:parse].match(s)
|
60
|
+
a_route = md['route'].to_sym
|
61
|
+
child_def = collection_def[:fetcher].fetch(a_route, collection_def[:format])
|
62
|
+
if child_def
|
63
|
+
klass = child_def[:class]
|
64
|
+
args = klass.extract_args(args[0]) if byname
|
65
|
+
embedded_args = klass.extract_args(md)
|
66
|
+
args.each_index {|i| embedded_args[i] = args[i] if args[i] }
|
67
|
+
child = klass.new(process_child_context(@_ctx, a_route), self,
|
68
|
+
{:route => a_route, :args => embedded_args}, child_def[:model])
|
69
|
+
found = true
|
70
|
+
break child
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
# If still not found, search ghost routes
|
75
|
+
child = missing_route(route, *args) unless found
|
76
|
+
end
|
77
|
+
@_cache[key] = child # Thread safe (possible overwrite, but within consistent state)
|
78
|
+
end
|
79
|
+
|
80
|
+
# Return true, if controller is active.
|
81
|
+
def active?
|
82
|
+
@_active
|
83
|
+
end
|
84
|
+
|
85
|
+
# Retrieves child route class. Searches static, dynamic, and ghost routes (in that order).
|
86
|
+
def child_class(route)
|
87
|
+
ensure_configured!
|
88
|
+
args = []
|
89
|
+
key = [route, args]
|
90
|
+
if cached = @_cache[key]
|
91
|
+
# Return from cache
|
92
|
+
return cached.class
|
93
|
+
elsif child_def = @_child_defs[route]
|
94
|
+
# Return from static routes
|
95
|
+
return child_def[:class]
|
96
|
+
else
|
97
|
+
# Search dynamic routes
|
98
|
+
s = route.to_s
|
99
|
+
@_child_collection_defs.each do |collection_def|
|
100
|
+
if md = collection_def[:parse].match(s)
|
101
|
+
a_route = md['route'].to_sym
|
102
|
+
child_def = collection_def[:fetcher].fetch(a_route, collection_def[:format])
|
103
|
+
return child_def[:class] if child_def
|
104
|
+
end
|
105
|
+
end
|
106
|
+
# If still not found, search ghost routes
|
107
|
+
return (@_cache[key] = missing_route(route, *args)).class
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# Invoked when controller need to be configured.
|
112
|
+
def configure
|
113
|
+
end
|
114
|
+
|
115
|
+
# Return true, if controller is current.
|
116
|
+
def current?
|
117
|
+
@_current
|
118
|
+
end
|
119
|
+
|
120
|
+
# If set, controller navigates to a given child route by default.
|
121
|
+
def default_route
|
122
|
+
nil
|
123
|
+
end
|
124
|
+
|
125
|
+
def each(&block)
|
126
|
+
return Enumerator.new(self) unless block_given?
|
127
|
+
ensure_configured!
|
128
|
+
@_child_defs.each_pair do |route, child|
|
129
|
+
if route.is_a? Regexp
|
130
|
+
cd = @_child_collection_defs[child]
|
131
|
+
cd[:fetcher].fetch_all(cd[:format]) do |child_def|
|
132
|
+
key = [child_def[:route], []]
|
133
|
+
unless child = @_cache[key]
|
134
|
+
child = child_def[:class].new(process_child_context(@_ctx, route), self,
|
135
|
+
{:route => child_def[:route], :args => {}}, child_def[:model])
|
136
|
+
@_cache[key] = child
|
137
|
+
end
|
138
|
+
block.call child
|
139
|
+
end
|
140
|
+
else
|
141
|
+
yield self[route] unless child[:hidden]
|
142
|
+
end
|
143
|
+
end
|
144
|
+
self
|
145
|
+
end
|
146
|
+
|
147
|
+
# Invoked when controller configuration needs to be ensured.
|
148
|
+
def ensure_configured!
|
149
|
+
unless @_configured
|
150
|
+
@_child_defs = {}
|
151
|
+
@_child_collection_defs = []
|
152
|
+
@_cache={}
|
153
|
+
@_length = 0
|
154
|
+
configure
|
155
|
+
@_configured = true
|
156
|
+
end
|
157
|
+
nil
|
158
|
+
end
|
159
|
+
|
160
|
+
# Returns the link to the current controller, switching the active controller on the respective path level to self.
|
161
|
+
def forward_link
|
162
|
+
uri_parts = @_ctx.env['REQUEST_PATH'].split(/(?<!\$)\//)
|
163
|
+
link_parts = link.split(/(?<!\$)\//)
|
164
|
+
link_parts.each_index {|i| uri_parts[i] = link_parts[i] }
|
165
|
+
uri_parts.join('/') << ((qs = @_ctx.env['QUERY_STRING']).empty? ? '' : "?#{qs}")
|
166
|
+
end
|
167
|
+
|
168
|
+
# Returns the number of children.
|
169
|
+
def length
|
170
|
+
if @_child_collection_defs.length > 0
|
171
|
+
if @_length_is_valid
|
172
|
+
@_length
|
173
|
+
else
|
174
|
+
@_child_collection_defs.each {|cd| @_length += cd[:fetcher].length }
|
175
|
+
@_length_is_valid = true
|
176
|
+
end
|
177
|
+
else
|
178
|
+
@_length
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
# Process context passed to child
|
183
|
+
def process_child_context(ctx, route)
|
184
|
+
ctx
|
185
|
+
end
|
186
|
+
|
187
|
+
alias_method :size, :length
|
188
|
+
|
189
|
+
# Returns controller string representation. Defaults to route name.
|
190
|
+
def to_s
|
191
|
+
@_route.to_s
|
192
|
+
end
|
193
|
+
|
194
|
+
# Invoked when visual parent needs to be determined. Defaults to logical parent.
|
195
|
+
def visual_parent
|
196
|
+
@_logical_parent
|
197
|
+
end
|
198
|
+
|
199
|
+
private
|
200
|
+
|
201
|
+
# Defines a child of class klass on route with model, optionally hidden.
|
202
|
+
def has_child(klass, route, model=nil, hidden=false)
|
203
|
+
@_child_defs[route] = {:class => klass, :model => model, :hidden => hidden}
|
204
|
+
@_length += 1 unless hidden
|
205
|
+
self
|
206
|
+
end
|
207
|
+
|
208
|
+
# Defines a child collection of type parse_regexp.
|
209
|
+
def has_child_collection(parse_regexp, format_string, child_def_fetcher)
|
210
|
+
@_child_defs[parse_regexp] = @_child_collection_defs.size
|
211
|
+
@_child_collection_defs << {:parse => parse_regexp, :format => format_string, :fetcher => child_def_fetcher}
|
212
|
+
@_length_is_valid = false
|
213
|
+
end
|
214
|
+
|
215
|
+
# Invoked for route with args when a route is missing.
|
216
|
+
def missing_route(route, *args)
|
217
|
+
@_ctx.missing_page.new(@_ctx, self, {:route => route, :args => []})
|
218
|
+
end
|
219
|
+
|
220
|
+
# Tanuki::ControllerBehavior mixed-in class methods.
|
221
|
+
module ClassMethods
|
222
|
+
|
223
|
+
# Returns own or superclass argument definitions.
|
224
|
+
def arg_defs
|
225
|
+
@_arg_defs ||= superclass.arg_defs.dup
|
226
|
+
end
|
227
|
+
|
228
|
+
# Escapes a given string for use in links.
|
229
|
+
def escape(s, chrs)
|
230
|
+
s ? Rack::Utils.escape(s.to_s.gsub(/[\$#{chrs}]/, '$\0')) : nil
|
231
|
+
end
|
232
|
+
|
233
|
+
# Extracts arguments, initializing default values beforehand. Searches md hash for default value overrides.
|
234
|
+
def extract_args(md)
|
235
|
+
res = []
|
236
|
+
arg_defs.each_pair do |name, arg|
|
237
|
+
res[arg[:index]] = md[name]
|
238
|
+
end
|
239
|
+
res
|
240
|
+
end
|
241
|
+
|
242
|
+
# Builds link from root to self.
|
243
|
+
def grow_link(ctrl, route_part, arg_defs)
|
244
|
+
own_link = escape(route_part[:route], '\/:') << route_part[:args].map do |k, v|
|
245
|
+
arg_defs[k][:arg].default == v ? '' : ":#{escape(k, '\/:-')}-#{escape(v, '\/:')}"
|
246
|
+
end.join
|
247
|
+
"#{ctrl.link == '/' ? '' : ctrl.link}/#{own_link}"
|
248
|
+
end
|
249
|
+
|
250
|
+
# Defines an argument with name, derived from object obj with additional args.
|
251
|
+
def has_arg(name, obj, *args)
|
252
|
+
# TODO Ensure thread safety
|
253
|
+
arg_defs[name] = {:arg => Argument.to_argument(obj, *args), :index => @_arg_defs.size}
|
254
|
+
end
|
255
|
+
|
256
|
+
# Prepares the extended module.
|
257
|
+
def self.extended(mod)
|
258
|
+
mod.instance_variable_set(:@_arg_defs, {})
|
259
|
+
end
|
260
|
+
|
261
|
+
end # end ClassMethods
|
262
|
+
|
263
|
+
extend ClassMethods
|
264
|
+
|
265
|
+
class << self
|
266
|
+
|
267
|
+
# Dispathes route chain in context ctx on request_path, starting with controller klass.
|
268
|
+
def dispatch(ctx, klass, request_path)
|
269
|
+
parts = parse_path(request_path)
|
270
|
+
curr = root_ctrl = klass.new(ctx, nil, nil, true)
|
271
|
+
parts.each do |part|
|
272
|
+
curr.instance_variable_set :@_active, true
|
273
|
+
nxt = curr[part[:route], *part[:args]]
|
274
|
+
curr.logical_child = nxt
|
275
|
+
curr = nxt
|
276
|
+
end
|
277
|
+
curr.instance_variable_set :@_active, true
|
278
|
+
curr.instance_variable_set :@_current, true
|
279
|
+
if route = curr.default_route
|
280
|
+
klass = curr.child_class(route)
|
281
|
+
{:type => :redirect, :location => grow_link(curr, route, klass.arg_defs)}
|
282
|
+
else
|
283
|
+
type = (curr.is_a? ctx.missing_page) ? :missing_page : :page
|
284
|
+
prev = curr
|
285
|
+
while curr = prev.visual_parent
|
286
|
+
curr.visual_child = prev
|
287
|
+
prev = curr
|
288
|
+
end
|
289
|
+
{:type => type, :controller => prev}
|
290
|
+
end
|
291
|
+
end
|
292
|
+
|
293
|
+
# Extends the including module with Tanuki::ControllerBehavior::ClassMethods.
|
294
|
+
def included(mod)
|
295
|
+
mod.extend ClassMethods
|
296
|
+
end
|
297
|
+
|
298
|
+
private
|
299
|
+
|
300
|
+
# Parses path to return route name and arguments.
|
301
|
+
def parse_path(path)
|
302
|
+
path[1..-1].split(/(?<!\$)\//).map do |s|
|
303
|
+
arr = s.gsub('$/', '/').split(/(?<!\$):/)
|
304
|
+
route_part = {:route => unescape(arr[0]).to_sym}
|
305
|
+
args = {}
|
306
|
+
arr[1..-1].each do |argval|
|
307
|
+
varr = argval.split(/(?<!\$)-/)
|
308
|
+
args[unescape(varr[0])] = unescape(varr[1..-1].join) # TODO Predict argument
|
309
|
+
end
|
310
|
+
route_part[:args] = extract_args(args)
|
311
|
+
route_part
|
312
|
+
end
|
313
|
+
end
|
314
|
+
|
315
|
+
# Unescape a given link part for internal use.
|
316
|
+
def unescape(s)
|
317
|
+
s ? s.gsub(/\$([\/\$:-])/, '\1') : nil
|
318
|
+
end
|
319
|
+
|
320
|
+
end # end class << self
|
321
|
+
|
322
|
+
end # end ControllerBehavior
|
323
|
+
|
324
|
+
end # end Tanuki
|
data/lib/tanuki/i18n.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
module Tanuki
|
2
|
+
|
3
|
+
# Tanuki::I18n is a drop-in controller for localizable applications.
|
4
|
+
class I18n
|
5
|
+
|
6
|
+
include ControllerBehavior
|
7
|
+
|
8
|
+
# Adds language routes of root controller class when invoked.
|
9
|
+
def configure
|
10
|
+
root_page = @_ctx.root_page
|
11
|
+
@_ctx.languages.each {|lng| has_child root_page, lng }
|
12
|
+
end
|
13
|
+
|
14
|
+
# Returns default route according to default language.
|
15
|
+
def default_route
|
16
|
+
{:route => @_ctx.language.to_s, :args => {}}
|
17
|
+
end
|
18
|
+
|
19
|
+
# Calls default view of visual child.
|
20
|
+
def default_view
|
21
|
+
@_visual_child.default_view
|
22
|
+
end
|
23
|
+
|
24
|
+
# Adds child language to its context.
|
25
|
+
def process_child_context(ctx, route)
|
26
|
+
ctx = ctx.child
|
27
|
+
ctx.language = route.to_sym
|
28
|
+
ctx
|
29
|
+
end
|
30
|
+
|
31
|
+
end # end I18n
|
32
|
+
|
33
|
+
end # end Tanuki
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Tanuki
|
2
|
+
|
3
|
+
# Tanuki::Launcher is called on every request.
|
4
|
+
# It is used to build output starting with the default view of the root controller.
|
5
|
+
class Launcher
|
6
|
+
|
7
|
+
# Creates a new Tanuki::Launcher with root controller ctrl in context ctx.
|
8
|
+
def initialize(ctrl, ctx)
|
9
|
+
@ctrl = ctrl
|
10
|
+
@ctx = ctx
|
11
|
+
end
|
12
|
+
|
13
|
+
# Passes a given block to the requested page template tree.
|
14
|
+
def each(&block)
|
15
|
+
@ctrl.default_view.call(proc {|out| block.call(out.to_s) }, @ctx)
|
16
|
+
end
|
17
|
+
|
18
|
+
end # end Launcher
|
19
|
+
|
20
|
+
end # end Tanuki
|
@@ -0,0 +1,131 @@
|
|
1
|
+
module Tanuki
|
2
|
+
|
3
|
+
# Tanuki::Loader deals with framework paths resolving, and object and template loading.
|
4
|
+
class Loader
|
5
|
+
|
6
|
+
class << self
|
7
|
+
|
8
|
+
# Returns the path to a source file containing class klass.
|
9
|
+
def class_path(klass)
|
10
|
+
path = const_to_path(klass, @context.app_root, File::SEPARATOR)
|
11
|
+
File.join(path, path.match("#{File::SEPARATOR}([^#{File::SEPARATOR}]*)$")[1] << '.rb')
|
12
|
+
end
|
13
|
+
|
14
|
+
def context=(ctx)
|
15
|
+
@context = ctx
|
16
|
+
end
|
17
|
+
|
18
|
+
# Checks if templates contain a compiled template sym for class klass
|
19
|
+
def has_template?(templates, klass, sym)
|
20
|
+
templates.include? "#{klass}##{sym}"
|
21
|
+
end
|
22
|
+
|
23
|
+
# Runs template sym from obj.
|
24
|
+
# Template is recompiled from source on two conditions:
|
25
|
+
# * template source modification time is older than compiled template modification time,
|
26
|
+
# * Tanuki::TemplateCompiler source modification time is older than compiled template modification time.
|
27
|
+
def run_template(templates, obj, sym, *args, &block)
|
28
|
+
st_path = source_template_path(obj.class, sym)
|
29
|
+
if st_path
|
30
|
+
owner = template_owner(obj.class, sym)
|
31
|
+
ct_path = compiled_template_path(obj.class, sym)
|
32
|
+
ct_file_exists = File.file?(ct_path)
|
33
|
+
ct_file_mtime = ct_file_exists ? File.mtime(ct_path) : nil
|
34
|
+
st_file = File.new(st_path, 'r:UTF-8')
|
35
|
+
if !ct_file_exists || st_file.mtime > ct_file_mtime || File.mtime(COMPILER_PATH) > ct_file_mtime
|
36
|
+
no_refresh = compile_template(st_file, ct_path, ct_file_mtime, owner, sym)
|
37
|
+
else
|
38
|
+
no_refresh = true
|
39
|
+
end
|
40
|
+
method_name = "#{sym}_view".to_sym
|
41
|
+
owner.instance_eval do
|
42
|
+
unless (method_exists = instance_methods(false).include? method_name) && no_refresh
|
43
|
+
remove_method method_name if method_exists
|
44
|
+
load ct_path
|
45
|
+
end
|
46
|
+
end
|
47
|
+
templates["#{owner}##{sym}"] = nil
|
48
|
+
templates["#{obj.class}##{sym}"] = nil
|
49
|
+
obj.send(method_name, *args, &block)
|
50
|
+
else
|
51
|
+
raise "undefined template `#{sym}' for #{obj.class}"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
# Path to Tanuki::TemplateCompiler for internal use.
|
58
|
+
COMPILER_PATH = File.join(File.expand_path('..', __FILE__), 'template_compiler.rb')
|
59
|
+
|
60
|
+
# Extension glob for template files.
|
61
|
+
TEMPLATE_EXT = '.t{html,txt}'
|
62
|
+
|
63
|
+
# Compiles template sym from owner class using source in st_file to ct_path.
|
64
|
+
# Compilation is only done if destination file modification time has not changed
|
65
|
+
# (is equal to ct_file_mtime) since file locking was initiated.
|
66
|
+
def compile_template(st_file, ct_path, ct_file_mtime, owner, sym)
|
67
|
+
no_refresh = true
|
68
|
+
st_file.flock(File::LOCK_EX)
|
69
|
+
if !File.file?(ct_path) || File.mtime(ct_path) == ct_file_mtime
|
70
|
+
no_refresh = false
|
71
|
+
ct_dir = File.dirname(ct_path)
|
72
|
+
FileUtils.mkdir_p(ct_dir) unless File.directory?(ct_dir)
|
73
|
+
File.open(tmp_ct_path = ct_path + '~', 'w:UTF-8') do |ct_file|
|
74
|
+
TemplateCompiler.compile_template(ct_file, st_file.read, owner, sym)
|
75
|
+
end
|
76
|
+
FileUtils.mv(tmp_ct_path, ct_path)
|
77
|
+
end
|
78
|
+
st_file.flock(File::LOCK_UN)
|
79
|
+
no_refresh
|
80
|
+
end
|
81
|
+
|
82
|
+
# Returns the path to a compiled template file containing template method_name for class klass.
|
83
|
+
def compiled_template_path(klass, method_name)
|
84
|
+
File.join(const_to_path(klass, @context.cache_root, '.'), method_name.to_s << '.rb')
|
85
|
+
end
|
86
|
+
|
87
|
+
# Transforms a given constant klass to path with a given root and separated by sep.
|
88
|
+
def const_to_path(klass, root, sep)
|
89
|
+
File.join(root, klass.to_s.split('_').map {|item| item.gsub(/(?!^)([A-Z])/, '_\1') }.join(sep).downcase)
|
90
|
+
end
|
91
|
+
|
92
|
+
# Returns the path to a source file containing template method_name for class klass.
|
93
|
+
def source_template_path(klass, method_name)
|
94
|
+
template_path(klass, method_name, @context.app_root, File::SEPARATOR, TEMPLATE_EXT)
|
95
|
+
end
|
96
|
+
|
97
|
+
# Finds the direct template method_name owner among ancestors of class klass.
|
98
|
+
def template_owner(klass, method_name)
|
99
|
+
method_file = method_name.to_s << TEMPLATE_EXT
|
100
|
+
klass.ancestors.each do |ancestor|
|
101
|
+
unless Dir.glob(File.join(const_to_path(ancestor, @context.app_root, File::SEPARATOR), method_file)).empty?
|
102
|
+
return ancestor
|
103
|
+
end
|
104
|
+
end
|
105
|
+
nil
|
106
|
+
end
|
107
|
+
|
108
|
+
# Returns the path to a file containing template method_name for class klass.
|
109
|
+
# This is done with a given root, extension ext, and separated by sep.
|
110
|
+
def template_path(klass, method_name, root, sep, ext)
|
111
|
+
if owner = template_owner(klass, method_name)
|
112
|
+
return Dir.glob(File.join(const_to_path(owner, root, sep), method_name.to_s << ext))[0]
|
113
|
+
end
|
114
|
+
nil
|
115
|
+
end
|
116
|
+
|
117
|
+
end # end class << self
|
118
|
+
|
119
|
+
end # end Path
|
120
|
+
|
121
|
+
end # end Tanuki
|
122
|
+
|
123
|
+
|
124
|
+
# Runs Tanuki::Loader for every missing constant in main namespace.
|
125
|
+
def Object.const_missing(sym)
|
126
|
+
if File.file?(path = Tanuki::Loader.class_path(sym))
|
127
|
+
require path
|
128
|
+
return const_get(sym)
|
129
|
+
end
|
130
|
+
super
|
131
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
class Module
|
2
|
+
|
3
|
+
# Creates a reader +sym+ and a writer +sym=+ for the instance variable +@_sym+.
|
4
|
+
def internal_attr_accessor(*syms)
|
5
|
+
internal_attr_reader(*syms)
|
6
|
+
internal_attr_writer(*syms)
|
7
|
+
end
|
8
|
+
|
9
|
+
# Creates a reader +sym+ for the instance variable +@_sym+.
|
10
|
+
def internal_attr_reader(*syms)
|
11
|
+
syms.each do |sym|
|
12
|
+
ivar = "@_#{sym}".to_sym
|
13
|
+
instance_variable_set(ivar, nil) unless instance_variable_defined? ivar
|
14
|
+
class_eval "def #{sym};#{ivar};end"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# Creates a writer +sym=+ for the instance variable +@_sym+.
|
19
|
+
def internal_attr_writer(*syms)
|
20
|
+
syms.each do |sym|
|
21
|
+
ivar = "@_#{sym}".to_sym
|
22
|
+
instance_variable_set(ivar, nil) unless instance_variable_defined? ivar
|
23
|
+
class_eval "def #{sym}=(obj);#{ivar}=obj;end"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
end # end Module
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Tanuki
|
2
|
+
|
3
|
+
# Tanuki::ControllerBehavior contains basic methods for a framework object.
|
4
|
+
# In is included in the base framework object class.
|
5
|
+
module ObjectBehavior
|
6
|
+
|
7
|
+
# Shortcut to Tanuki::Loader.has_template?. Used internally by templates.
|
8
|
+
def _has_tpl(ctx, klass, sym)
|
9
|
+
Tanuki::Loader.has_template?(ctx.templates, klass, sym)
|
10
|
+
end
|
11
|
+
|
12
|
+
# Shortcut to Tanuki::Loader.run_template. Used internally by templates.
|
13
|
+
def _run_tpl(ctx, obj, sym, *args, &block)
|
14
|
+
Tanuki::Loader.run_template(ctx.templates, obj, sym, *args, &block)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Returns the same context as given. Used internally by templates.
|
18
|
+
def _ctx(ctx)
|
19
|
+
ctx
|
20
|
+
end
|
21
|
+
|
22
|
+
# Kernel#method_missing hook for fetching views.
|
23
|
+
def method_missing(sym, *args, &block)
|
24
|
+
if matches = sym.to_s.match(/^(.*)_view$/)
|
25
|
+
return Tanuki::Loader.run_template({}, self, matches[1].to_sym, *args, &block)
|
26
|
+
end
|
27
|
+
super
|
28
|
+
end
|
29
|
+
|
30
|
+
end # end ObjectBehavior
|
31
|
+
|
32
|
+
end # end Tanuki
|