merb 0.3.7 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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