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