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