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.
Files changed (98) hide show
  1. data/CHANGELOG +992 -0
  2. data/CONTRIBUTORS +94 -0
  3. data/LICENSE +20 -0
  4. data/PUBLIC_CHANGELOG +142 -0
  5. data/README +21 -0
  6. data/Rakefile +458 -0
  7. data/TODO +0 -0
  8. data/bin/merb +11 -0
  9. data/bin/merb-specs +5 -0
  10. data/lib/merb-core.rb +598 -0
  11. data/lib/merb-core/autoload.rb +31 -0
  12. data/lib/merb-core/bootloader.rb +717 -0
  13. data/lib/merb-core/config.rb +305 -0
  14. data/lib/merb-core/constants.rb +45 -0
  15. data/lib/merb-core/controller/abstract_controller.rb +568 -0
  16. data/lib/merb-core/controller/exceptions.rb +315 -0
  17. data/lib/merb-core/controller/merb_controller.rb +256 -0
  18. data/lib/merb-core/controller/mime.rb +107 -0
  19. data/lib/merb-core/controller/mixins/authentication.rb +123 -0
  20. data/lib/merb-core/controller/mixins/conditional_get.rb +83 -0
  21. data/lib/merb-core/controller/mixins/controller.rb +319 -0
  22. data/lib/merb-core/controller/mixins/render.rb +513 -0
  23. data/lib/merb-core/controller/mixins/responder.rb +469 -0
  24. data/lib/merb-core/controller/template.rb +254 -0
  25. data/lib/merb-core/core_ext.rb +9 -0
  26. data/lib/merb-core/core_ext/hash.rb +7 -0
  27. data/lib/merb-core/core_ext/kernel.rb +340 -0
  28. data/lib/merb-core/dispatch/cookies.rb +130 -0
  29. data/lib/merb-core/dispatch/default_exception/default_exception.rb +93 -0
  30. data/lib/merb-core/dispatch/default_exception/views/_css.html.erb +198 -0
  31. data/lib/merb-core/dispatch/default_exception/views/_javascript.html.erb +73 -0
  32. data/lib/merb-core/dispatch/default_exception/views/index.html.erb +94 -0
  33. data/lib/merb-core/dispatch/dispatcher.rb +176 -0
  34. data/lib/merb-core/dispatch/request.rb +729 -0
  35. data/lib/merb-core/dispatch/router.rb +151 -0
  36. data/lib/merb-core/dispatch/router/behavior.rb +566 -0
  37. data/lib/merb-core/dispatch/router/cached_proc.rb +52 -0
  38. data/lib/merb-core/dispatch/router/resources.rb +191 -0
  39. data/lib/merb-core/dispatch/router/route.rb +511 -0
  40. data/lib/merb-core/dispatch/session.rb +222 -0
  41. data/lib/merb-core/dispatch/session/container.rb +74 -0
  42. data/lib/merb-core/dispatch/session/cookie.rb +173 -0
  43. data/lib/merb-core/dispatch/session/memcached.rb +68 -0
  44. data/lib/merb-core/dispatch/session/memory.rb +99 -0
  45. data/lib/merb-core/dispatch/session/store_container.rb +150 -0
  46. data/lib/merb-core/dispatch/worker.rb +28 -0
  47. data/lib/merb-core/gem_ext/erubis.rb +77 -0
  48. data/lib/merb-core/logger.rb +203 -0
  49. data/lib/merb-core/plugins.rb +67 -0
  50. data/lib/merb-core/rack.rb +25 -0
  51. data/lib/merb-core/rack/adapter.rb +44 -0
  52. data/lib/merb-core/rack/adapter/ebb.rb +25 -0
  53. data/lib/merb-core/rack/adapter/evented_mongrel.rb +26 -0
  54. data/lib/merb-core/rack/adapter/fcgi.rb +17 -0
  55. data/lib/merb-core/rack/adapter/irb.rb +118 -0
  56. data/lib/merb-core/rack/adapter/mongrel.rb +26 -0
  57. data/lib/merb-core/rack/adapter/runner.rb +28 -0
  58. data/lib/merb-core/rack/adapter/swiftiplied_mongrel.rb +26 -0
  59. data/lib/merb-core/rack/adapter/thin.rb +39 -0
  60. data/lib/merb-core/rack/adapter/thin_turbo.rb +24 -0
  61. data/lib/merb-core/rack/adapter/webrick.rb +36 -0
  62. data/lib/merb-core/rack/application.rb +32 -0
  63. data/lib/merb-core/rack/handler/mongrel.rb +97 -0
  64. data/lib/merb-core/rack/middleware.rb +20 -0
  65. data/lib/merb-core/rack/middleware/conditional_get.rb +29 -0
  66. data/lib/merb-core/rack/middleware/content_length.rb +18 -0
  67. data/lib/merb-core/rack/middleware/csrf.rb +73 -0
  68. data/lib/merb-core/rack/middleware/path_prefix.rb +31 -0
  69. data/lib/merb-core/rack/middleware/profiler.rb +19 -0
  70. data/lib/merb-core/rack/middleware/static.rb +45 -0
  71. data/lib/merb-core/rack/middleware/tracer.rb +20 -0
  72. data/lib/merb-core/server.rb +284 -0
  73. data/lib/merb-core/tasks/audit.rake +68 -0
  74. data/lib/merb-core/tasks/gem_management.rb +229 -0
  75. data/lib/merb-core/tasks/merb.rb +1 -0
  76. data/lib/merb-core/tasks/merb_rake_helper.rb +80 -0
  77. data/lib/merb-core/tasks/stats.rake +71 -0
  78. data/lib/merb-core/test.rb +11 -0
  79. data/lib/merb-core/test/helpers.rb +9 -0
  80. data/lib/merb-core/test/helpers/controller_helper.rb +8 -0
  81. data/lib/merb-core/test/helpers/multipart_request_helper.rb +175 -0
  82. data/lib/merb-core/test/helpers/request_helper.rb +393 -0
  83. data/lib/merb-core/test/helpers/route_helper.rb +39 -0
  84. data/lib/merb-core/test/helpers/view_helper.rb +121 -0
  85. data/lib/merb-core/test/matchers.rb +9 -0
  86. data/lib/merb-core/test/matchers/controller_matchers.rb +351 -0
  87. data/lib/merb-core/test/matchers/route_matchers.rb +137 -0
  88. data/lib/merb-core/test/matchers/view_matchers.rb +375 -0
  89. data/lib/merb-core/test/run_specs.rb +49 -0
  90. data/lib/merb-core/test/tasks/spectasks.rb +68 -0
  91. data/lib/merb-core/test/test_ext/hpricot.rb +32 -0
  92. data/lib/merb-core/test/test_ext/object.rb +14 -0
  93. data/lib/merb-core/test/test_ext/string.rb +14 -0
  94. data/lib/merb-core/vendor/facets.rb +2 -0
  95. data/lib/merb-core/vendor/facets/dictionary.rb +433 -0
  96. data/lib/merb-core/vendor/facets/inflect.rb +342 -0
  97. data/lib/merb-core/version.rb +3 -0
  98. 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