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