merb 0.3.7 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. data/README +25 -26
  2. data/Rakefile +48 -36
  3. data/app_generators/merb/USAGE +5 -0
  4. data/app_generators/merb/merb_generator.rb +107 -0
  5. data/app_generators/merb/templates/Rakefile +99 -0
  6. data/{examples/skeleton/dist → app_generators/merb/templates}/app/controllers/application.rb +1 -1
  7. data/app_generators/merb/templates/app/controllers/exceptions.rb +13 -0
  8. data/{examples/skeleton/dist → app_generators/merb/templates}/app/helpers/global_helper.rb +0 -0
  9. data/{examples/skeleton/dist/app/mailers → app_generators/merb/templates/app/mailers/views}/layout/application.erb +0 -0
  10. data/app_generators/merb/templates/app/views/exceptions/internal_server_error.html.erb +207 -0
  11. data/app_generators/merb/templates/app/views/exceptions/not_acceptable.html.erb +38 -0
  12. data/app_generators/merb/templates/app/views/exceptions/not_found.html.erb +40 -0
  13. data/app_generators/merb/templates/app/views/layout/application.html.erb +11 -0
  14. data/app_generators/merb/templates/config/boot.rb +11 -0
  15. data/app_generators/merb/templates/config/dependencies.rb +41 -0
  16. data/{examples/skeleton/dist/conf → app_generators/merb/templates/config}/environments/development.rb +0 -0
  17. data/{examples/skeleton/dist/conf → app_generators/merb/templates/config}/environments/production.rb +0 -0
  18. data/{examples/skeleton/dist/conf → app_generators/merb/templates/config}/environments/test.rb +0 -0
  19. data/app_generators/merb/templates/config/merb.yml +64 -0
  20. data/app_generators/merb/templates/config/merb_init.rb +16 -0
  21. data/app_generators/merb/templates/config/plugins.yml +1 -0
  22. data/app_generators/merb/templates/config/router.rb +32 -0
  23. data/{lib/merb/core_ext/merb_array.rb → app_generators/merb/templates/config/upload.conf} +0 -0
  24. data/app_generators/merb/templates/public/images/merb.jpg +0 -0
  25. data/app_generators/merb/templates/public/merb.fcgi +6 -0
  26. data/app_generators/merb/templates/public/stylesheets/master.css +119 -0
  27. data/app_generators/merb/templates/script/destroy +28 -0
  28. data/app_generators/merb/templates/script/generate +28 -0
  29. data/{examples/skeleton → app_generators/merb/templates}/script/stop_merb +0 -0
  30. data/app_generators/merb/templates/script/win_script.cmd +1 -0
  31. data/app_generators/merb/templates/spec/spec.opts +6 -0
  32. data/app_generators/merb/templates/spec/spec_helper.rb +10 -0
  33. data/app_generators/merb/templates/test/test_helper.rb +13 -0
  34. data/app_generators/merb_plugin/USAGE +5 -0
  35. data/app_generators/merb_plugin/merb_plugin_generator.rb +64 -0
  36. data/app_generators/merb_plugin/templates/LICENSE +20 -0
  37. data/app_generators/merb_plugin/templates/README +4 -0
  38. data/app_generators/merb_plugin/templates/Rakefile +35 -0
  39. data/app_generators/merb_plugin/templates/TODO +5 -0
  40. data/app_generators/merb_plugin/templates/merbtasks.rb +6 -0
  41. data/app_generators/merb_plugin/templates/sampleplugin.rb +10 -0
  42. data/app_generators/merb_plugin/templates/sampleplugin_spec.rb +7 -0
  43. data/app_generators/merb_plugin/templates/spec_helper.rb +2 -0
  44. data/bin/merb +1 -1
  45. data/lib/autotest/discover.rb +3 -0
  46. data/lib/autotest/merb_rspec.rb +79 -0
  47. data/lib/merb.rb +72 -93
  48. data/lib/merb/{merb_abstract_controller.rb → abstract_controller.rb} +28 -5
  49. data/lib/merb/caching/action_cache.rb +65 -29
  50. data/lib/merb/caching/fragment_cache.rb +9 -4
  51. data/lib/merb/caching/store/file_cache.rb +22 -14
  52. data/lib/merb/caching/store/memory_cache.rb +26 -8
  53. data/lib/merb/{merb_constants.rb → constants.rb} +9 -7
  54. data/lib/merb/controller.rb +178 -0
  55. data/lib/merb/core_ext.rb +13 -11
  56. data/lib/merb/core_ext/array.rb +0 -0
  57. data/lib/merb/core_ext/{merb_class.rb → class.rb} +0 -0
  58. data/lib/merb/core_ext/{merb_enumerable.rb → enumerable.rb} +0 -0
  59. data/lib/merb/core_ext/get_args.rb +52 -0
  60. data/lib/merb/core_ext/{merb_hash.rb → hash.rb} +40 -11
  61. data/lib/merb/core_ext/{merb_inflections.rb → inflections.rb} +0 -0
  62. data/lib/merb/core_ext/{merb_inflector.rb → inflector.rb} +1 -1
  63. data/lib/merb/core_ext/{merb_kernel.rb → kernel.rb} +56 -3
  64. data/lib/merb/core_ext/mash.rb +88 -0
  65. data/lib/merb/core_ext/{merb_module.rb → module.rb} +0 -0
  66. data/lib/merb/core_ext/{merb_numeric.rb → numeric.rb} +0 -0
  67. data/lib/merb/core_ext/{merb_object.rb → object.rb} +10 -47
  68. data/lib/merb/core_ext/string.rb +56 -0
  69. data/lib/merb/core_ext/{merb_symbol.rb → symbol.rb} +0 -0
  70. data/lib/merb/dispatcher.rb +109 -0
  71. data/lib/merb/{merb_drb_server.rb → drb_server.rb} +0 -0
  72. data/lib/merb/erubis_ext.rb +10 -0
  73. data/lib/merb/exceptions.rb +173 -0
  74. data/lib/merb/generators/merb_app/merb_app.rb +5 -25
  75. data/lib/merb/generators/merb_generator_helpers.rb +317 -0
  76. data/lib/merb/generators/merb_plugin.rb +19 -0
  77. data/lib/merb/logger.rb +65 -0
  78. data/lib/merb/{merb_mail_controller.rb → mail_controller.rb} +102 -49
  79. data/lib/merb/{merb_mailer.rb → mailer.rb} +31 -27
  80. data/lib/merb/mixins/{basic_authentication_mixin.rb → basic_authentication.rb} +3 -3
  81. data/lib/merb/mixins/{controller_mixin.rb → controller.rb} +131 -112
  82. data/lib/merb/mixins/{erubis_capture_mixin.rb → erubis_capture.rb} +12 -21
  83. data/lib/merb/mixins/{form_control_mixin.rb → form_control.rb} +6 -12
  84. data/lib/merb/mixins/render.rb +401 -0
  85. data/lib/merb/mixins/responder.rb +378 -0
  86. data/lib/merb/mixins/{view_context_mixin.rb → view_context.rb} +65 -10
  87. data/lib/merb/mixins/web_controller.rb +29 -0
  88. data/lib/merb/{merb_handler.rb → mongrel_handler.rb} +59 -38
  89. data/lib/merb/part_controller.rb +19 -0
  90. data/lib/merb/plugins.rb +16 -0
  91. data/lib/merb/rack_adapter.rb +37 -0
  92. data/lib/merb/request.rb +421 -0
  93. data/lib/merb/router.rb +576 -0
  94. data/lib/merb/{merb_server.rb → server.rb} +275 -71
  95. data/lib/merb/session.rb +10 -10
  96. data/lib/merb/session/cookie_store.rb +125 -0
  97. data/lib/merb/session/{merb_mem_cache_session.rb → mem_cache_session.rb} +22 -9
  98. data/lib/merb/session/{merb_memory_session.rb → memory_session.rb} +15 -11
  99. data/lib/merb/template.rb +35 -8
  100. data/lib/merb/template/erubis.rb +16 -10
  101. data/lib/merb/template/haml.rb +33 -20
  102. data/lib/merb/template/markaby.rb +16 -14
  103. data/lib/merb/template/xml_builder.rb +8 -4
  104. data/lib/merb/test/{merb_fake_request.rb → fake_request.rb} +11 -5
  105. data/lib/merb/test/helper.rb +31 -0
  106. data/lib/merb/test/hpricot.rb +136 -0
  107. data/lib/merb/test/{merb_multipart.rb → multipart.rb} +1 -1
  108. data/lib/merb/test/rspec.rb +93 -0
  109. data/lib/merb/{merb_upload_handler.rb → upload_handler.rb} +5 -6
  110. data/lib/merb/{merb_upload_progress.rb → upload_progress.rb} +1 -1
  111. data/lib/merb/{merb_view_context.rb → view_context.rb} +27 -42
  112. data/lib/{merb_tasks.rb → tasks.rb} +0 -0
  113. data/lib/tasks/merb.rake +21 -11
  114. data/merb_default_generators/model/USAGE +0 -0
  115. data/merb_default_generators/model/model_generator.rb +16 -0
  116. data/merb_default_generators/model/templates/new_model_template.erb +5 -0
  117. data/merb_default_generators/resource_controller/USAGE +0 -0
  118. data/merb_default_generators/resource_controller/resource_controller_generator.rb +26 -0
  119. data/merb_default_generators/resource_controller/templates/controller.rb +30 -0
  120. data/merb_default_generators/resource_controller/templates/edit.html.erb +1 -0
  121. data/merb_default_generators/resource_controller/templates/helper.rb +5 -0
  122. data/merb_default_generators/resource_controller/templates/index.html.erb +1 -0
  123. data/merb_default_generators/resource_controller/templates/new.html.erb +1 -0
  124. data/merb_default_generators/resource_controller/templates/show.html.erb +1 -0
  125. data/merb_generators/controller/USAGE +5 -0
  126. data/merb_generators/controller/controller_generator.rb +16 -0
  127. data/merb_generators/controller/templates/controller.rb +8 -0
  128. data/merb_generators/controller/templates/helper.rb +5 -0
  129. data/merb_generators/controller/templates/index.html.erb +3 -0
  130. data/merb_generators/resource/USAGE +0 -0
  131. data/merb_generators/resource/resource_generator.rb +60 -0
  132. data/rspec_generators/merb_controller_test/merb_controller_test_generator.rb +67 -0
  133. data/rspec_generators/merb_controller_test/templates/controller_spec.rb +8 -0
  134. data/rspec_generators/merb_controller_test/templates/edit_spec.rb +12 -0
  135. data/rspec_generators/merb_controller_test/templates/helper_spec.rb +5 -0
  136. data/rspec_generators/merb_controller_test/templates/index_spec.rb +12 -0
  137. data/rspec_generators/merb_controller_test/templates/new_spec.rb +12 -0
  138. data/rspec_generators/merb_controller_test/templates/show_spec.rb +5 -0
  139. data/rspec_generators/merb_model_test/merb_model_test_generator.rb +26 -0
  140. data/rspec_generators/merb_model_test/templates/model_spec_template.erb +7 -0
  141. data/script/destroy +14 -0
  142. data/script/generate +14 -0
  143. data/test_unit_generators/merb_controller_test/merb_controller_test_generator.rb +53 -0
  144. data/test_unit_generators/merb_controller_test/templates/functional_test.rb +17 -0
  145. data/test_unit_generators/merb_controller_test/templates/helper_test.rb +9 -0
  146. data/test_unit_generators/merb_model_test/merb_model_test_generator.rb +29 -0
  147. data/test_unit_generators/merb_model_test/templates/model_test_unit_template.erb +9 -0
  148. metadata +172 -94
  149. data/examples/README_EXAMPLES +0 -10
  150. data/examples/skeleton/Rakefile +0 -68
  151. data/examples/skeleton/dist/app/views/layout/application.herb +0 -12
  152. data/examples/skeleton/dist/conf/database.yml +0 -23
  153. data/examples/skeleton/dist/conf/merb.yml +0 -57
  154. data/examples/skeleton/dist/conf/merb_init.rb +0 -24
  155. data/examples/skeleton/dist/conf/router.rb +0 -22
  156. data/examples/skeleton/dist/conf/upload.conf +0 -5
  157. data/examples/skeleton/dist/schema/migrations/001_add_sessions_table.rb +0 -14
  158. data/examples/skeleton/script/new_migration +0 -21
  159. data/lib/merb/core_ext/merb_string.rb +0 -18
  160. data/lib/merb/merb_controller.rb +0 -206
  161. data/lib/merb/merb_dispatcher.rb +0 -87
  162. data/lib/merb/merb_exceptions.rb +0 -319
  163. data/lib/merb/merb_part_controller.rb +0 -42
  164. data/lib/merb/merb_plugins.rb +0 -293
  165. data/lib/merb/merb_request.rb +0 -165
  166. data/lib/merb/merb_router.rb +0 -309
  167. data/lib/merb/merb_yaml_store.rb +0 -31
  168. data/lib/merb/mixins/render_mixin.rb +0 -283
  169. data/lib/merb/mixins/responder_mixin.rb +0 -159
  170. data/lib/merb/session/merb_ar_session.rb +0 -131
  171. data/lib/merb/vendor/paginator/README.txt +0 -84
  172. data/lib/merb/vendor/paginator/paginator.rb +0 -124
  173. data/lib/tasks/db.rake +0 -55
@@ -0,0 +1,576 @@
1
+ module Merb
2
+
3
+ class Router
4
+ SEGMENT_REGEXP = /(:([a-z_][a-z0-9_]*|:))/
5
+ SEGMENT_REGEXP_WITH_BRACKETS = /(:[a-z_]+)(\[(\d+)\])?/
6
+ JUST_BRACKETS = /\[(\d+)\]/
7
+ PARENTHETICAL_SEGMENT_STRING = "([^\/.,;?]+)".freeze
8
+
9
+ @@named_routes = {}
10
+ @@routes = []
11
+ cattr_accessor :routes, :named_routes
12
+
13
+ class << self
14
+ def append(&block)
15
+ prepare(@@routes, [], &block)
16
+ end
17
+
18
+ def prepend(&block)
19
+ prepare([], @@routes, &block)
20
+ end
21
+
22
+ def prepare(first = [], last = [], &block)
23
+ @@routes = []
24
+ yield Behavior.new({}, {:action => 'index'}) # defaults
25
+ @@routes = first + @@routes + last
26
+ compile
27
+ end
28
+
29
+ def compiled_statement
30
+ @@compiled_statement = "lambda { |request, params|\n"
31
+ @@compiled_statement << " cached_path = request.path\n cached_method = request.method.to_s\n "
32
+ # @@compiled_statement << " puts cached_path.inspect; puts cached_method.inspect\n"
33
+ @@routes.each_with_index { |route, i| @@compiled_statement << route.compile(i == 0) }
34
+ @@compiled_statement << " else\n [nil, {}]\n"
35
+ @@compiled_statement << " end\n"
36
+ @@compiled_statement << "}"
37
+ end
38
+
39
+ def compile
40
+ meta_def(:match, &eval(compiled_statement))
41
+ end
42
+
43
+ def generate(name, params = {}, fallback = {})
44
+ raise "Named route not found: #{name}" unless @@named_routes.has_key? name
45
+ @@named_routes[name].generate(params, fallback)
46
+ end
47
+ end # self
48
+
49
+ # Cache procs for future reference in eval statement
50
+ class CachedProc
51
+ @@index = 0
52
+ @@list = []
53
+
54
+ attr_accessor :cache, :index
55
+
56
+ def initialize(cache)
57
+ @cache, @index = cache, CachedProc.register(self)
58
+ end
59
+
60
+ # Make each CachedProc object embeddable within a string
61
+ def to_s() "CachedProc[#{@index}].cache" end
62
+
63
+ class << self
64
+ def register(cached_code)
65
+ CachedProc[@@index] = cached_code
66
+ @@index += 1
67
+ @@index - 1
68
+ end
69
+ def []=(index, code) @@list[index] = code end
70
+ def [](index) @@list[index] end
71
+ end
72
+ end # CachedProc
73
+
74
+ class Route
75
+ attr_reader :conditions, :conditional_block
76
+ attr_reader :params, :behavior, :segments, :index, :symbol
77
+
78
+ def initialize(conditions, params, behavior = nil, &conditional_block)
79
+ @conditions, @params, @behavior = conditions, params, behavior
80
+ @conditional_block = conditional_block
81
+ if @behavior && (path = @behavior.merged_original_conditions[:path])
82
+ @segments = segments_from_path(path)
83
+ end
84
+ end
85
+
86
+ # Registers itself in the Router.routes array
87
+ def register
88
+ @index = Router.routes.size
89
+ Router.routes << self
90
+ self
91
+ end
92
+
93
+ # Get the symbols out of the segments array
94
+ def symbol_segments
95
+ segments.select{ |s| s.is_a?(Symbol) }
96
+ end
97
+
98
+ # Turn a path into string and symbol segments so it can be reconstructed, as in the
99
+ # case of a named route.
100
+ def segments_from_path(path)
101
+ # Remove leading ^ and trailing $ from each segment (left-overs from regexp joining)
102
+ strip = proc { |str| str.gsub(/^\^/, '').gsub(/\$$/, '') }
103
+ segments = []
104
+ while match = (path.match(SEGMENT_REGEXP))
105
+ segments << strip[match.pre_match] unless match.pre_match.empty?
106
+ segments << match[2].intern
107
+ path = strip[match.post_match]
108
+ end
109
+ segments << strip[path] unless path.empty?
110
+ segments
111
+ end
112
+
113
+ # Name this route
114
+ def name(symbol = nil)
115
+ @symbol = symbol
116
+ Router.named_routes[@symbol] = self
117
+ end
118
+
119
+ def regexp?
120
+ behavior.regexp? || behavior.send(:ancestors).any?{ |a| a.regexp? }
121
+ end
122
+
123
+ # Given a hash of +params+, returns a string using the stored route segments
124
+ # for reconstruction of the URL.
125
+ def generate(params = {}, fallback = {})
126
+ url = @segments.map do |segment|
127
+ value =
128
+ if segment.is_a? Symbol
129
+ if params.is_a? Hash
130
+ params[segment] || fallback[segment]
131
+ else
132
+ if params.respond_to?(segment)
133
+ params.send(segment)
134
+ else
135
+ fallback[segment]
136
+ end
137
+ end
138
+ elsif segment.respond_to? :to_s
139
+ segment
140
+ else
141
+ raise "Segment type '#{segment.class}' can't be converted to a string"
142
+ end
143
+ (value.respond_to?(:to_param) ? value.to_param : value).to_s
144
+ end.join
145
+ end
146
+
147
+ def if_conditions(params_as_string)
148
+ cond = []
149
+ condition_string = proc do |key, value, regexp_string|
150
+ max = Behavior.count_parens_up_to(value.source, value.source.size)
151
+ captures = if max == 0 then "" else
152
+ " && (" +
153
+ (1..max).to_a.map{ |n| "#{key}#{n}" }.join(", ") + " = " +
154
+ (1..max).to_a.map{ |n| "$#{n}"}.join(", ") +
155
+ ")"
156
+ end
157
+ " (#{value.inspect} =~ #{regexp_string}" + captures + ")"
158
+ end
159
+ @conditions.each_pair do |key, value|
160
+
161
+ # Note: =~ is slightly faster than .match
162
+ cond << case key
163
+ when :path then condition_string[key, value, "cached_path"]
164
+ when :method then condition_string[key, value, "cached_method"]
165
+ else condition_string[key, value, "request.#{key}.to_s"]
166
+ end
167
+ end
168
+ if @conditional_block
169
+ str = " # #{@conditional_block.inspect.scan(/@([^>]+)/).flatten.first}\n"
170
+ str << " (block_result = #{CachedProc.new(@conditional_block)}.call(request, params.merge({#{params_as_string}})))" if @conditional_block
171
+ cond << str
172
+ end
173
+ cond
174
+ end
175
+
176
+ def compile(first = false)
177
+ code = ""
178
+ default_params = {:action => "index"}
179
+ get_value = proc do |key|
180
+ if default_params.has_key?(key) && params[key][0] != ?"
181
+ "#{params[key]} || \"#{default_params[key]}\""
182
+ else
183
+ "#{params[key]}"
184
+ end
185
+ end
186
+ params_as_string = params.keys.map{|k| "#{k.inspect} => #{get_value[k]}"}.join(", ")
187
+ code << " els" unless first
188
+ code << "if # #{@behavior.merged_original_conditions.inspect}\n "
189
+ code << if_conditions(params_as_string).join(" &&\n ") << "\n"
190
+ code << " # then\n"
191
+ if @conditional_block
192
+ code << " [#{@index.inspect}, block_result]\n"
193
+ else
194
+ code << " [#{@index.inspect}, {#{params_as_string}}]\n"
195
+ end
196
+ end
197
+
198
+ def behavior_trace
199
+ if @behavior
200
+ puts @behavior.send(:ancestors).reverse.map{|a| a.inspect}.join("\n"); puts @behavior.inspect; puts
201
+ else
202
+ puts "No behavior to trace #{self}"
203
+ end
204
+ end
205
+ end # Route
206
+
207
+ # The Behavior class is an interim route-building class that ties pattern-matching +conditions+ to
208
+ # output parameters, +params+.
209
+ class Behavior
210
+ attr_reader :placeholders, :conditions, :params
211
+ attr_accessor :parent
212
+
213
+ def initialize(conditions = {}, params = {}, parent = nil)
214
+ @conditions, @params, @parent = conditions, {}, parent
215
+ @placeholders = {}
216
+ stringify_conditions
217
+ copy_original_conditions
218
+ deduce_placeholders
219
+ # Must wait until after deducing placeholders to set @params
220
+ @params.merge!(params)
221
+ end
222
+
223
+ def add(path, params = {})
224
+ match(path).to(params)
225
+ end
226
+
227
+ # Matches a +path+ and any number of optional request methods as conditions of a route.
228
+ # Alternatively, +path+ can be a hash of conditions, in which case +conditions+ is ignored.
229
+ # Yields 'self' so that sub-matching may occur.
230
+ def match(path = '', conditions = {}, &block)
231
+ if path.is_a? Hash
232
+ conditions = path
233
+ else
234
+ conditions[:path] = path
235
+ end
236
+ match_without_path(conditions, &block)
237
+ end
238
+
239
+ def match_without_path(conditions = {})
240
+ new_behavior = self.class.new(conditions, {}, self)
241
+ conditions.delete :path if ['', '^$'].include?(conditions[:path])
242
+ yield new_behavior if block_given?
243
+ new_behavior
244
+ end
245
+
246
+ def to_route(params = {}, &conditional_block)
247
+ @params.merge!(params)
248
+ Route.new(compiled_conditions, compiled_params, self, &conditional_block)
249
+ end
250
+
251
+ # Creates a Route from one or more Behavior objects, unless a +block+ is passed in.
252
+ # If a block is passed in, a Behavior object is yielded and further .to operations
253
+ # may be called in the block. For example:
254
+ # r.match('/:controller/:id).to(:action => 'show')
255
+ # vs.
256
+ # r.to(:controller => "simple") do |simple|
257
+ # simple.match('/test').to(:action => 'index')
258
+ # simple.match('/other').to(:action => 'other')
259
+ # end
260
+ def to(params = {}, &block)
261
+ if block_given?
262
+ new_behavior = self.class.new({}, params, self)
263
+ yield new_behavior if block_given?
264
+ new_behavior
265
+ else
266
+ to_route(params).register
267
+ end
268
+ end
269
+
270
+ # Takes a block and stores it for defered conditional routes. The block takes the
271
+ # +request+ object and the +params+ hash as parameters and should return a hash of params.
272
+ # For example:
273
+ # r.defer_to do |request, params|
274
+ # params.merge(:controller => 'here', :action => 'there') if External.says_so?
275
+ # end
276
+ def defer_to(params = {}, &conditional_block)
277
+ Router.routes << (route = to_route(params, &conditional_block))
278
+ route
279
+ end
280
+
281
+ def default_routes(params = {}, &block)
282
+ match(%r[/:controller(/:action(/:id)?)?(\.:format)?]).to(params, &block)
283
+ end
284
+
285
+ def to_resources(params = {}, &block)
286
+ many_behaviors_to(resources_behaviors, params, &block)
287
+ end
288
+
289
+ def resources(name, options = {})
290
+ # singular = name.singularize
291
+ next_level = match("/#{name}")
292
+ behaviors = []
293
+
294
+ singular = name.to_s.singularize
295
+
296
+ route_plural_name = (options[:name_prefix] || "") + name.to_s
297
+ route_singular_name = (options[:name_prefix] || "") + singular
298
+
299
+ # Add optional member actions
300
+ options[:member].each_pair do |action, methods|
301
+ conditions = {:path => %r[^/:id[/;]#{action}$], :method => /^(#{[methods].flatten.join("|")})$/}
302
+ behaviors << Behavior.new(conditions, {:action => action.to_s}, next_level)
303
+ next_level.match("/:id/#{action}").
304
+ to_route.name(:"#{action}_#{route_singular_name}")
305
+ end if options[:member]
306
+
307
+ # Add optional collection actions
308
+ options[:collection].each_pair do |action, methods|
309
+ conditions = {:path => %r[^[/;]#{action}$], :method => /^(#{[methods].flatten.join("|")})$/}
310
+ behaviors << Behavior.new(conditions, {:action => action.to_s}, next_level)
311
+ next_level.match("/#{action}").to_route.name(:"#{action}_#{route_plural_name}")
312
+ end if options[:collection]
313
+
314
+ controller = options[:controller] || merged_params[:controller] || name.to_s
315
+ routes = many_behaviors_to(behaviors + next_level.send(:resources_behaviors), :controller => controller)
316
+
317
+ # Add names to some routes
318
+ next_level.match("").to_route.name(:"#{route_plural_name}")
319
+ next_level.match("/:id").to_route.name(:"#{route_singular_name}")
320
+ next_level.match("/new").to_route.name(:"new_#{route_singular_name}")
321
+ next_level.match("/:id/edit").to_route.name(:"edit_#{route_singular_name}")
322
+ next_level.match("/:action/:id").to_route.name(:"custom_#{route_singular_name}")
323
+
324
+ yield next_level.match("/:#{singular}_id") if block_given?
325
+ routes
326
+ end
327
+
328
+ def to_resource(params = {}, &block)
329
+ many_behaviors_to(resource_behaviors, params, &block)
330
+ end
331
+
332
+ def resource(name, options = {})
333
+ next_level = match("/#{name}")
334
+
335
+ controller = options[:controller] || merged_params[:controller] || name.to_s
336
+ routes = next_level.to_resource(:controller => controller)
337
+
338
+ route_name = (options[:name_prefix] || "") + name.to_s
339
+
340
+ next_level.match("").to_route.name(:"#{route_name}")
341
+ next_level.match("/new").to_route.name(:"new_#{route_name}")
342
+ next_level.match("/edit").to_route.name(:"edit_#{route_name}")
343
+
344
+ yield next_level if block_given?
345
+ routes
346
+ end
347
+
348
+ def merged_original_conditions
349
+ if parent.nil?
350
+ @original_conditions
351
+ else
352
+ merged_so_far = parent.merged_original_conditions
353
+ path = Behavior.concat_without_endcaps(merged_so_far[:path], @original_conditions[:path])
354
+ path ?
355
+ merged_so_far.merge(@original_conditions).merge(:path => path) :
356
+ merged_so_far.merge(@original_conditions)
357
+ end
358
+ end
359
+
360
+ def merged_conditions
361
+ if parent.nil?
362
+ @conditions
363
+ else
364
+ merged_so_far = parent.merged_conditions
365
+ path = Behavior.concat_without_endcaps(merged_so_far[:path], @conditions[:path])
366
+ path ?
367
+ merged_so_far.merge(@conditions).merge(:path => path) :
368
+ merged_so_far.merge(@conditions)
369
+ end
370
+ end
371
+
372
+ def merged_params
373
+ if parent.nil?
374
+ @params
375
+ else
376
+ parent.merged_params.merge(@params)
377
+ end
378
+ end
379
+
380
+ def merged_placeholders
381
+ placeholders = {}
382
+ (ancestors.reverse + [self]).each do |a|
383
+ a.placeholders.each_pair do |key, pair|
384
+ param, place = pair
385
+ placeholders[key] = [param, place + (param == :path ? a.total_previous_captures : 0)]
386
+ end
387
+ end
388
+ placeholders
389
+ end
390
+
391
+ def inspect
392
+ "[captures: #{path_captures}, conditions: #{@original_conditions.inspect}, params: #{@params.inspect}, placeholders: #{@placeholders.inspect}]"
393
+ end
394
+
395
+ def regexp?
396
+ @conditions_have_regexp
397
+ end
398
+
399
+ protected
400
+ def resources_behaviors(parent = self)
401
+ [
402
+ Behavior.new({:path => %r[^/?(\.:format)?$], :method => :get}, {:action => "index"}, parent),
403
+ Behavior.new({:path => %r[^/index(\.:format)?$], :method => :get}, {:action => "index"}, parent),
404
+ Behavior.new({:path => %r[^/new$], :method => :get}, {:action => "new"}, parent),
405
+ Behavior.new({:path => %r[^/?(\.:format)?$], :method => :post}, {:action => "create"}, parent),
406
+ Behavior.new({:path => %r[^/:id(\.:format)?$], :method => :get}, {:action => "show"}, parent),
407
+ Behavior.new({:path => %r[^/:id[;/]edit$], :method => :get}, {:action => "edit"}, parent),
408
+ Behavior.new({:path => %r[^/:id(\.:format)?$], :method => :put}, {:action => "update"}, parent),
409
+ Behavior.new({:path => %r[^/:id(\.:format)?$], :method => :delete}, {:action => "destroy"}, parent)
410
+ ]
411
+ end
412
+
413
+ def resource_behaviors(parent = self)
414
+ [
415
+ Behavior.new({:path => %r{^[;/]new$}, :method => :get}, {:action => "new"}, parent),
416
+ Behavior.new({:path => %r{^/?(\.:format)?$}, :method => :post}, {:action => "create"}, parent),
417
+ Behavior.new({:path => %r{^/?(\.:format)?$}, :method => :get}, {:action => "show"}, parent),
418
+ Behavior.new({:path => %r{^[;/]edit$}, :method => :get}, {:action => "edit"}, parent),
419
+ Behavior.new({:path => %r{^/?(\.:format)?$}, :method => :put}, {:action => "update"}, parent),
420
+ Behavior.new({:path => %r{^/?(\.:format)?$}, :method => :delete}, {:action => "destroy"}, parent)
421
+ ]
422
+ end
423
+
424
+ # Creates a series of routes from an array of Behavior objects.
425
+ # You can pass in optional +params+, and an optional block that will be
426
+ # passed along to the #to method.
427
+ def many_behaviors_to(behaviors, params = {}, &conditional_block)
428
+ routes = []
429
+ behaviors.each { |b| routes << b.to(params, &conditional_block) }
430
+ routes
431
+ end
432
+
433
+ # Convert conditions to regular expression string sources for consistency
434
+ def stringify_conditions
435
+ @conditions_have_regexp = false
436
+ @conditions.each_pair do |key, value|
437
+ # TODO: Other Regexp special chars
438
+ @conditions[key] = case value
439
+ when String then "^" + value.escape_regexp + "$"
440
+ when Symbol then "^" + value.to_s + "$"
441
+ when Regexp:
442
+ @conditions_have_regexp = true
443
+ value.source
444
+ end
445
+ end
446
+ end
447
+
448
+ def copy_original_conditions
449
+ @original_conditions = {}
450
+ @conditions.each_pair do |key, value|
451
+ @original_conditions[key] = value.dup
452
+ end
453
+ @original_conditions
454
+ end
455
+
456
+ def deduce_placeholders
457
+ @conditions.each_pair do |match_key, source|
458
+ while match = SEGMENT_REGEXP.match(source)
459
+ source.sub!(SEGMENT_REGEXP, PARENTHETICAL_SEGMENT_STRING)
460
+ unless match[2] == ":" # No need to store anonymous place holders
461
+ placeholder_key = match[2].intern
462
+ @params[placeholder_key] = "#{match[1]}"
463
+ @placeholders[placeholder_key] = [match_key, Behavior.count_parens_up_to(source, match.offset(1)[0])]
464
+ end
465
+ end
466
+ end
467
+ end
468
+
469
+ def ancestors(list = [])
470
+ if parent.nil?
471
+ list
472
+ else
473
+ list.push parent
474
+ parent.ancestors(list)
475
+ list
476
+ end
477
+ end
478
+
479
+ # Count the number of regexp captures in the :path condition
480
+ def path_captures
481
+ return 0 unless conditions[:path]
482
+ Behavior.count_parens_up_to(conditions[:path], conditions[:path].size)
483
+ end
484
+
485
+ def total_previous_captures
486
+ ancestors.map{|a| a.path_captures}.inject(0){|sum, n| sum + n}
487
+ end
488
+
489
+ # def merge_with_ancestors
490
+ # self.class.new(merged_conditions, merged_params)
491
+ # end
492
+
493
+ def compiled_conditions(conditions = merged_conditions)
494
+ compiled = {}
495
+ conditions.each { |key, value| compiled[key] = Regexp.new(value) }
496
+ compiled
497
+ end
498
+
499
+ # Compiles the params hash into 'eval'-able form.
500
+ # For example:
501
+ # @params = {:controller => "admin/:controller"}
502
+ # Could become:
503
+ # {:controller => "'admin/' + matches[:path][1]"}
504
+ #
505
+ def compiled_params(params = merged_params, placeholders = merged_placeholders)
506
+ compiled = {}
507
+ params.each_pair do |key, value|
508
+ raise ArgumentError, "param value must be string (#{value.inspect})" unless value.is_a? String
509
+ result = []
510
+ value = value.dup
511
+ match = true
512
+ while match
513
+ if match = SEGMENT_REGEXP_WITH_BRACKETS.match(value)
514
+ result << match.pre_match unless match.pre_match.empty?
515
+ ph_key = match[1][1..-1].intern
516
+ if match[2] # has brackets, e.g. :path[2]
517
+ result << :"#{ph_key}#{match[3]}"
518
+ else # no brackets, e.g. a named placeholder such as :controller
519
+ if place = placeholders[ph_key]
520
+ result << :"#{place[0]}#{place[1]}"
521
+ else
522
+ raise "Placeholder not found while compiling routes: :#{ph_key}"
523
+ end
524
+ end
525
+ value = match.post_match
526
+ elsif match = JUST_BRACKETS.match(value)
527
+ # This is a reference to :path
528
+ result << match.pre_match unless match.pre_match.empty?
529
+ result << :"path#{match[1]}"
530
+ value = match.post_match
531
+ else
532
+ result << value unless value.empty?
533
+ end
534
+ end
535
+ compiled[key] = Behavior.array_to_code(result).gsub("\\_", "_")
536
+ end
537
+ compiled
538
+ end
539
+
540
+ public
541
+ # Count the number of open parentheses in +string+, up to and including +pos+
542
+ def self.count_parens_up_to(string, pos)
543
+ string[0..pos].gsub(/[^\(]/, "").size
544
+ end
545
+
546
+ # Concatenate strings and remove regexp end caps
547
+ def self.concat_without_endcaps(string1, string2)
548
+ return nil if !string1 and !string2
549
+ return string1 if string2.nil?
550
+ return string2 if string1.nil?
551
+ s1 = string1[-1] == ?$ ? string1[0..-2] : string1
552
+ s2 = string2[0] == ?^ ? string2[1..-1] : string2
553
+ s1 + s2
554
+ end
555
+
556
+ # Join an array's elements into a string using " + " as a joiner, and
557
+ # surround string elements in quotes.
558
+ def self.array_to_code(arr)
559
+ code = ""
560
+ arr.each_with_index do |part, i|
561
+ code << " + " if i > 0
562
+ case part
563
+ when Symbol
564
+ code << "#{part}"
565
+ when String
566
+ code << "\"" + part + "\""
567
+ else
568
+ raise "Don't know how to compile array part: #{part.class} [#{i}]"
569
+ end
570
+ end
571
+ code
572
+ end
573
+ end # Behavior
574
+
575
+ end
576
+ end