tanuki 0.1.3
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/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
|