tanuki 0.3.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. data/README.rdoc +5 -4
  2. data/app/tanuki/controller/{link.thtml → controller.link.thtml} +0 -0
  3. data/app/tanuki/controller/controller.page.thtml +14 -0
  4. data/app/tanuki/controller/controller.rb +1 -2
  5. data/app/tanuki/controller/controller.title.ttxt +1 -0
  6. data/app/tanuki/controller/controller.view.thtml +3 -0
  7. data/app/tanuki/fetcher/sequel/sequel.rb +34 -0
  8. data/app/tanuki/manager/controller/controller.rb +1 -1
  9. data/app/tanuki/manager/page/page.rb +1 -1
  10. data/app/tanuki/meta_model/{manager.ttxt → meta_model.manager.ttxt} +0 -0
  11. data/app/tanuki/meta_model/{manager_base.ttxt → meta_model.manager_base.ttxt} +0 -0
  12. data/app/tanuki/meta_model/{model.ttxt → meta_model.model.ttxt} +0 -0
  13. data/app/tanuki/meta_model/{model_base.ttxt → meta_model.model_base.ttxt} +0 -0
  14. data/app/tanuki/meta_model/meta_model.rb +1 -2
  15. data/app/tanuki/model/controller/controller.rb +1 -1
  16. data/app/tanuki/model/page/page.rb +1 -1
  17. data/app/tanuki/page/missing/{default.thtml → missing.page.thtml} +1 -1
  18. data/app/tanuki/page/missing/missing.rb +3 -2
  19. data/app/user/page/home/home.rb +2 -0
  20. data/app/user/page/home/home.title.thtml +1 -0
  21. data/app/user/page/home/home.view.css +88 -0
  22. data/app/user/page/home/home.view.thtml +22 -0
  23. data/bin/tanuki +2 -1
  24. data/config/common.rb +1 -0
  25. data/config/common_application.rb +3 -6
  26. data/config/development_application.rb +0 -3
  27. data/lib/tanuki.rb +8 -7
  28. data/lib/tanuki/application.rb +108 -81
  29. data/lib/tanuki/argument.rb +10 -5
  30. data/lib/tanuki/argument/integer_range.rb +4 -2
  31. data/lib/tanuki/{behavior/object_behavior.rb → base_behavior.rb} +21 -4
  32. data/lib/tanuki/configurator.rb +20 -8
  33. data/lib/tanuki/const.rb +32 -0
  34. data/lib/tanuki/context.rb +18 -7
  35. data/lib/tanuki/controller.rb +517 -0
  36. data/lib/tanuki/css_compressor.rb +50 -0
  37. data/lib/tanuki/extensions/module.rb +21 -5
  38. data/lib/tanuki/extensions/rack/frozen_route.rb +35 -0
  39. data/lib/tanuki/extensions/rack/static_dir.rb +1 -1
  40. data/lib/tanuki/extensions/sequel/model.rb +7 -0
  41. data/lib/tanuki/i18n.rb +8 -6
  42. data/lib/tanuki/loader.rb +166 -33
  43. data/lib/tanuki/meta_model.rb +176 -0
  44. data/lib/tanuki/{behavior/model_behavior.rb → model_behavior.rb} +7 -3
  45. data/lib/tanuki/model_generator.rb +49 -29
  46. data/lib/tanuki/template_compiler.rb +72 -41
  47. data/lib/tanuki/utility.rb +11 -4
  48. data/lib/tanuki/utility/create.rb +52 -11
  49. data/lib/tanuki/utility/generate.rb +16 -10
  50. data/lib/tanuki/utility/version.rb +1 -1
  51. data/lib/tanuki/version.rb +7 -2
  52. metadata +50 -66
  53. data/app/tanuki/controller/default.thtml +0 -5
  54. data/app/tanuki/controller/index.thtml +0 -1
  55. data/app/user/page/index/default.thtml +0 -121
  56. data/app/user/page/index/index.rb +0 -2
  57. data/config/test_application.rb +0 -2
  58. data/lib/tanuki/behavior/controller_behavior.rb +0 -366
  59. data/lib/tanuki/behavior/meta_model_behavior.rb +0 -160
  60. data/lib/tanuki/extensions/rack/builder.rb +0 -26
  61. data/lib/tanuki/extensions/rack/server.rb +0 -18
  62. data/lib/tanuki/launcher.rb +0 -21
  63. data/lib/tanuki/utility/server.rb +0 -23
@@ -5,7 +5,8 @@ require 'tanuki/argument/string'
5
5
 
6
6
  module Tanuki
7
7
 
8
- # Tanuki::Argument contains basic classes and methods for controller arguments.
8
+ # Tanuki::Argument contains basic classes and methods for controller
9
+ # arguments.
9
10
  module Argument
10
11
 
11
12
  @assoc = {}
@@ -14,18 +15,22 @@ module Tanuki
14
15
 
15
16
  # Removes argument association for a given type class +klass+.
16
17
  def delete(klass)
17
- @assoc.delete(klass)
18
+ @assoc.delete klass
18
19
  end
19
20
 
20
- # Associates a given type class +klass+ with an argument class +arg_class+.
21
+ # Associates a given type class +klass+ with an argument class
22
+ # +arg_class+.
21
23
  def store(klass, arg_class)
22
- warn "Tanuki::Argument::Base is not an ancestor of `#{arg_class}'" unless arg_class.ancestors.include? Argument::Base
24
+ unless arg_class.ancestors.include? Argument::Base
25
+ warn "Tanuki::Argument::Base is not an ancestor of `#{arg_class}'"
26
+ end
23
27
  @assoc[klass] = arg_class
24
28
  end
25
29
 
26
30
  alias_method :[], :store
27
31
 
28
- # Converts a given type object +obj+ to an argument object with optional +args+.
32
+ # Converts a given type object +obj+ to an argument object with
33
+ # optional +args+.
29
34
  def to_argument(obj, *args)
30
35
  if @assoc.include?(klass = obj.class)
31
36
  @assoc[klass].new(obj, *args)
@@ -1,10 +1,12 @@
1
1
  module Tanuki
2
2
  module Argument
3
3
 
4
- # Tanuki::Argument::IntegerRange is a class for +Integer+ arguments with a certain value range.
4
+ # Tanuki::Argument::IntegerRange is a class for +Integer+ arguments with
5
+ # a certain value range.
5
6
  class IntegerRange < Integer
6
7
 
7
- # Initializes the argument with a +default+ value and allowed value +range+.
8
+ # Initializes the argument with a +default+ value and allowed value
9
+ # +range+.
8
10
  def initialize(range, default=nil)
9
11
  super(default ? default : range.first)
10
12
  @range = range
@@ -15,14 +15,31 @@ module Tanuki
15
15
  end
16
16
 
17
17
  # Returns the same context as given. Used internally by templates.
18
- def _ctx(ctx)
18
+ def _ctx(ctx, template_signature)
19
+ if !ctx.resources.include? template_signature
20
+ Loader.load_template_files(ctx, template_signature)
21
+ end
19
22
  ctx
20
23
  end
21
24
 
22
- # Allows to return template blocks. E. g. returns +foobar+ template block when +foobar_view+ method is called.
25
+ # Allows to return template blocks. E. g. returns +foobar+ template block
26
+ # when +foobar_view+ method is called.
23
27
  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)
28
+ if matches = sym.to_s.match(/^.*(?=_view$)|view$/)
29
+ return Tanuki::Loader.run_template(
30
+ {},
31
+ self,
32
+ matches[0].to_sym,
33
+ *args,
34
+ &block
35
+ )
36
+ end
37
+ super
38
+ end
39
+
40
+ def method(sym)
41
+ if !respond_to?(sym) && (m = sym.to_s.match(/^.*(?=_view$)|view$/))
42
+ Tanuki::Loader.load_template({}, self, m[0].to_sym)
26
43
  end
27
44
  super
28
45
  end
@@ -1,19 +1,31 @@
1
1
  module Tanuki
2
2
 
3
- # Tanuki::Configurator is a scope for evaluating a Tanuki application configuration block.
3
+ # Tanuki::Configurator is a scope for evaluating
4
+ # a Tanuki application configuration block.
4
5
  class Configurator
5
6
 
6
7
  # Configuration root.
7
8
  attr_writer :config_root
8
9
 
10
+ # Configurator context.
11
+ attr_reader :context
12
+
9
13
  # Creates a new configurator in context +ctx+ and +root+ directory.
10
- # Configuration root +config_root+ defaults to _config_ directory in +root+.
14
+ # Configuration root +config_root+ defaults
15
+ # to _config_ directory in +root+.
11
16
  def initialize(ctx, root, config_root=nil)
12
17
  @context = ctx
13
18
  set :root, root ? root : Dir.pwd
14
19
  end
15
20
 
16
- # Loads and executes a given configuraion file with symbolic name +config+.
21
+ # Returns true, if a configuration file
22
+ # with a symbolic name +config+ exists.
23
+ def config_file?(config)
24
+ File.file?(File.join(@config_root, config.to_s) << '.rb')
25
+ end
26
+
27
+ # Loads and executes a given configuraion file
28
+ # with symbolic name +config+.
17
29
  # If +silent+ is +true+, exception is not raised on missing file.
18
30
  def load_config(config, silent=false)
19
31
  file = File.join(@config_root, config.to_s) << '.rb'
@@ -26,27 +38,27 @@ module Tanuki
26
38
 
27
39
  # Invokes Tanuki::Argument::store.
28
40
  def argument(klass, arg_class)
29
- Argument.store(klass, arg_class)
41
+ Argument.store klass, arg_class
30
42
  end
31
43
 
32
44
  # Sets an +option+ to +value+ in the current context.
33
45
  def set(option, value)
34
- @context.send("#{option}=".to_sym, value)
46
+ @context.send "#{option}=".to_sym, value
35
47
  end
36
48
 
37
49
  # Invokes Tanuki::Application::use.
38
50
  def use(middleware, *args, &block)
39
- Application.use(middleware, *args, &block)
51
+ Application.use middleware, *args, &block
40
52
  end
41
53
 
42
54
  # Invokes Tanuki::Application::discard.
43
55
  def discard(middleware)
44
- Application.discard(middleware)
56
+ Application.discard middleware
45
57
  end
46
58
 
47
59
  # Invokes Tanuki::Application::visitor.
48
60
  def visitor(sym, &block)
49
- Application.visitor(sym, &block)
61
+ Application.visitor sym, &block
50
62
  end
51
63
 
52
64
  end # Configurator
@@ -0,0 +1,32 @@
1
+ module Tanuki
2
+
3
+ # This module includes constants that are commonly used
4
+ # during requests in the framework.
5
+ # All of them are frozen to keep the GC from unwanted stress.
6
+ module Const
7
+
8
+ ARG_KEY_ESCAPE = '\/:-'.freeze
9
+ ARG_VALUE_ESCAPE = '\/:'.freeze
10
+ CONTENT_TYPE = 'Content-Type'.freeze
11
+ EMPTY_ARRAY = [].freeze
12
+ EMPTY_STRING = ''.freeze
13
+ ESCAPED_MATCH = '$\0'.freeze
14
+ ESCAPED_ROUTE_CHARS = /\$([\/\$:-])/.freeze
15
+ ESCAPED_SLASH = '$/'.freeze
16
+ ETAG = 'ETag'.freeze
17
+ FIRST_SUBPATTERN = '\1'.freeze
18
+ LOCATION = 'Location'.freeze
19
+ MIME_TEXT_HTML = 'text/html; charset=utf-8'.freeze
20
+ PATH_INFO = 'PATH_INFO'.freeze
21
+ QUERY_STRING = 'QUERY_STRING'.freeze
22
+ SLASH = '/'.freeze
23
+ TRAILING_SLASH = /^(.+)(?<!\$)\/$/.freeze
24
+ UNESCAPED_COLON = /(?<!\$):/.freeze
25
+ UNESCAPED_MINUS = /(?<!\$)-/.freeze
26
+ UNESCAPED_SLASH = /(?<!\$)\//.freeze
27
+ UTF_8 = 'UTF-8'.freeze
28
+ VIEW_METHOD = /^.*_view$/.freeze
29
+
30
+ end # Const
31
+
32
+ end # Tanuki
@@ -1,7 +1,8 @@
1
1
  module Tanuki
2
2
 
3
3
  # Tanuki::Context is used to create unique environments for each request.
4
- # Child contexts inherit parent context entries and can override them without modifying the parent context.
4
+ # Child contexts inherit parent context entries and can override them
5
+ # without modifying the parent context.
5
6
  # Use Tanuki::Context::child to create new contexts.
6
7
  class Context
7
8
 
@@ -17,13 +18,17 @@ module Tanuki
17
18
  child
18
19
  end
19
20
 
20
- # Returns a printable version of Tanuki::Context, represented as a +Hash+.
21
+ # Returns a printable version of Tanuki::Context,
22
+ # represented as a +Hash+.
21
23
  # Can be used during development for inspection purposes.
22
24
  #--
23
- # When changing this method, remember to update `#{__LINE__ + 12}' to `defined.inspect` line number.
25
+ # When changing this method, remember to update `#{__LINE__ + 13}'
26
+ # to `defined.inspect` line number.
24
27
  # This is required to avoid infinite recursion.
25
28
  def inspect
26
- return to_s if caller.any? {|entry_point| entry_point =~ /\A#{__FILE__}:#{__LINE__ + 12}/}
29
+ return to_s if caller.any? do |entry_point|
30
+ entry_point =~ /\A#{__FILE__}:#{__LINE__ + 13}/
31
+ end
27
32
  defined = {}
28
33
  ancestors.each do |ancestor|
29
34
  ancestor.instance_variable_get(:@_defined).each_key do |key|
@@ -38,11 +43,17 @@ module Tanuki
38
43
  defined.inspect
39
44
  end
40
45
 
41
- # Allowes arbitary values to be assigned to context with a +key=+ method.
46
+ # Allowes arbitary values to be assigned to context
47
+ # with a +key=+ method.
42
48
  # A reader in context object class is created for each assigned value.
43
49
  def method_missing(sym, arg=nil)
44
- match = sym.to_s.match(/\A(?!(?:child|inspect|method_missing)=\Z)([^=]+)(=)?\Z/)
45
- raise "`#{sym}' method cannot be called for Context and its descendants" unless match
50
+ match = sym.to_s.match(%r{
51
+ \A(?!(?:child|inspect|method_missing)=\Z)([^=]+)(=)?\Z
52
+ }x)
53
+ unless match
54
+ raise "`#{sym}' method cannot be called for Context " \
55
+ "and its descendants"
56
+ end
46
57
  defined = @_defined
47
58
  class << self; self; end.instance_eval do
48
59
  method_sym = match[1].to_sym
@@ -0,0 +1,517 @@
1
+ module Tanuki
2
+
3
+ # Tanuki::Controller provides basic methods for
4
+ # a subclassable framework controller.
5
+ class Controller
6
+
7
+ include Tanuki::BaseBehavior
8
+ include Enumerable
9
+
10
+ internal_attr_reader :model, :logical_parent, :link, :ctx
11
+ internal_attr_accessor :logical_child, :visual_child
12
+
13
+ # Creates new controller with context +ctx+, +logical_parent+ controller,
14
+ # +route_part+ definitions and a +model+.
15
+ def initialize(ctx, logical_parent, route_part, model=nil)
16
+ @_configured = false
17
+ @_ctx = ctx
18
+ @_model = model
19
+ @_args = {}
20
+ if @_logical_parent = logical_parent
21
+
22
+ # Register controller arguments, as declared with #has_arg.
23
+ @_route = route_part[:route]
24
+ self.class.arg_defs.each_pair do |arg_name, arg_def|
25
+ arg_val = arg_def[:arg].to_value(route_part[:args][arg_def[:index]])
26
+ route_part[:args][arg_def[:index]] = @_args[arg_name] = arg_val
27
+ end
28
+
29
+ @_link = self.class.grow_link(@_logical_parent, {
30
+ :route => @_route,
31
+ :args => @_args
32
+ }, self.class.arg_defs)
33
+ initialize_route(*route_part[:args])
34
+ else
35
+ @_link = '/'
36
+ @_route = nil
37
+ initialize_route
38
+ end
39
+ end
40
+
41
+ # Invoked with route +args+ when current route is initialized.
42
+ def initialize_route(*args)
43
+ end
44
+
45
+ # Returns controller context. Used internally by templates.
46
+ def _ctx(ctx, template_signature)
47
+ if !ctx.resources.include? template_signature
48
+ Loader.load_template_files(ctx, template_signature)
49
+ end
50
+ @_ctx
51
+ end
52
+
53
+ # Initializes and retrieves child controller on +route+.
54
+ # Searches static, dynamic, and ghost routes (in that order).
55
+ def [](route, *args)
56
+ byname = (args.length == 1 and args[0].is_a? Hash)
57
+ ensure_configured!
58
+ key = [route, args.dup]
59
+ if cached = @_cache[key]
60
+
61
+ # Return form cache
62
+ return cached
63
+
64
+ elsif child_def = @_child_defs[route]
65
+
66
+ # Search actions
67
+ if child_def[:type] == :action &&
68
+ action = child_def[ctx.request.request_method]
69
+ then
70
+ throw :action, action
71
+ else
72
+
73
+ # Search static routes
74
+ klass = child_def[:class]
75
+ args = klass.extract_args(args[0]) if byname
76
+ child = klass.new(process_child_context(@_ctx, route), self, {
77
+ :route => route,
78
+ :args => args
79
+ }, child_def[:model])
80
+ end
81
+
82
+ else
83
+
84
+ # Search dynamic routes
85
+ found = false
86
+ s = route.to_s
87
+ @_child_collection_defs.each do |collection_def|
88
+ collection_def[:parse].match(s) do |route_match|
89
+ child_def = collection_def[:fetcher].fetch(
90
+ route_match,
91
+ collection_def[:format]
92
+ )
93
+ if child_def
94
+ klass = child_def[:class]
95
+ args = klass.extract_args(args[0]) if byname
96
+ embedded_args = klass.extract_args(route_match)
97
+ args.each_index {|i| embedded_args[i] = args[i] if args[i] }
98
+ child_context = process_child_context(@_ctx, child_def[:route])
99
+ child = klass.new(child_context, self, {
100
+ :route => child_def[:route],
101
+ :args => embedded_args
102
+ }, child_def[:model])
103
+ found = true
104
+ break child
105
+ end # if
106
+ end # match
107
+ end # each
108
+
109
+ # If still not found, search ghost routes
110
+ child = missing_route(route, *args) unless found
111
+
112
+ end
113
+ # Thread safe (possible overwrite, but within consistent state)
114
+ @_cache[key] = child
115
+ end
116
+
117
+ # Returns +true+, if controller is active.
118
+ def active?
119
+ @_active
120
+ end
121
+
122
+ # Retrieves child controller class on +route+.
123
+ # Searches static, dynamic, and ghost routes (in that order).
124
+ def child_class(route)
125
+ ensure_configured!
126
+ args = []
127
+ key = [route, args]
128
+ if cached = @_cache[key]
129
+
130
+ # Return from cache
131
+ return cached.class
132
+
133
+ elsif child_def = @_child_defs[route]
134
+
135
+ # Return from static routes
136
+ return child_def[:class]
137
+
138
+ else
139
+
140
+ # Search dynamic routes
141
+ s = route.to_s
142
+ @_child_collection_defs.each do |collection_def|
143
+ collection_def[:parse].match(s) do |route_match|
144
+ child_def = collection_def[:fetcher].fetch(
145
+ route_match,
146
+ collection_def[:format]
147
+ )
148
+ return child_def[:class] if child_def
149
+ end
150
+ end
151
+
152
+ # If still not found, search ghost routes
153
+ return (@_cache[key] = missing_route(route, *args)).class
154
+
155
+ end
156
+ end
157
+
158
+ # Invoked when controller needs to be configured.
159
+ def configure
160
+ end
161
+
162
+ # Returns +true+, if controller is current.
163
+ def current?
164
+ @_current
165
+ end
166
+
167
+ # If set, controller navigates to a given child route by default.
168
+ # Returned object should be either +nil+ (don't navigate),
169
+ # or a +Hash+ with keys:
170
+ # * +:route+ is the +Symbol+ for the route
171
+ # * +:args+ contain route arguments +Hash+
172
+ # * +:redirect+ makes a 302 redirect to this route, if true (optional)
173
+ def default_route
174
+ nil
175
+ end
176
+
177
+ # Calls +block+ once for each visible child controller
178
+ # on static or dynamic routes, passing it as a parameter.
179
+ def each(&block)
180
+ return Enumerator.new(self) unless block_given?
181
+ ensure_configured!
182
+ @_child_defs.each_pair do |route, child|
183
+ if route.is_a? Regexp
184
+ cd = @_child_collection_defs[child]
185
+ cd[:fetcher].fetch_all(cd[:format]) do |child_def|
186
+ key = [child_def[:route], []]
187
+ unless child = @_cache[key]
188
+ child_context = process_child_context(@_ctx, route)
189
+ child = child_def[:class].new(child_context, self, {
190
+ :route => child_def[:route],
191
+ :args => {}
192
+ }, child_def[:model])
193
+ @_cache[key] = child
194
+ end
195
+ block.call child
196
+ end
197
+ else
198
+ yield self[route] unless child[:hidden]
199
+ end
200
+ end
201
+ self
202
+ end
203
+
204
+ # Invoked when controller configuration needs to be ensured.
205
+ def ensure_configured!
206
+ unless @_configured
207
+ @_child_defs = {}
208
+ @_child_collection_defs = []
209
+ @_cache={}
210
+ @_length = 0
211
+ configure
212
+ @_configured = true
213
+ end
214
+ nil
215
+ end
216
+
217
+ # Returns the link to the current controller, switching
218
+ # the active controller on the respective path level to +self+.
219
+ def forward_link
220
+ uri_parts = @_ctx.request.path_info.split(Const::UNESCAPED_SLASH)
221
+ link_parts = link.split(Const::UNESCAPED_SLASH)
222
+ link_parts.each_index {|i| uri_parts[i] = link_parts[i] }
223
+ query_string = @_ctx.request.query_string
224
+ query_string = query_string.empty? ? '' : "?#{query_string}"
225
+ uri_parts.join(Const::SLASH) << query_string
226
+ end
227
+
228
+ # Returns the number of visible child controllers
229
+ # on static and dynamic routes.
230
+ def length
231
+ if @_child_collection_defs.length > 0
232
+ if @_length_is_valid
233
+ @_length
234
+ else
235
+ @_child_collection_defs.each {|cd| @_length += cd[:fetcher].length }
236
+ @_length_is_valid = true
237
+ end
238
+ else
239
+ @_length
240
+ end
241
+ end
242
+
243
+ # Invoked when child controller context needs to be processed
244
+ # before initializing.
245
+ def process_child_context(ctx, route)
246
+ ctx
247
+ end
248
+
249
+ alias_method :size, :length
250
+
251
+ # Returns controller string representation. Defaults to route name.
252
+ def to_s
253
+ @_route.to_s
254
+ end
255
+
256
+ # Invoked when visual parent needs to be determined.
257
+ # Defaults to logical parent.
258
+ def visual_parent
259
+ @_logical_parent
260
+ end
261
+
262
+ # Returns the topmost visual container that should be rendered.
263
+ def visual_top
264
+ @_ctx.visual_top
265
+ end
266
+
267
+ # Returns Rack request object
268
+ def request
269
+ @_ctx.request
270
+ end
271
+
272
+ # Returns Rack response object
273
+ def response
274
+ @_ctx.response
275
+ end
276
+
277
+ # Returns Rack params hash
278
+ def params
279
+ request.params
280
+ end
281
+
282
+ # Sets HTTP response code.
283
+ def status(value)
284
+ @_ctx.response.status = value
285
+ end
286
+
287
+ # Redirects to the specified URL.
288
+ def redirect(url)
289
+ @_ctx.response.redirect(url)
290
+ halt
291
+ end
292
+
293
+ # Includes JavaScript in page footer
294
+ def javascript(file)
295
+ if file.is_a? Symbol
296
+ _, file = *Loader.resource_owner(self.class, file, '.js')
297
+ end
298
+ external = file =~ /^https?:/
299
+ ctx.javascripts[file] = external
300
+ end
301
+
302
+ # Immediately stops request and returns response.
303
+ def halt
304
+ throw :halt
305
+ end
306
+
307
+ # Returns default result for HTTP GET to controller's address.
308
+ def get
309
+ visual_top.method(:page_view)
310
+ end
311
+
312
+ # Returns default result for HTTP POST to controller's address.
313
+ def post
314
+ status 404
315
+ nil
316
+ end
317
+
318
+ # Returns default result for HTTP PUT to controller's address.
319
+ def put
320
+ status 404
321
+ nil
322
+ end
323
+
324
+ # Returns default result for HTTP DELETE to controller's address.
325
+ def delete
326
+ status 404
327
+ nil
328
+ end
329
+
330
+ def has_action(method, route, &block)
331
+ (@_child_defs[route.to_sym] ||= {:type => :action})[method] = block
332
+ end
333
+
334
+ private
335
+
336
+ # Defines a child of class +klass+ on +route+ with +model+,
337
+ # optionally +hidden+.
338
+ def has_child(klass, route, model=nil, hidden=false)
339
+ @_child_defs[route.to_sym] = {
340
+ :class => klass,
341
+ :model => model,
342
+ :hidden => hidden
343
+ }
344
+ @_length += 1 unless hidden
345
+ self
346
+ end
347
+
348
+ # Defines a child collection of type +parse_regexp+,
349
+ # formatted back by +format+ block.
350
+ def has_child_collection(child_def_fetcher, parse_regexp, &format)
351
+ @_child_defs[parse_regexp] = @_child_collection_defs.size
352
+ @_child_collection_defs << {
353
+ :parse => parse_regexp,
354
+ :format => format,
355
+ :fetcher => child_def_fetcher
356
+ }
357
+ @_length_is_valid = false
358
+ end
359
+
360
+ # Invoked for +route+ with +args+ when a route is missing.
361
+ # This hook can be used to make ghost routes.
362
+ def missing_route(route, *args)
363
+ @_ctx.missing_page.new(@_ctx, self, {:route => route, :args => []})
364
+ end
365
+
366
+ @_arg_defs = {}
367
+
368
+ class << self
369
+
370
+ # Returns own or superclass argument definitions.
371
+ def arg_defs
372
+ @_arg_defs ||= superclass.arg_defs.dup
373
+ end
374
+
375
+ # Dispathes route chain in context +ctx+ on +request_path+,
376
+ # starting with controller +klass+.
377
+ def dispatch(ctx, klass, request_path)
378
+ route_parts = parse_path(request_path)
379
+
380
+ # Prepare for getting an Action result
381
+ action_result = nil
382
+
383
+
384
+ # Set logical children for active controllers
385
+ curr = root_ctrl = klass.new(ctx, nil, nil, true)
386
+ nxt = nil
387
+ route_parts.each do |route_part|
388
+ curr.instance_variable_set :@_active, true
389
+ action_result = catch :action do
390
+ nxt = curr[route_part[:route], *route_part[:args]]
391
+ nil
392
+ end
393
+ break if action_result
394
+ curr.logical_child = nxt
395
+ curr = nxt
396
+ end
397
+
398
+
399
+ # Set links for active controllers and default routes (only for GET)
400
+ if ctx.request.get? && !action_result
401
+ while route_part = curr.default_route
402
+
403
+ # Do a redirect, if some controller in the chain asks for it
404
+ if route_part[:redirect]
405
+ klass = curr.child_class(route_part)
406
+ curr.redirect grow_link(curr, route_part, klass.arg_defs)
407
+ end
408
+
409
+ # Add default route as logical child
410
+ curr.instance_variable_set :@_active, true
411
+ action_result = catch :action do
412
+ nxt = curr[route_part[:route], *route_part[:args]]
413
+ nil
414
+ end
415
+ break if action_result
416
+ curr.logical_child = nxt
417
+ curr = nxt
418
+
419
+ end
420
+ end
421
+
422
+ # Find out dispatch result type from current controller
423
+ curr.instance_variable_set :@_active, true
424
+ curr.instance_variable_set :@_current, true
425
+ type = (curr.is_a? ctx.missing_page) ? :missing_page : :page
426
+
427
+ # Set visual children for active controllers
428
+ last = curr
429
+ prev = curr
430
+ while curr = prev.visual_parent
431
+ curr.visual_child = prev
432
+ prev = curr
433
+ end
434
+
435
+ # Set visual top
436
+ ctx.visual_top = prev
437
+
438
+ return action_result.call if action_result
439
+ last.send :"#{ctx.request.request_method.downcase}"
440
+ end
441
+
442
+ # Escapes characters +chrs+ and encodes a given string +s+
443
+ # for use in links.
444
+ def escape(s, chrs)
445
+ if s
446
+ Rack::Utils.escape(s.to_s.gsub(/[\$#{chrs}]/, Const::ESCAPED_MATCH))
447
+ else
448
+ nil
449
+ end
450
+ end
451
+
452
+ # Extracts arguments, initializing default values beforehand.
453
+ # Searches +md+ hash for default value overrides.
454
+ def extract_args(md)
455
+ res = []
456
+ arg_defs.each_pair do |name, arg|
457
+ res[arg[:index]] = md[name]
458
+ end
459
+ res
460
+ end
461
+
462
+ # Builds link from controller +ctrl+ to a given route.
463
+ def grow_link(ctrl, route_part, arg_defs)
464
+ args = route_part[:args].map {|k, v|
465
+ if arg_defs[k][:arg].default == v
466
+ ''
467
+ else
468
+ key = escape(k, Const::ARG_KEY_ESCAPE)
469
+ value = escape(v, Const::ARG_VALUE_ESCAPE)
470
+ ":#{key}-#{value}"
471
+ end
472
+ }.join
473
+ own_link = escape(route_part[:route], Const::ARG_VALUE_ESCAPE) << args
474
+ "#{ctrl.link == Const::SLASH ? '' : ctrl.link}/#{own_link}"
475
+ end
476
+
477
+ # Defines an argument with a +name+,
478
+ # derived from type +obj+ with additional +args+.
479
+ def has_arg(name, obj, *args)
480
+ # TODO Ensure thread safety
481
+ arg_defs[name] = {
482
+ :arg => Argument.to_argument(obj, *args),
483
+ :index => @_arg_defs.size
484
+ }
485
+ end
486
+
487
+ private
488
+
489
+ # Parses +path+ to return route name and arguments.
490
+ def parse_path(path)
491
+ path[1..-1].split(Const::UNESCAPED_SLASH).map do |s|
492
+ arr = s.gsub(
493
+ Const::ESCAPED_SLASH,
494
+ Const::SLASH
495
+ ).split(Const::UNESCAPED_COLON)
496
+ route_part = {:route => unescape(arr[0]).to_sym}
497
+ args = {}
498
+ arr[1..-1].each do |argval|
499
+ varr = argval.split(Const::UNESCAPED_MINUS)
500
+ args[unescape(varr[0])] = unescape(varr[1..-1].join)
501
+ # TODO Predict argument
502
+ end
503
+ route_part[:args] = extract_args(args)
504
+ route_part
505
+ end # map
506
+ end
507
+
508
+ # Unescapes a given link part for internal use.
509
+ def unescape(s)
510
+ s ? s.gsub(Const::ESCAPED_ROUTE_CHARS, Const::FIRST_SUBPATTERN) : nil
511
+ end
512
+
513
+ end # class << self
514
+
515
+ end # Controller
516
+
517
+ end # Tanuki