joe-merb-core 0.9.8
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +992 -0
- data/CONTRIBUTORS +94 -0
- data/LICENSE +20 -0
- data/PUBLIC_CHANGELOG +142 -0
- data/README +21 -0
- data/Rakefile +456 -0
- data/TODO +0 -0
- data/bin/merb +11 -0
- data/bin/merb-specs +5 -0
- data/lib/merb-core.rb +648 -0
- data/lib/merb-core/autoload.rb +31 -0
- data/lib/merb-core/bootloader.rb +889 -0
- data/lib/merb-core/config.rb +380 -0
- data/lib/merb-core/constants.rb +45 -0
- data/lib/merb-core/controller/abstract_controller.rb +620 -0
- data/lib/merb-core/controller/exceptions.rb +302 -0
- data/lib/merb-core/controller/merb_controller.rb +283 -0
- data/lib/merb-core/controller/mime.rb +111 -0
- data/lib/merb-core/controller/mixins/authentication.rb +123 -0
- data/lib/merb-core/controller/mixins/conditional_get.rb +83 -0
- data/lib/merb-core/controller/mixins/controller.rb +316 -0
- data/lib/merb-core/controller/mixins/render.rb +513 -0
- data/lib/merb-core/controller/mixins/responder.rb +469 -0
- data/lib/merb-core/controller/template.rb +254 -0
- data/lib/merb-core/core_ext.rb +9 -0
- data/lib/merb-core/core_ext/hash.rb +7 -0
- data/lib/merb-core/core_ext/kernel.rb +345 -0
- data/lib/merb-core/dispatch/cookies.rb +130 -0
- data/lib/merb-core/dispatch/default_exception/default_exception.rb +93 -0
- data/lib/merb-core/dispatch/default_exception/views/_css.html.erb +200 -0
- data/lib/merb-core/dispatch/default_exception/views/_javascript.html.erb +77 -0
- data/lib/merb-core/dispatch/default_exception/views/index.html.erb +98 -0
- data/lib/merb-core/dispatch/dispatcher.rb +172 -0
- data/lib/merb-core/dispatch/request.rb +718 -0
- data/lib/merb-core/dispatch/router.rb +228 -0
- data/lib/merb-core/dispatch/router/behavior.rb +610 -0
- data/lib/merb-core/dispatch/router/cached_proc.rb +52 -0
- data/lib/merb-core/dispatch/router/resources.rb +220 -0
- data/lib/merb-core/dispatch/router/route.rb +560 -0
- data/lib/merb-core/dispatch/session.rb +222 -0
- data/lib/merb-core/dispatch/session/container.rb +74 -0
- data/lib/merb-core/dispatch/session/cookie.rb +173 -0
- data/lib/merb-core/dispatch/session/memcached.rb +68 -0
- data/lib/merb-core/dispatch/session/memory.rb +99 -0
- data/lib/merb-core/dispatch/session/store_container.rb +150 -0
- data/lib/merb-core/dispatch/worker.rb +28 -0
- data/lib/merb-core/gem_ext/erubis.rb +77 -0
- data/lib/merb-core/logger.rb +215 -0
- data/lib/merb-core/plugins.rb +67 -0
- data/lib/merb-core/rack.rb +27 -0
- data/lib/merb-core/rack/adapter.rb +47 -0
- data/lib/merb-core/rack/adapter/ebb.rb +24 -0
- data/lib/merb-core/rack/adapter/evented_mongrel.rb +13 -0
- data/lib/merb-core/rack/adapter/fcgi.rb +17 -0
- data/lib/merb-core/rack/adapter/irb.rb +119 -0
- data/lib/merb-core/rack/adapter/mongrel.rb +33 -0
- data/lib/merb-core/rack/adapter/runner.rb +28 -0
- data/lib/merb-core/rack/adapter/swiftiplied_mongrel.rb +14 -0
- data/lib/merb-core/rack/adapter/thin.rb +40 -0
- data/lib/merb-core/rack/adapter/thin_turbo.rb +17 -0
- data/lib/merb-core/rack/adapter/webrick.rb +72 -0
- data/lib/merb-core/rack/application.rb +32 -0
- data/lib/merb-core/rack/handler/mongrel.rb +96 -0
- data/lib/merb-core/rack/middleware.rb +20 -0
- data/lib/merb-core/rack/middleware/conditional_get.rb +29 -0
- data/lib/merb-core/rack/middleware/content_length.rb +18 -0
- data/lib/merb-core/rack/middleware/csrf.rb +73 -0
- data/lib/merb-core/rack/middleware/path_prefix.rb +31 -0
- data/lib/merb-core/rack/middleware/profiler.rb +19 -0
- data/lib/merb-core/rack/middleware/static.rb +45 -0
- data/lib/merb-core/rack/middleware/tracer.rb +20 -0
- data/lib/merb-core/server.rb +321 -0
- data/lib/merb-core/tasks/audit.rake +68 -0
- data/lib/merb-core/tasks/gem_management.rb +252 -0
- data/lib/merb-core/tasks/merb.rb +2 -0
- data/lib/merb-core/tasks/merb_rake_helper.rb +51 -0
- data/lib/merb-core/tasks/stats.rake +71 -0
- data/lib/merb-core/test.rb +17 -0
- data/lib/merb-core/test/helpers.rb +10 -0
- data/lib/merb-core/test/helpers/controller_helper.rb +8 -0
- data/lib/merb-core/test/helpers/multipart_request_helper.rb +176 -0
- data/lib/merb-core/test/helpers/request_helper.rb +61 -0
- data/lib/merb-core/test/helpers/route_helper.rb +47 -0
- data/lib/merb-core/test/helpers/view_helper.rb +121 -0
- data/lib/merb-core/test/matchers.rb +10 -0
- data/lib/merb-core/test/matchers/controller_matchers.rb +108 -0
- data/lib/merb-core/test/matchers/route_matchers.rb +137 -0
- data/lib/merb-core/test/matchers/view_matchers.rb +393 -0
- data/lib/merb-core/test/run_specs.rb +141 -0
- data/lib/merb-core/test/tasks/spectasks.rb +68 -0
- data/lib/merb-core/test/test_ext/hpricot.rb +32 -0
- data/lib/merb-core/test/test_ext/object.rb +14 -0
- data/lib/merb-core/test/test_ext/string.rb +14 -0
- data/lib/merb-core/vendor/facets.rb +2 -0
- data/lib/merb-core/vendor/facets/dictionary.rb +433 -0
- data/lib/merb-core/vendor/facets/inflect.rb +342 -0
- data/lib/merb-core/version.rb +3 -0
- metadata +253 -0
@@ -0,0 +1,52 @@
|
|
1
|
+
module Merb
|
2
|
+
|
3
|
+
class Router
|
4
|
+
# Cache procs for future reference in eval statement
|
5
|
+
class CachedProc
|
6
|
+
@@index = 0
|
7
|
+
@@list = []
|
8
|
+
|
9
|
+
attr_accessor :cache, :index
|
10
|
+
|
11
|
+
# ==== Parameters
|
12
|
+
# cache<Proc>:: The block of code to cache.
|
13
|
+
def initialize(cache)
|
14
|
+
@cache, @index = cache, CachedProc.register(self)
|
15
|
+
end
|
16
|
+
|
17
|
+
# ==== Returns
|
18
|
+
# String:: The CachedProc object in a format embeddable within a string.
|
19
|
+
def to_s
|
20
|
+
"CachedProc[#{@index}].cache"
|
21
|
+
end
|
22
|
+
|
23
|
+
class << self
|
24
|
+
|
25
|
+
# ==== Parameters
|
26
|
+
# cached_code<CachedProc>:: The cached code to register.
|
27
|
+
#
|
28
|
+
# ==== Returns
|
29
|
+
# Fixnum:: The index of the newly registered CachedProc.
|
30
|
+
def register(cached_code)
|
31
|
+
CachedProc[@@index] = cached_code
|
32
|
+
@@index += 1
|
33
|
+
@@index - 1
|
34
|
+
end
|
35
|
+
|
36
|
+
# Sets the cached code for a specific index.
|
37
|
+
#
|
38
|
+
# ==== Parameters
|
39
|
+
# index<Fixnum>:: The index of the cached code to set.
|
40
|
+
# code<CachedProc>:: The cached code to set.
|
41
|
+
def []=(index, code) @@list[index] = code end
|
42
|
+
|
43
|
+
# ==== Parameters
|
44
|
+
# index<Fixnum>:: The index of the cached code to retrieve.
|
45
|
+
#
|
46
|
+
# ==== Returns
|
47
|
+
# CachedProc:: The cached code at index.
|
48
|
+
def [](index) @@list[index] end
|
49
|
+
end
|
50
|
+
end # CachedProc
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,220 @@
|
|
1
|
+
module Merb
|
2
|
+
class Router
|
3
|
+
class Behavior
|
4
|
+
module Resources
|
5
|
+
|
6
|
+
# Behavior#+resources+ is a route helper for defining a collection of
|
7
|
+
# RESTful resources. It yields to a block for child routes.
|
8
|
+
#
|
9
|
+
# ==== Parameters
|
10
|
+
# name<String, Symbol>:: The name of the resources
|
11
|
+
# options<Hash>::
|
12
|
+
# Ovverides and parameters to be associated with the route
|
13
|
+
#
|
14
|
+
# ==== Options (options)
|
15
|
+
# :namespace<~to_s>: The namespace for this route.
|
16
|
+
# :name_prefix<~to_s>:
|
17
|
+
# A prefix for the named routes. If a namespace is passed and there
|
18
|
+
# isn't a name prefix, the namespace will become the prefix.
|
19
|
+
# :controller<~to_s>: The controller for this route
|
20
|
+
# :collection<~to_s>: Special settings for the collections routes
|
21
|
+
# :member<Hash>:
|
22
|
+
# Special settings and resources related to a specific member of this
|
23
|
+
# resource.
|
24
|
+
# :keys<Array>:
|
25
|
+
# A list of the keys to be used instead of :id with the resource in the order of the url.
|
26
|
+
# :singular<Symbol>
|
27
|
+
#
|
28
|
+
# ==== Block parameters
|
29
|
+
# next_level<Behavior>:: The child behavior.
|
30
|
+
#
|
31
|
+
# ==== Returns
|
32
|
+
# Array::
|
33
|
+
# Routes which will define the specified RESTful collection of resources
|
34
|
+
#
|
35
|
+
# ==== Examples
|
36
|
+
#
|
37
|
+
# r.resources :posts # will result in the typical RESTful CRUD
|
38
|
+
# # lists resources
|
39
|
+
# # GET /posts/?(\.:format)? :action => "index"
|
40
|
+
# # GET /posts/index(\.:format)? :action => "index"
|
41
|
+
#
|
42
|
+
# # shows new resource form
|
43
|
+
# # GET /posts/new :action => "new"
|
44
|
+
#
|
45
|
+
# # creates resource
|
46
|
+
# # POST /posts/?(\.:format)?, :action => "create"
|
47
|
+
#
|
48
|
+
# # shows resource
|
49
|
+
# # GET /posts/:id(\.:format)? :action => "show"
|
50
|
+
#
|
51
|
+
# # shows edit form
|
52
|
+
# # GET /posts/:id/edit :action => "edit"
|
53
|
+
#
|
54
|
+
# # updates resource
|
55
|
+
# # PUT /posts/:id(\.:format)? :action => "update"
|
56
|
+
#
|
57
|
+
# # shows deletion confirmation page
|
58
|
+
# # GET /posts/:id/delete :action => "delete"
|
59
|
+
#
|
60
|
+
# # destroys resources
|
61
|
+
# # DELETE /posts/:id(\.:format)? :action => "destroy"
|
62
|
+
#
|
63
|
+
# # Nesting resources
|
64
|
+
# r.resources :posts do |posts|
|
65
|
+
# posts.resources :comments
|
66
|
+
# end
|
67
|
+
#---
|
68
|
+
# @public
|
69
|
+
def resources(name, *args, &block)
|
70
|
+
name = name.to_s
|
71
|
+
options = extract_options_from_args!(args) || {}
|
72
|
+
singular = options[:singular] ? options[:singular].to_s : Extlib::Inflection.singularize(name)
|
73
|
+
klass = args.first ? args.first.to_s : Extlib::Inflection.classify(singular)
|
74
|
+
keys = [ options.delete(:keys) || options.delete(:key) || :id ].flatten
|
75
|
+
params = { :controller => options.delete(:controller) || name }
|
76
|
+
collection = options.delete(:collection) || {}
|
77
|
+
member = { :edit => :get, :delete => :get }.merge(options.delete(:member) || {})
|
78
|
+
|
79
|
+
# Try pulling :namespace out of options for backwards compatibility
|
80
|
+
options[:name_prefix] ||= nil # Don't use a name_prefix if not needed
|
81
|
+
options[:resource_prefix] ||= nil # Don't use a resource_prefix if not needed
|
82
|
+
options[:controller_prefix] ||= options.delete(:namespace)
|
83
|
+
|
84
|
+
self.namespace(name, options).to(params) do |resource|
|
85
|
+
root_keys = keys.map { |k| ":#{k}" }.join("/")
|
86
|
+
|
87
|
+
# => index
|
88
|
+
resource.match("(/index)(.:format)", :method => :get).to(:action => "index").
|
89
|
+
name(name).register_resource(name)
|
90
|
+
|
91
|
+
# => create
|
92
|
+
resource.match("(.:format)", :method => :post).to(:action => "create")
|
93
|
+
|
94
|
+
# => new
|
95
|
+
resource.match("/new(.:format)", :method => :get).to(:action => "new").
|
96
|
+
name("new", singular).register_resource(name, "new")
|
97
|
+
|
98
|
+
# => user defined collection routes
|
99
|
+
collection.each_pair do |action, method|
|
100
|
+
action = action.to_s
|
101
|
+
resource.match("/#{action}(.:format)", :method => method).to(:action => "#{action}").
|
102
|
+
name(action, name).register_resource(name, action)
|
103
|
+
end
|
104
|
+
|
105
|
+
# => show
|
106
|
+
resource.match("/#{root_keys}(.:format)", :method => :get).to(:action => "show").
|
107
|
+
name(singular).register_resource(klass)
|
108
|
+
|
109
|
+
# => user defined member routes
|
110
|
+
member.each_pair do |action, method|
|
111
|
+
action = action.to_s
|
112
|
+
resource.match("/#{root_keys}/#{action}(.:format)", :method => method).
|
113
|
+
to(:action => "#{action}").name(action, singular).register_resource(klass, action)
|
114
|
+
end
|
115
|
+
|
116
|
+
# => update
|
117
|
+
resource.match("/#{root_keys}(.:format)", :method => :put).
|
118
|
+
to(:action => "update")
|
119
|
+
|
120
|
+
# => destroy
|
121
|
+
resource.match("/#{root_keys}(.:format)", :method => :delete).
|
122
|
+
to(:action => "destroy")
|
123
|
+
|
124
|
+
if block_given?
|
125
|
+
nested_keys = keys.map { |k| k.to_s == "id" ? ":#{singular}_id" : ":#{k}" }.join("/")
|
126
|
+
resource.options(:name_prefix => singular, :resource_prefix => klass).match("/#{nested_keys}", &block)
|
127
|
+
end
|
128
|
+
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
# Behavior#+resource+ is a route helper for defining a singular RESTful
|
133
|
+
# resource. It yields to a block for child routes.
|
134
|
+
#
|
135
|
+
# ==== Parameters
|
136
|
+
# name<String, Symbol>:: The name of the resource.
|
137
|
+
# options<Hash>::
|
138
|
+
# Overides and parameters to be associated with the route.
|
139
|
+
#
|
140
|
+
# ==== Options (options)
|
141
|
+
# :namespace<~to_s>: The namespace for this route.
|
142
|
+
# :name_prefix<~to_s>:
|
143
|
+
# A prefix for the named routes. If a namespace is passed and there
|
144
|
+
# isn't a name prefix, the namespace will become the prefix.
|
145
|
+
# :controller<~to_s>: The controller for this route
|
146
|
+
#
|
147
|
+
# ==== Block parameters
|
148
|
+
# next_level<Behavior>:: The child behavior.
|
149
|
+
#
|
150
|
+
# ==== Returns
|
151
|
+
# Array:: Routes which define a RESTful single resource.
|
152
|
+
#
|
153
|
+
# ==== Examples
|
154
|
+
#
|
155
|
+
# r.resource :account # will result in the typical RESTful CRUD
|
156
|
+
# # shows new resource form
|
157
|
+
# # GET /account/new :action => "new"
|
158
|
+
#
|
159
|
+
# # creates resource
|
160
|
+
# # POST /account/?(\.:format)?, :action => "create"
|
161
|
+
#
|
162
|
+
# # shows resource
|
163
|
+
# # GET /account/(\.:format)? :action => "show"
|
164
|
+
#
|
165
|
+
# # shows edit form
|
166
|
+
# # GET /account//edit :action => "edit"
|
167
|
+
#
|
168
|
+
# # updates resource
|
169
|
+
# # PUT /account/(\.:format)? :action => "update"
|
170
|
+
#
|
171
|
+
# # shows deletion confirmation page
|
172
|
+
# # GET /account//delete :action => "delete"
|
173
|
+
#
|
174
|
+
# # destroys resources
|
175
|
+
# # DELETE /account/(\.:format)? :action => "destroy"
|
176
|
+
#
|
177
|
+
# You can optionally pass :namespace and :controller to refine the routing
|
178
|
+
# or pass a block to nest resources.
|
179
|
+
#
|
180
|
+
# r.resource :account, :namespace => "admin" do |account|
|
181
|
+
# account.resources :preferences, :controller => "settings"
|
182
|
+
# end
|
183
|
+
# ---
|
184
|
+
# @public
|
185
|
+
def resource(name, *args, &block)
|
186
|
+
name = name.to_s
|
187
|
+
options = extract_options_from_args!(args) || {}
|
188
|
+
params = { :controller => options.delete(:controller) || name.pluralize }
|
189
|
+
|
190
|
+
options[:name_prefix] ||= nil # Don't use a name_prefix if not needed
|
191
|
+
options[:resource_prefix] ||= nil # Don't use a resource_prefix if not needed
|
192
|
+
options[:controller_prefix] ||= options.delete(:namespace)
|
193
|
+
|
194
|
+
self.namespace(name, options).to(params) do |resource|
|
195
|
+
resource.match("(.:format)", :method => :get ).to(:action => "show" ).name(name).register_resource(name)
|
196
|
+
resource.match("(.:format)", :method => :post ).to(:action => "create" )
|
197
|
+
resource.match("(.:format)", :method => :put ).to(:action => "update" )
|
198
|
+
resource.match("(.:format)", :method => :delete).to(:action => "destroy")
|
199
|
+
resource.match("/new(.:format)", :method => :get ).to(:action => "new" ).name(:new, name).register_resource(name, "new")
|
200
|
+
resource.match("/edit(.:format)", :method => :get ).to(:action => "edit" ).name(:edit, name).register_resource(name, "edit")
|
201
|
+
resource.match("/delete(.:format)", :method => :get ).to(:action => "delete" ).name(:delete, name).register_resource(name, "delete")
|
202
|
+
|
203
|
+
resource.options(:name_prefix => name, :resource_prefix => name, &block) if block_given?
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
protected
|
208
|
+
|
209
|
+
def register_resource(*key)
|
210
|
+
key = [@options[:resource_prefix], key].flatten.compact
|
211
|
+
@route.resource = key
|
212
|
+
self
|
213
|
+
end
|
214
|
+
|
215
|
+
end
|
216
|
+
|
217
|
+
include Resources
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
@@ -0,0 +1,560 @@
|
|
1
|
+
module Merb
|
2
|
+
|
3
|
+
class Router
|
4
|
+
# This entire class is private and should never be accessed outside of
|
5
|
+
# Merb::Router and Behavior
|
6
|
+
class Route #:nodoc:
|
7
|
+
SEGMENT_REGEXP = /(:([a-z](_?[a-z0-9])*))/
|
8
|
+
OPTIONAL_SEGMENT_REGEX = /^.*?([\(\)])/i
|
9
|
+
SEGMENT_REGEXP_WITH_BRACKETS = /(:[a-z_]+)(\[(\d+)\])?/
|
10
|
+
JUST_BRACKETS = /\[(\d+)\]/
|
11
|
+
SEGMENT_CHARACTERS = "[^\/.,;?]".freeze
|
12
|
+
|
13
|
+
attr_reader :conditions, :params, :segments
|
14
|
+
attr_reader :index, :variables, :name
|
15
|
+
attr_accessor :fixation
|
16
|
+
|
17
|
+
def initialize(conditions, params, deferred_procs, options = {})
|
18
|
+
@conditions, @params = conditions, params
|
19
|
+
|
20
|
+
if options[:redirects]
|
21
|
+
@redirects = true
|
22
|
+
@redirect_status = @params[:status]
|
23
|
+
@redirect_url = @params[:url]
|
24
|
+
@defaults = {}
|
25
|
+
else
|
26
|
+
@defaults = options[:defaults] || {}
|
27
|
+
end
|
28
|
+
|
29
|
+
# @conditional_block = conditional_block
|
30
|
+
|
31
|
+
@identifiers = options[:identifiers]
|
32
|
+
@deferred_procs = deferred_procs
|
33
|
+
@segments = []
|
34
|
+
@symbol_conditions = {}
|
35
|
+
@placeholders = {}
|
36
|
+
compile
|
37
|
+
end
|
38
|
+
|
39
|
+
def regexp?
|
40
|
+
@regexp
|
41
|
+
end
|
42
|
+
|
43
|
+
def allow_fixation?
|
44
|
+
@fixation
|
45
|
+
end
|
46
|
+
|
47
|
+
def to_s
|
48
|
+
regexp? ?
|
49
|
+
"/#{conditions[:path].source}/" :
|
50
|
+
segment_level_to_s(segments)
|
51
|
+
end
|
52
|
+
|
53
|
+
alias_method :inspect, :to_s
|
54
|
+
|
55
|
+
def register
|
56
|
+
@index = Merb::Router.routes.size
|
57
|
+
Merb::Router.routes << self
|
58
|
+
self
|
59
|
+
end
|
60
|
+
|
61
|
+
# Sets the route as a resource route with the given key as the
|
62
|
+
# lookup key.
|
63
|
+
def resource=(key)
|
64
|
+
Router.resource_routes[key] = self
|
65
|
+
key
|
66
|
+
end
|
67
|
+
|
68
|
+
def name=(name)
|
69
|
+
@name = name.to_sym
|
70
|
+
Router.named_routes[@name] = self
|
71
|
+
@name
|
72
|
+
end
|
73
|
+
|
74
|
+
# === Compiled method ===
|
75
|
+
def generate(args = [], defaults = {})
|
76
|
+
raise GenerationError, "Cannot generate regexp Routes" if regexp?
|
77
|
+
|
78
|
+
params = extract_options_from_args!(args) || { }
|
79
|
+
|
80
|
+
# Support for anonymous params
|
81
|
+
unless args.empty?
|
82
|
+
# First, let's determine which variables are missing
|
83
|
+
variables = @variables - params.keys
|
84
|
+
|
85
|
+
raise GenerationError, "The route has #{@variables.length} variables: #{@variables.inspect}" if args.length > variables.length
|
86
|
+
|
87
|
+
args.each_with_index do |param, i|
|
88
|
+
params[variables[i]] ||= param
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
uri = @generator[params, defaults] or raise GenerationError, "Named route #{name} could not be generated with #{params.inspect}"
|
93
|
+
uri = Merb::Config[:path_prefix] + uri if Merb::Config[:path_prefix]
|
94
|
+
uri
|
95
|
+
end
|
96
|
+
|
97
|
+
def compiled_statement(first)
|
98
|
+
els_if = first ? ' if ' : ' elsif '
|
99
|
+
|
100
|
+
code = ""
|
101
|
+
code << els_if << condition_statements.join(" && ") << "\n"
|
102
|
+
|
103
|
+
# First, we need to always return the value of the
|
104
|
+
# deferred block if it explicitly matched the route
|
105
|
+
if @redirects && @deferred_procs.any?
|
106
|
+
code << " return [#{@index.inspect}, block_result] if request.matched?" << "\n"
|
107
|
+
code << " request.redirects!" << "\n"
|
108
|
+
code << " [#{@index.inspect}, { :url => #{@redirect_url.inspect}, :status => #{@redirect_status.inspect} }]" << "\n"
|
109
|
+
elsif @redirects
|
110
|
+
code << " request.redirects!" << "\n"
|
111
|
+
code << " [#{@index.inspect}, { :url => #{@redirect_url.inspect}, :status => #{@redirect_status.inspect} }]" << "\n"
|
112
|
+
elsif @deferred_procs.any?
|
113
|
+
code << " [#{@index.inspect}, block_result]" << "\n"
|
114
|
+
else
|
115
|
+
code << " [#{@index.inspect}, #{params_as_string}]" << "\n"
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
private
|
120
|
+
|
121
|
+
# === Compilation ===
|
122
|
+
|
123
|
+
def compile
|
124
|
+
compile_conditions
|
125
|
+
compile_params
|
126
|
+
@generator = Generator.new(@segments, @symbol_conditions, @identifiers).compiled
|
127
|
+
end
|
128
|
+
|
129
|
+
# The Generator class handles compiling the route down to a lambda that
|
130
|
+
# can generate the URL from a params hash and a default params hash.
|
131
|
+
class Generator #:nodoc:
|
132
|
+
|
133
|
+
def initialize(segments, symbol_conditions, identifiers)
|
134
|
+
@segments = segments
|
135
|
+
@symbol_conditions = symbol_conditions
|
136
|
+
@identifiers = identifiers
|
137
|
+
@stack = []
|
138
|
+
@opt_segment_count = 0
|
139
|
+
@opt_segment_stack = [[]]
|
140
|
+
end
|
141
|
+
|
142
|
+
def compiled
|
143
|
+
ruby = ""
|
144
|
+
ruby << "lambda do |params, defaults|\n"
|
145
|
+
ruby << " fragment = params.delete(:fragment)\n"
|
146
|
+
ruby << " query_params = params.dup\n"
|
147
|
+
|
148
|
+
with(@segments) do
|
149
|
+
ruby << " include_defaults = true\n"
|
150
|
+
ruby << " return unless url = #{block_for_level}\n"
|
151
|
+
end
|
152
|
+
|
153
|
+
ruby << " query_params.delete_if { |key, value| value.nil? }\n"
|
154
|
+
ruby << " unless query_params.empty?\n"
|
155
|
+
ruby << ' url << "?#{Merb::Request.params_to_query_string(query_params)}"' << "\n"
|
156
|
+
ruby << " end\n"
|
157
|
+
ruby << ' url << "##{fragment}" if fragment' << "\n"
|
158
|
+
ruby << " url\n"
|
159
|
+
ruby << "end\n"
|
160
|
+
|
161
|
+
eval(ruby)
|
162
|
+
end
|
163
|
+
|
164
|
+
private
|
165
|
+
|
166
|
+
# Cleans up methods a bunch. We don't need to pass the current segment
|
167
|
+
# level around everywhere anymore. It's kept track for us in the stack.
|
168
|
+
def with(segments, &block)
|
169
|
+
@stack.push(segments)
|
170
|
+
retval = yield
|
171
|
+
@stack.pop
|
172
|
+
retval
|
173
|
+
end
|
174
|
+
|
175
|
+
def segments
|
176
|
+
@stack.last || []
|
177
|
+
end
|
178
|
+
|
179
|
+
def symbol_segments
|
180
|
+
segments.flatten.select { |s| s.is_a?(Symbol) }
|
181
|
+
end
|
182
|
+
|
183
|
+
def current_segments
|
184
|
+
segments.select { |s| s.is_a?(Symbol) }
|
185
|
+
end
|
186
|
+
|
187
|
+
def nested_segments
|
188
|
+
segments.select { |s| s.is_a?(Array) }.flatten.select { |s| s.is_a?(Symbol) }
|
189
|
+
end
|
190
|
+
|
191
|
+
def block_for_level
|
192
|
+
ruby = ""
|
193
|
+
ruby << "if #{segment_level_matches_conditions}\n"
|
194
|
+
ruby << " #{remove_used_segments_in_query_path}\n"
|
195
|
+
ruby << " #{generate_optional_segments}\n"
|
196
|
+
ruby << %{ "#{combine_required_and_optional_segments}"\n}
|
197
|
+
ruby << "end"
|
198
|
+
end
|
199
|
+
|
200
|
+
def check_if_defaults_should_be_included
|
201
|
+
ruby = ""
|
202
|
+
ruby << "include_defaults = "
|
203
|
+
symbol_segments.each { |s| ruby << "params[#{s.inspect}] || " }
|
204
|
+
ruby << "false"
|
205
|
+
end
|
206
|
+
|
207
|
+
# --- Not so pretty ---
|
208
|
+
def segment_level_matches_conditions
|
209
|
+
conditions = current_segments.map do |segment|
|
210
|
+
condition = "(cached_#{segment} = params[#{segment.inspect}] || include_defaults && defaults[#{segment.inspect}])"
|
211
|
+
|
212
|
+
if @symbol_conditions[segment] && @symbol_conditions[segment].is_a?(Regexp)
|
213
|
+
condition << " =~ #{@symbol_conditions[segment].inspect}"
|
214
|
+
elsif @symbol_conditions[segment]
|
215
|
+
condition << " == #{@symbol_conditions[segment].inspect}"
|
216
|
+
end
|
217
|
+
|
218
|
+
condition
|
219
|
+
end
|
220
|
+
|
221
|
+
conditions << "true" if conditions.empty?
|
222
|
+
conditions.join(" && ")
|
223
|
+
end
|
224
|
+
|
225
|
+
def remove_used_segments_in_query_path
|
226
|
+
"#{current_segments.inspect}.each { |s| query_params.delete(s) }"
|
227
|
+
end
|
228
|
+
|
229
|
+
def generate_optional_segments
|
230
|
+
optionals = []
|
231
|
+
|
232
|
+
segments.each_with_index do |segment, i|
|
233
|
+
if segment.is_a?(Array) && segment.any? { |s| !s.is_a?(String) }
|
234
|
+
with(segment) do
|
235
|
+
@opt_segment_stack.last << (optional_name = "_optional_segments_#{@opt_segment_count += 1}")
|
236
|
+
@opt_segment_stack.push []
|
237
|
+
optionals << "#{check_if_defaults_should_be_included}\n"
|
238
|
+
optionals << "#{optional_name} = #{block_for_level}"
|
239
|
+
@opt_segment_stack.pop
|
240
|
+
end
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
optionals.join("\n")
|
245
|
+
end
|
246
|
+
|
247
|
+
def combine_required_and_optional_segments
|
248
|
+
bits = ""
|
249
|
+
|
250
|
+
segments.each_with_index do |segment, i|
|
251
|
+
bits << case
|
252
|
+
when segment.is_a?(String) then segment
|
253
|
+
when segment.is_a?(Symbol) then '#{param_for_route(cached_' + segment.to_s + ')}'
|
254
|
+
when segment.is_a?(Array) && segment.any? { |s| !s.is_a?(String) } then "\#{#{@opt_segment_stack.last.shift}}"
|
255
|
+
else ""
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
bits
|
260
|
+
end
|
261
|
+
|
262
|
+
def param_for_route(param)
|
263
|
+
case param
|
264
|
+
when String, Symbol, Numeric, TrueClass, FalseClass, NilClass
|
265
|
+
param
|
266
|
+
else
|
267
|
+
_, identifier = @identifiers.find { |klass, _| param.is_a?(klass) }
|
268
|
+
identifier ? param.send(identifier) : param
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
end
|
273
|
+
|
274
|
+
# === Conditions ===
|
275
|
+
|
276
|
+
def compile_conditions
|
277
|
+
@original_conditions = conditions.dup
|
278
|
+
|
279
|
+
if path = conditions[:path]
|
280
|
+
path = [path].flatten.compact
|
281
|
+
if path = compile_path(path)
|
282
|
+
conditions[:path] = Regexp.new("^#{path}$")
|
283
|
+
else
|
284
|
+
conditions.delete(:path)
|
285
|
+
end
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
# The path is passed in as an array of different parts. We basically have
|
290
|
+
# to concat all the parts together, then parse the path and extract the
|
291
|
+
# variables. However, if any of the parts are a regular expression, then
|
292
|
+
# we abort the parsing and just convert it to a regexp.
|
293
|
+
def compile_path(path)
|
294
|
+
@segments = []
|
295
|
+
compiled = ""
|
296
|
+
|
297
|
+
return nil if path.nil? || path.empty?
|
298
|
+
|
299
|
+
path.each do |part|
|
300
|
+
case part
|
301
|
+
when Regexp
|
302
|
+
@regexp = true
|
303
|
+
@segments = []
|
304
|
+
compiled << part.source.sub(/^\^/, '').sub(/\$$/, '')
|
305
|
+
when String
|
306
|
+
segments = segments_with_optionals_from_string(part.dup)
|
307
|
+
compile_path_segments(compiled, segments)
|
308
|
+
# Concat the segments
|
309
|
+
unless regexp?
|
310
|
+
if @segments[-1].is_a?(String) && segments[0].is_a?(String)
|
311
|
+
@segments[-1] << segments.shift
|
312
|
+
end
|
313
|
+
@segments.concat segments
|
314
|
+
end
|
315
|
+
else
|
316
|
+
raise ArgumentError.new("A route path can only be specified as a String or Regexp")
|
317
|
+
end
|
318
|
+
end
|
319
|
+
|
320
|
+
unless regexp?
|
321
|
+
@variables = @segments.flatten.select { |s| s.is_a?(Symbol) }
|
322
|
+
compiled.gsub!(%r[/+], '/')
|
323
|
+
compiled.gsub!(%r[(.+)/$], '\1')
|
324
|
+
end
|
325
|
+
|
326
|
+
compiled
|
327
|
+
end
|
328
|
+
|
329
|
+
# Simple nested parenthesis parser
|
330
|
+
def segments_with_optionals_from_string(path, nest_level = 0)
|
331
|
+
segments = []
|
332
|
+
|
333
|
+
# Extract all the segments at this parenthesis level
|
334
|
+
while segment = path.slice!(OPTIONAL_SEGMENT_REGEX)
|
335
|
+
# Append the segments that we came across so far
|
336
|
+
# at this level
|
337
|
+
segments.concat segments_from_string(segment[0..-2]) if segment.length > 1
|
338
|
+
# If the parenthesis that we came across is an opening
|
339
|
+
# then we need to jump to the higher level
|
340
|
+
if segment[-1,1] == '('
|
341
|
+
segments << segments_with_optionals_from_string(path, nest_level + 1)
|
342
|
+
else
|
343
|
+
# Throw an error if we can't actually go back down (aka syntax error)
|
344
|
+
raise "There are too many closing parentheses" if nest_level == 0
|
345
|
+
return segments
|
346
|
+
end
|
347
|
+
end
|
348
|
+
|
349
|
+
# Save any last bit of the string that didn't match the original regex
|
350
|
+
segments.concat segments_from_string(path) unless path.empty?
|
351
|
+
|
352
|
+
# Throw an error if the string should not actually be done (aka syntax error)
|
353
|
+
raise "You have too many opening parentheses" unless nest_level == 0
|
354
|
+
|
355
|
+
segments
|
356
|
+
end
|
357
|
+
|
358
|
+
def segments_from_string(path)
|
359
|
+
segments = []
|
360
|
+
|
361
|
+
while match = (path.match(SEGMENT_REGEXP))
|
362
|
+
segments << match.pre_match unless match.pre_match.empty?
|
363
|
+
segments << match[2].intern
|
364
|
+
path = match.post_match
|
365
|
+
end
|
366
|
+
|
367
|
+
segments << path unless path.empty?
|
368
|
+
segments
|
369
|
+
end
|
370
|
+
|
371
|
+
# --- Yeah, this could probably be refactored
|
372
|
+
def compile_path_segments(compiled, segments)
|
373
|
+
segments.each do |segment|
|
374
|
+
case segment
|
375
|
+
when String
|
376
|
+
compiled << Regexp.escape(segment)
|
377
|
+
when Symbol
|
378
|
+
condition = (@symbol_conditions[segment] ||= @conditions.delete(segment))
|
379
|
+
compiled << compile_segment_condition(condition)
|
380
|
+
# Create a param for the Symbol segment if none already exists
|
381
|
+
@params[segment] = "#{segment.inspect}" unless @params.has_key?(segment)
|
382
|
+
@placeholders[segment] ||= capturing_parentheses_count(compiled)
|
383
|
+
when Array
|
384
|
+
compiled << "(?:"
|
385
|
+
compile_path_segments(compiled, segment)
|
386
|
+
compiled << ")?"
|
387
|
+
else
|
388
|
+
raise ArgumentError, "conditions[:path] segments can only be a Strings, Symbols, or Arrays"
|
389
|
+
end
|
390
|
+
end
|
391
|
+
end
|
392
|
+
|
393
|
+
# Handles anchors in Regexp conditions
|
394
|
+
def compile_segment_condition(condition)
|
395
|
+
return "(#{SEGMENT_CHARACTERS}+)" unless condition
|
396
|
+
return "(#{condition})" unless condition.is_a?(Regexp)
|
397
|
+
|
398
|
+
condition = condition.source
|
399
|
+
# Handle the start anchor
|
400
|
+
condition = if condition =~ /^\^/
|
401
|
+
condition[1..-1]
|
402
|
+
else
|
403
|
+
"#{SEGMENT_CHARACTERS}*#{condition}"
|
404
|
+
end
|
405
|
+
# Handle the end anchor
|
406
|
+
condition = if condition =~ /\$$/
|
407
|
+
condition[0..-2]
|
408
|
+
else
|
409
|
+
"#{condition}#{SEGMENT_CHARACTERS}*"
|
410
|
+
end
|
411
|
+
|
412
|
+
"(#{condition})"
|
413
|
+
end
|
414
|
+
|
415
|
+
def compile_params
|
416
|
+
# Loop through each param and compile it
|
417
|
+
@defaults.merge(@params).each do |key, value|
|
418
|
+
if value.nil?
|
419
|
+
@params.delete(key)
|
420
|
+
elsif value.is_a?(String)
|
421
|
+
@params[key] = compile_param(value)
|
422
|
+
else
|
423
|
+
@params[key] = value.inspect
|
424
|
+
end
|
425
|
+
end
|
426
|
+
end
|
427
|
+
|
428
|
+
# This was pretty much a copy / paste from the old router
|
429
|
+
def compile_param(value)
|
430
|
+
result = []
|
431
|
+
match = true
|
432
|
+
while match
|
433
|
+
if match = SEGMENT_REGEXP_WITH_BRACKETS.match(value)
|
434
|
+
result << match.pre_match.inspect unless match.pre_match.empty?
|
435
|
+
placeholder_key = match[1][1..-1].intern
|
436
|
+
if match[2] # has brackets, e.g. :path[2]
|
437
|
+
result << "#{placeholder_key}#{match[3]}"
|
438
|
+
else # no brackets, e.g. a named placeholder such as :controller
|
439
|
+
if place = @placeholders[placeholder_key]
|
440
|
+
# result << "(path#{place} || )" # <- Defaults
|
441
|
+
with_defaults = ["(path#{place}"]
|
442
|
+
with_defaults << " || #{@defaults[placeholder_key].inspect}" if @defaults[placeholder_key]
|
443
|
+
with_defaults << ")"
|
444
|
+
result << with_defaults.join
|
445
|
+
else
|
446
|
+
raise GenerationError, "Placeholder not found while compiling routes: #{placeholder_key.inspect}. Add it to the conditions part of the route."
|
447
|
+
end
|
448
|
+
end
|
449
|
+
value = match.post_match
|
450
|
+
elsif match = JUST_BRACKETS.match(value)
|
451
|
+
result << match.pre_match.inspect unless match.pre_match.empty?
|
452
|
+
result << "path#{match[1]}"
|
453
|
+
value = match.post_match
|
454
|
+
else
|
455
|
+
result << value.inspect unless value.empty?
|
456
|
+
end
|
457
|
+
end
|
458
|
+
|
459
|
+
result.join(' + ').gsub("\\_", "_")
|
460
|
+
end
|
461
|
+
|
462
|
+
def condition_statements
|
463
|
+
statements = []
|
464
|
+
|
465
|
+
# First, let's build the conditions for the regular
|
466
|
+
conditions.each_pair do |key, value|
|
467
|
+
statements << case value
|
468
|
+
when Regexp
|
469
|
+
captures = ""
|
470
|
+
|
471
|
+
if (max = capturing_parentheses_count(value)) > 0
|
472
|
+
captures << (1..max).to_a.map { |n| "#{key}#{n}" }.join(", ")
|
473
|
+
captures << " = "
|
474
|
+
captures << (1..max).to_a.map { |n| "$#{n}" }.join(", ")
|
475
|
+
end
|
476
|
+
|
477
|
+
# Note: =~ is slightly faster than .match
|
478
|
+
%{(#{value.inspect} =~ cached_#{key} #{' && ((' + captures + ') || true)' unless captures.empty?})}
|
479
|
+
when Array
|
480
|
+
%{(#{arrays_to_regexps(value).inspect} =~ cached_#{key})}
|
481
|
+
else
|
482
|
+
%{(cached_#{key} == #{value.inspect})}
|
483
|
+
end
|
484
|
+
end
|
485
|
+
|
486
|
+
# The first one is special, so let's extract it
|
487
|
+
if first = @deferred_procs.first
|
488
|
+
deferred = ""
|
489
|
+
deferred << "(block_result = "
|
490
|
+
deferred << "request._process_block_return("
|
491
|
+
deferred << "#{first}.call(request, #{params_as_string})"
|
492
|
+
deferred << ")"
|
493
|
+
deferred << ")"
|
494
|
+
|
495
|
+
# Let's build the rest of them now
|
496
|
+
if @deferred_procs.length > 1
|
497
|
+
deferred << deferred_condition_statement(@deferred_procs[1..-1])
|
498
|
+
end
|
499
|
+
|
500
|
+
statements << deferred
|
501
|
+
end
|
502
|
+
|
503
|
+
statements
|
504
|
+
end
|
505
|
+
|
506
|
+
# (request.matched? || ((block_result = process(proc.call))))
|
507
|
+
def deferred_condition_statement(deferred)
|
508
|
+
if current = deferred.first
|
509
|
+
html = " && (request.matched? || ("
|
510
|
+
html << "(block_result = "
|
511
|
+
html << "request._process_block_return("
|
512
|
+
html << "#{current}.call(request, block_result)"
|
513
|
+
html << ")"
|
514
|
+
html << ")"
|
515
|
+
html << "#{deferred_condition_statement(deferred[1..-1])}"
|
516
|
+
html << "))"
|
517
|
+
end
|
518
|
+
end
|
519
|
+
|
520
|
+
def params_as_string
|
521
|
+
elements = params.keys.map do |k|
|
522
|
+
"#{k.inspect} => #{params[k]}"
|
523
|
+
end
|
524
|
+
"{#{elements.join(', ')}}"
|
525
|
+
end
|
526
|
+
|
527
|
+
# ---------- Utilities ----------
|
528
|
+
|
529
|
+
def arrays_to_regexps(condition)
|
530
|
+
return condition unless condition.is_a?(Array)
|
531
|
+
|
532
|
+
source = condition.map do |value|
|
533
|
+
value = if value.is_a?(Regexp)
|
534
|
+
value.source
|
535
|
+
else
|
536
|
+
"^#{Regexp.escape(value.to_s)}$"
|
537
|
+
end
|
538
|
+
"(?:#{value})"
|
539
|
+
end
|
540
|
+
|
541
|
+
Regexp.compile(source.join('|'))
|
542
|
+
end
|
543
|
+
|
544
|
+
def segment_level_to_s(segments)
|
545
|
+
(segments || []).inject('') do |str, seg|
|
546
|
+
str << case seg
|
547
|
+
when String then seg
|
548
|
+
when Symbol then ":#{seg}"
|
549
|
+
when Array then "(#{segment_level_to_s(seg)})"
|
550
|
+
end
|
551
|
+
end
|
552
|
+
end
|
553
|
+
|
554
|
+
def capturing_parentheses_count(regexp)
|
555
|
+
regexp = regexp.source if regexp.is_a?(Regexp)
|
556
|
+
regexp.scan(/(?!\\)[(](?!\?[#=:!>-imx])/).length
|
557
|
+
end
|
558
|
+
end
|
559
|
+
end
|
560
|
+
end
|