tanuki 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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