tanuki 0.3.1 → 0.4.0

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