tanuki 0.3.1 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +5 -4
- data/app/tanuki/controller/{link.thtml → controller.link.thtml} +0 -0
- data/app/tanuki/controller/controller.page.thtml +14 -0
- data/app/tanuki/controller/controller.rb +1 -2
- data/app/tanuki/controller/controller.title.ttxt +1 -0
- data/app/tanuki/controller/controller.view.thtml +3 -0
- data/app/tanuki/fetcher/sequel/sequel.rb +34 -0
- data/app/tanuki/manager/controller/controller.rb +1 -1
- data/app/tanuki/manager/page/page.rb +1 -1
- data/app/tanuki/meta_model/{manager.ttxt → meta_model.manager.ttxt} +0 -0
- data/app/tanuki/meta_model/{manager_base.ttxt → meta_model.manager_base.ttxt} +0 -0
- data/app/tanuki/meta_model/{model.ttxt → meta_model.model.ttxt} +0 -0
- data/app/tanuki/meta_model/{model_base.ttxt → meta_model.model_base.ttxt} +0 -0
- data/app/tanuki/meta_model/meta_model.rb +1 -2
- data/app/tanuki/model/controller/controller.rb +1 -1
- data/app/tanuki/model/page/page.rb +1 -1
- data/app/tanuki/page/missing/{default.thtml → missing.page.thtml} +1 -1
- data/app/tanuki/page/missing/missing.rb +3 -2
- data/app/user/page/home/home.rb +2 -0
- data/app/user/page/home/home.title.thtml +1 -0
- data/app/user/page/home/home.view.css +88 -0
- data/app/user/page/home/home.view.thtml +22 -0
- data/bin/tanuki +2 -1
- data/config/common.rb +1 -0
- data/config/common_application.rb +3 -6
- data/config/development_application.rb +0 -3
- data/lib/tanuki.rb +8 -7
- data/lib/tanuki/application.rb +108 -81
- data/lib/tanuki/argument.rb +10 -5
- data/lib/tanuki/argument/integer_range.rb +4 -2
- data/lib/tanuki/{behavior/object_behavior.rb → base_behavior.rb} +21 -4
- data/lib/tanuki/configurator.rb +20 -8
- data/lib/tanuki/const.rb +32 -0
- data/lib/tanuki/context.rb +18 -7
- data/lib/tanuki/controller.rb +517 -0
- data/lib/tanuki/css_compressor.rb +50 -0
- data/lib/tanuki/extensions/module.rb +21 -5
- data/lib/tanuki/extensions/rack/frozen_route.rb +35 -0
- data/lib/tanuki/extensions/rack/static_dir.rb +1 -1
- data/lib/tanuki/extensions/sequel/model.rb +7 -0
- data/lib/tanuki/i18n.rb +8 -6
- data/lib/tanuki/loader.rb +166 -33
- data/lib/tanuki/meta_model.rb +176 -0
- data/lib/tanuki/{behavior/model_behavior.rb → model_behavior.rb} +7 -3
- data/lib/tanuki/model_generator.rb +49 -29
- data/lib/tanuki/template_compiler.rb +72 -41
- data/lib/tanuki/utility.rb +11 -4
- data/lib/tanuki/utility/create.rb +52 -11
- data/lib/tanuki/utility/generate.rb +16 -10
- data/lib/tanuki/utility/version.rb +1 -1
- data/lib/tanuki/version.rb +7 -2
- metadata +50 -66
- data/app/tanuki/controller/default.thtml +0 -5
- data/app/tanuki/controller/index.thtml +0 -1
- data/app/user/page/index/default.thtml +0 -121
- data/app/user/page/index/index.rb +0 -2
- data/config/test_application.rb +0 -2
- data/lib/tanuki/behavior/controller_behavior.rb +0 -366
- data/lib/tanuki/behavior/meta_model_behavior.rb +0 -160
- data/lib/tanuki/extensions/rack/builder.rb +0 -26
- data/lib/tanuki/extensions/rack/server.rb +0 -18
- data/lib/tanuki/launcher.rb +0 -21
- data/lib/tanuki/utility/server.rb +0 -23
data/lib/tanuki/argument.rb
CHANGED
@@ -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
|
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
|
18
|
+
@assoc.delete klass
|
18
19
|
end
|
19
20
|
|
20
|
-
# Associates a given type class +klass+ with an argument class
|
21
|
+
# Associates a given type class +klass+ with an argument class
|
22
|
+
# +arg_class+.
|
21
23
|
def store(klass, arg_class)
|
22
|
-
|
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
|
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
|
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
|
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
|
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(
|
25
|
-
return Tanuki::Loader.run_template(
|
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
|
data/lib/tanuki/configurator.rb
CHANGED
@@ -1,19 +1,31 @@
|
|
1
1
|
module Tanuki
|
2
2
|
|
3
|
-
# Tanuki::Configurator is a scope for evaluating
|
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
|
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
|
-
#
|
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
|
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
|
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
|
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
|
56
|
+
Application.discard middleware
|
45
57
|
end
|
46
58
|
|
47
59
|
# Invokes Tanuki::Application::visitor.
|
48
60
|
def visitor(sym, &block)
|
49
|
-
Application.visitor
|
61
|
+
Application.visitor sym, &block
|
50
62
|
end
|
51
63
|
|
52
64
|
end # Configurator
|
data/lib/tanuki/const.rb
ADDED
@@ -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
|
data/lib/tanuki/context.rb
CHANGED
@@ -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
|
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,
|
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__ +
|
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?
|
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
|
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(
|
45
|
-
|
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
|