| 40 | | # Cache procs for future reference in eval statement |
|---|
| 41 | | class CachedProc |
|---|
| 42 | | @@index = 0 |
|---|
| 43 | | @@list = [] |
|---|
| 44 | | |
|---|
| 45 | | attr_accessor :cache, :index |
|---|
| 46 | | |
|---|
| 47 | | def initialize(cache) |
|---|
| 48 | | @cache, @index = cache, CachedProc.register(self) |
|---|
| 49 | | end |
|---|
| 50 | | |
|---|
| 51 | | # Make each CachedProc object embeddable within a string |
|---|
| 52 | | def to_s() "CachedProc[#{@index}].cache" end |
|---|
| 53 | | |
|---|
| 54 | | class << self |
|---|
| 55 | | def register(cached_code) |
|---|
| 56 | | CachedProc[@@index] = cached_code |
|---|
| 57 | | @@index += 1 |
|---|
| 58 | | @@index - 1 |
|---|
| 59 | | end |
|---|
| 60 | | def []=(index, code) @@list[index] = code end |
|---|
| 61 | | def [](index) @@list[index] end |
|---|
| 62 | | end |
|---|
| 63 | | end # CachedProc |
|---|
| 64 | | |
|---|
| 65 | | class Route |
|---|
| 66 | | attr_reader :conditions, :conditional_block |
|---|
| 67 | | attr_reader :params, :behavior, :segments, :index, :symbol |
|---|
| 68 | | |
|---|
| 69 | | def initialize(conditions, params, behavior = nil, &conditional_block) |
|---|
| 70 | | @conditions, @params, @behavior = conditions, params, behavior |
|---|
| 71 | | @conditional_block = conditional_block |
|---|
| 72 | | if @behavior && (path = @behavior.merged_original_conditions[:path]) |
|---|
| 73 | | @segments = segments_from_path(path) |
|---|
| 74 | | end |
|---|
| 75 | | end |
|---|
| 76 | | |
|---|
| 77 | | # Registers itself in the Router.routes array |
|---|
| 78 | | def register |
|---|
| 79 | | @index = Router.routes.size |
|---|
| 80 | | Router.routes << self |
|---|
| 81 | | self |
|---|
| 82 | | end |
|---|
| 83 | | |
|---|
| 84 | | # Get the symbols out of the segments array |
|---|
| 85 | | def symbol_segments |
|---|
| 86 | | segments.select{ |s| s.is_a?(Symbol) } |
|---|
| 87 | | end |
|---|
| 88 | | |
|---|
| 89 | | # Turn a path into string and symbol segments so it can be reconstructed, as in the |
|---|
| 90 | | # case of a named route. |
|---|
| 91 | | def segments_from_path(path) |
|---|
| 92 | | # Remove leading ^ and trailing $ from each segment (left-overs from regexp joining) |
|---|
| 93 | | strip = proc { |str| str.gsub(/^\^/, '').gsub(/\$$/, '') } |
|---|
| 94 | | segments = [] |
|---|
| 95 | | while match = (path.match(SEGMENT_REGEXP)) |
|---|
| 96 | | segments << strip[match.pre_match] unless match.pre_match.empty? |
|---|
| 97 | | segments << match[2].intern |
|---|
| 98 | | path = strip[match.post_match] |
|---|
| 99 | | end |
|---|
| 100 | | segments << strip[path] unless path.empty? |
|---|
| 101 | | segments |
|---|
| 102 | | end |
|---|
| 103 | | |
|---|
| 104 | | # Name this route |
|---|
| 105 | | def name(symbol = nil) |
|---|
| 106 | | @symbol = symbol |
|---|
| 107 | | Router.named_routes[@symbol] = self |
|---|
| 108 | | end |
|---|
| 109 | | |
|---|
| 110 | | def regexp? |
|---|
| 111 | | behavior.regexp? || behavior.send(:ancestors).any?{ |a| a.regexp? } |
|---|
| 112 | | end |
|---|
| 113 | | |
|---|
| 114 | | # Given a hash of +params+, returns a string using the stored route segments |
|---|
| 115 | | # for reconstruction of the URL. |
|---|
| 116 | | def generate(params = {}, fallback = {}) |
|---|
| 117 | | url = @segments.map do |segment| |
|---|
| 118 | | value = |
|---|
| 119 | | if segment.is_a? Symbol |
|---|
| 120 | | if params.is_a? Hash |
|---|
| 121 | | params[segment] || fallback[segment] |
|---|
| 122 | | else |
|---|
| 123 | | if params.respond_to?(segment) |
|---|
| 124 | | params.send(segment) |
|---|
| 125 | | else |
|---|
| 126 | | fallback[segment] |
|---|
| 127 | | end |
|---|
| 128 | | end |
|---|
| 129 | | elsif segment.respond_to? :to_s |
|---|
| 130 | | segment |
|---|
| 131 | | else |
|---|
| 132 | | raise "Segment type '#{segment.class}' can't be converted to a string" |
|---|
| 133 | | end |
|---|
| 134 | | (value.respond_to?(:to_param) ? value.to_param : value).to_s |
|---|
| 135 | | end.join |
|---|
| 136 | | end |
|---|
| 137 | | |
|---|
| 138 | | def if_conditions(params_as_string) |
|---|
| 139 | | cond = [] |
|---|
| 140 | | condition_string = proc do |key, value, regexp_string| |
|---|
| 141 | | max = Behavior.count_parens_up_to(value.source, value.source.size) |
|---|
| 142 | | captures = if max == 0 then "" else |
|---|
| 143 | | " && (" + |
|---|
| 144 | | (1..max).to_a.map{ |n| "#{key}#{n}" }.join(", ") + " = " + |
|---|
| 145 | | (1..max).to_a.map{ |n| "$#{n}"}.join(", ") + |
|---|
| 146 | | ")" |
|---|
| 147 | | end |
|---|
| 148 | | " (#{value.inspect} =~ #{regexp_string}" + captures + ")" |
|---|
| 149 | | end |
|---|
| 150 | | @conditions.each_pair do |key, value| |
|---|
| 151 | | |
|---|
| 152 | | # Note: =~ is slightly faster than .match |
|---|
| 153 | | cond << case key |
|---|
| 154 | | when :path then condition_string[key, value, "cached_path"] |
|---|
| 155 | | when :method then condition_string[key, value, "cached_method"] |
|---|
| 156 | | else condition_string[key, value, "request.#{key}.to_s"] |
|---|
| 157 | | end |
|---|
| 158 | | end |
|---|
| 159 | | if @conditional_block |
|---|
| 160 | | str = " # #{@conditional_block.inspect.scan(/@([^>]+)/).flatten.first}\n" |
|---|
| 161 | | str << " (block_result = #{CachedProc.new(@conditional_block)}.call(request, params.merge({#{params_as_string}})))" if @conditional_block |
|---|
| 162 | | cond << str |
|---|
| 163 | | end |
|---|
| 164 | | cond |
|---|
| 165 | | end |
|---|
| 166 | | |
|---|
| 167 | | def compile(first = false) |
|---|
| 168 | | code = "" |
|---|
| 169 | | default_params = {:action => "index"} |
|---|
| 170 | | get_value = proc do |key| |
|---|
| 171 | | if default_params.has_key?(key) && params[key][0] != ?" |
|---|
| 172 | | "#{params[key]} || \"#{default_params[key]}\"" |
|---|
| 173 | | else |
|---|
| 174 | | "#{params[key]}" |
|---|
| 175 | | end |
|---|
| 176 | | end |
|---|
| 177 | | params_as_string = params.keys.map{|k| "#{k.inspect} => #{get_value[k]}"}.join(", ") |
|---|
| 178 | | code << " els" unless first |
|---|
| 179 | | code << "if # #{@behavior.merged_original_conditions.inspect}\n " |
|---|
| 180 | | code << if_conditions(params_as_string).join(" &&\n ") << "\n" |
|---|
| 181 | | code << " # then\n" |
|---|
| 182 | | if @conditional_block |
|---|
| 183 | | code << " [#{@index.inspect}, block_result]\n" |
|---|
| 184 | | else |
|---|
| 185 | | code << " [#{@index.inspect}, {#{params_as_string}}]\n" |
|---|
| 186 | | end |
|---|
| 187 | | end |
|---|
| 188 | | |
|---|
| 189 | | def behavior_trace |
|---|
| 190 | | if @behavior |
|---|
| 191 | | puts @behavior.send(:ancestors).reverse.map{|a| a.inspect}.join("\n"); puts @behavior.inspect; puts |
|---|
| 192 | | else |
|---|
| 193 | | puts "No behavior to trace #{self}" |
|---|
| 194 | | end |
|---|
| 195 | | end |
|---|
| 196 | | end # Route |
|---|
| 197 | | |
|---|
| 198 | | # The Behavior class is an interim route-building class that ties pattern-matching +conditions+ to |
|---|
| 199 | | # output parameters, +params+. |
|---|
| 200 | | class Behavior |
|---|
| 201 | | attr_reader :placeholders, :conditions, :params |
|---|
| 202 | | attr_accessor :parent |
|---|
| 203 | | |
|---|
| 204 | | def initialize(conditions = {}, params = {}, parent = nil) |
|---|
| 205 | | @conditions, @params, @parent = conditions, {}, parent |
|---|
| 206 | | @placeholders = {} |
|---|
| 207 | | stringify_conditions |
|---|
| 208 | | copy_original_conditions |
|---|
| 209 | | deduce_placeholders |
|---|
| 210 | | # Must wait until after deducing placeholders to set @params |
|---|
| 211 | | @params.merge!(params) |
|---|
| 212 | | end |
|---|
| 213 | | |
|---|
| 214 | | def add(path, params = {}) |
|---|
| 215 | | match(path).to(params) |
|---|
| 216 | | end |
|---|
| 217 | | |
|---|
| 218 | | # Matches a +path+ and any number of optional request methods as conditions of a route. |
|---|
| 219 | | # Alternatively, +path+ can be a hash of conditions, in which case +conditions+ is ignored. |
|---|
| 220 | | # Yields 'self' so that sub-matching may occur. |
|---|
| 221 | | def match(path = '', conditions = {}, &block) |
|---|
| 222 | | if path.is_a? Hash |
|---|
| 223 | | conditions = path |
|---|
| 224 | | else |
|---|
| 225 | | conditions[:path] = path |
|---|
| 226 | | end |
|---|
| 227 | | match_without_path(conditions, &block) |
|---|
| 228 | | end |
|---|
| 229 | | |
|---|
| 230 | | def match_without_path(conditions = {}) |
|---|
| 231 | | new_behavior = self.class.new(conditions, {}, self) |
|---|
| 232 | | conditions.delete :path if ['', '^$'].include?(conditions[:path]) |
|---|
| 233 | | yield new_behavior if block_given? |
|---|
| 234 | | new_behavior |
|---|
| 235 | | end |
|---|
| 236 | | |
|---|
| 237 | | def to_route(params = {}, &conditional_block) |
|---|
| 238 | | @params.merge!(params) |
|---|
| 239 | | Route.new(compiled_conditions, compiled_params, self, &conditional_block) |
|---|
| 240 | | end |
|---|
| 241 | | |
|---|
| 242 | | # Creates a Route from one or more Behavior objects, unless a +block+ is passed in. |
|---|
| 243 | | # If a block is passed in, a Behavior object is yielded and further .to operations |
|---|
| 244 | | # may be called in the block. For example: |
|---|
| 245 | | # r.match('/:controller/:id).to(:action => 'show') |
|---|
| 246 | | # vs. |
|---|
| 247 | | # r.to(:controller => "simple") do |simple| |
|---|
| 248 | | # simple.match('/test').to(:action => 'index') |
|---|
| 249 | | # simple.match('/other').to(:action => 'other') |
|---|
| 250 | | # end |
|---|
| 251 | | def to(params = {}, &block) |
|---|
| | 82 | class RouteMatcher |
|---|
| | 83 | attr_reader :routes, :compiled_statement, :compiled_regexen |
|---|
| | 84 | |
|---|
| | 85 | def initialize |
|---|
| | 86 | @routes = Array.new |
|---|
| | 87 | # The final compiled lambda that gets used as the body of the route_request method. |
|---|
| | 88 | @compiled_statement = String.new |
|---|
| | 89 | @compiled_regexen = Array.new |
|---|
| | 90 | end |
|---|
| | 91 | |
|---|
| | 92 | # Add a route to be compiled. |
|---|
| | 93 | def add(*route) |
|---|
| | 94 | opt = Hash === route.last ? route.pop : {} |
|---|
| | 95 | if n = opt[:namespace] |
|---|
| | 96 | path = "/#{n}#{route[0]}" |
|---|
| | 97 | else |
|---|
| | 98 | path = route[0] |
|---|
| | 99 | end |
|---|
| | 100 | @routes << [path, opt] |
|---|
| | 101 | end |
|---|
| | 102 | |
|---|
| | 103 | def raw_add(*route) |
|---|
| | 104 | @routes << [route[0], (route[1]||{})] |
|---|
| | 105 | end |
|---|
| | 106 | |
|---|
| | 107 | # Build up a string that defines a lambda that does a case statement on |
|---|
| | 108 | # the PATH_INFO against each of the compiled routes in turn. First route |
|---|
| | 109 | # that matches wins. |
|---|
| | 110 | def compile_router |
|---|
| | 111 | router_lambda = @routes.inject("lambda{|path| \n sections={}\n case path\n") { |m,r| |
|---|
| | 112 | m << compile(r) |
|---|
| | 113 | } << " else\n return {:controller=>'Noroutefound', :action=>'noroute'}\n end\n}" |
|---|
| | 114 | @compiled_statement = router_lambda |
|---|
| | 115 | meta_def(:route_request, &eval(router_lambda)) |
|---|
| | 116 | end |
|---|
| | 117 | |
|---|
| | 118 | # Compile each individual route into a when /.../ component of the case |
|---|
| | 119 | # statement. Takes /:sections of the route def that start with : and |
|---|
| | 120 | # turns them into placeholders for whatever urls match against the route |
|---|
| | 121 | # in question. |
|---|
| | 122 | def compile(route) |
|---|
| | 123 | raise ArgumentError unless String === route[0] |
|---|
| | 124 | code, count = '', 0 |
|---|
| | 125 | while route[0] =~ Router::SECTION_REGEXP |
|---|
| | 126 | route[0] = route[0].dup |
|---|
| | 127 | name = $1 |
|---|
| | 128 | (name =~ /(\*+)(\w+)/) ? (flag = true; name = $2) : (flag = false) |
|---|
| | 129 | count += 1 |
|---|
| | 130 | if flag |
|---|
| | 131 | route[0].sub!(Router::SECTION_REGEXP, "([^,?]+)") |
|---|
| | 132 | else |
|---|
| | 133 | route[0].sub!(Router::SECTION_REGEXP, "([^\/,?]+)") |
|---|
| | 134 | end |
|---|
| | 135 | code << " sections[:#{name}] = $#{count}\n" |
|---|
| | 136 | end |
|---|
| | 137 | @compiled_regexen << Regexp.new(route[0]) |
|---|
| | 138 | index = @compiled_regexen.size - 1 |
|---|
| | 139 | condition = " when @compiled_regexen[#{index}] " |
|---|
| | 140 | statement = "#{condition}\n#{code}" |
|---|
| | 141 | statement << " return #{route[1].inspect}.update(sections)\n" |
|---|
| | 142 | statement |
|---|
| | 143 | end |
|---|
| | 144 | |
|---|
| | 145 | def generate_resources_routes(res,opt) |
|---|
| | 146 | with_options opt.merge(:controller => res.to_s, :rest => true) do |r| |
|---|
| | 147 | r.raw_add "#{opt[:prefix]}/#{res}/:id[;/]edit", :allowed => {:get => 'edit'} |
|---|
| | 148 | r.raw_add "#{opt[:prefix]}/#{res}/new[;/]:action", :allowed => {:get => 'new', :post => 'new', :put => 'new', :delete => 'new'} |
|---|
| | 149 | r.raw_add "#{opt[:prefix]}/#{res}/new" , :allowed => {:get => 'new'} |
|---|
| | 150 | if mem = opt[:member] |
|---|
| | 151 | mem.keys.sort_by{|x| "#{x}"}.each {|action| |
|---|
| | 152 | allowed = mem[action].injecting({}) {|h, verb| h[verb] = "#{action}"} |
|---|
| | 153 | r.raw_add "#{opt[:prefix]}/#{res}/:id[;/]+#{action}", :allowed => allowed |
|---|
| | 154 | } |
|---|
| | 155 | end |
|---|
| | 156 | if coll = opt[:collection] |
|---|
| | 157 | coll.keys.sort_by{|x| "#{x}"}.each {|action| |
|---|
| | 158 | allowed = coll[action].injecting({}) {|h, verb| h[verb] = "#{action}"} |
|---|
| | 159 | r.raw_add "#{opt[:prefix]}/#{res}[;/]#{action}", :allowed => allowed |
|---|
| | 160 | } |
|---|
| | 161 | end |
|---|
| | 162 | r.raw_add "#{opt[:prefix]}/#{res}/:id\\.:format", :allowed => {:get => 'show', :put => 'update', :delete => 'destroy'} |
|---|
| | 163 | r.raw_add "#{opt[:prefix]}/#{res}\\.:format", :allowed => {:get => 'index', :post => 'create'} |
|---|
| | 164 | r.raw_add "#{opt[:prefix]}/#{res}/:id", :allowed => {:get => 'show', :put => 'update', :delete => 'destroy'} |
|---|
| | 165 | r.raw_add "#{opt[:prefix]}/#{res}/?", :allowed => {:get => 'index', :post => 'create'} |
|---|
| | 166 | end |
|---|
| | 167 | end |
|---|
| | 168 | |
|---|
| | 169 | def generate_singleton_routes(res,opt) |
|---|
| | 170 | with_options opt.merge(:controller => res.to_s, :rest => true ) do |r| |
|---|
| | 171 | r.raw_add "#{opt[:prefix]}/#{res}[;/]edit", :allowed => {:get => 'edit'} |
|---|
| | 172 | r.raw_add "#{opt[:prefix]}/#{res}\\.:format", :allowed => {:get => 'show'} |
|---|
| | 173 | r.raw_add "#{opt[:prefix]}/#{res}/new" , :allowed => {:get => 'new'} |
|---|
| | 174 | r.raw_add "#{opt[:prefix]}/#{res}/?", :allowed => {:get => 'show', :post => 'create', :put => 'update', :delete => 'destroy'} |
|---|
| | 175 | end |
|---|
| | 176 | end |
|---|
| | 177 | |
|---|
| | 178 | def default_routes(opt={}) |
|---|
| | 179 | namespace = opt[:namespace] ? "/#{opt[:namespace]}" : "" |
|---|
| | 180 | with_options opt do |r| |
|---|
| | 181 | r.raw_add namespace + "/:controller/:action/:id\\.:format" |
|---|
| | 182 | r.raw_add namespace + "/:controller/:action/:id" |
|---|
| | 183 | r.raw_add namespace + "/:controller/:action\\.:format" |
|---|
| | 184 | r.raw_add namespace + "/:controller/:action" |
|---|
| | 185 | r.raw_add namespace + "/:controller\\.:format", :action => 'index' |
|---|
| | 186 | r.raw_add namespace + "/:controller", :action => 'index' |
|---|
| | 187 | end |
|---|
| | 188 | end |
|---|
| | 189 | end |
|---|
| | 190 | |
|---|
| | 191 | class RouteGenerator |
|---|
| | 192 | |
|---|
| | 193 | attr_accessor :paths |
|---|
| | 194 | |
|---|
| | 195 | def initialize |
|---|
| | 196 | @paths = {} |
|---|
| | 197 | end |
|---|
| | 198 | |
|---|
| | 199 | def add(name, path) |
|---|
| | 200 | @paths[name.to_sym] = path |
|---|
| | 201 | end |
|---|
| | 202 | |
|---|
| | 203 | def generate(name, *args) |
|---|
| | 204 | options = Hash === args.last ? args.pop : {} |
|---|
| | 205 | obj = args[0] |
|---|
| | 206 | options.each do |key, value| |
|---|
| | 207 | next unless value.respond_to?(:to_param) |
|---|
| | 208 | unless key.to_s =~ /_?id$/ |
|---|
| | 209 | old_key = key |
|---|
| | 210 | options[old_key] = value.to_param |
|---|
| | 211 | key = "#{key}_id".intern |
|---|
| | 212 | end |
|---|
| | 213 | options[key] = value.to_param |
|---|
| | 214 | end |
|---|
| | 215 | |
|---|
| | 216 | path = @paths[name].dup |
|---|
| | 217 | while path =~ Router::SECTION_REGEXP |
|---|
| | 218 | if obj.respond_to?($1) && ! obj.nil? |
|---|
| | 219 | path.sub!(Router::SECTION_REGEXP, obj.send($1).to_s) |
|---|
| | 220 | else |
|---|
| | 221 | path.sub!(Router::SECTION_REGEXP, options[$1.intern].to_s) |
|---|
| | 222 | end |
|---|
| | 223 | end |
|---|
| | 224 | if f = options[:format] |
|---|
| | 225 | "#{path}.#{f}" |
|---|
| | 226 | else |
|---|
| | 227 | path |
|---|
| | 228 | end |
|---|
| | 229 | end |
|---|
| | 230 | |
|---|
| | 231 | def generate_singleton_routes(res,opt) |
|---|
| | 232 | res = res.to_s |
|---|
| | 233 | if opt[:namespace] |
|---|
| | 234 | namespace = "#{opt[:namespace]}_" |
|---|
| | 235 | name = "/#{opt[:namespace]}" |
|---|
| | 236 | else |
|---|
| | 237 | namespace = '' |
|---|
| | 238 | name = '' |
|---|
| | 239 | end |
|---|
| | 240 | add namespace + "edit_#{res}", name + "#{opt[:prefix]}/#{res}/edit" |
|---|
| | 241 | add namespace + "new_#{res}", name + "#{opt[:prefix]}/#{res}/new" |
|---|
| | 242 | add namespace + res, name + "#{opt[:prefix]}/#{res}" |
|---|
| | 243 | end |
|---|
| | 244 | |
|---|
| | 245 | def generate_resources_routes(res,opt) |
|---|
| | 246 | res = res.to_s |
|---|
| | 247 | res_singular = res.singularize |
|---|
| | 248 | if opt[:namespace] |
|---|
| | 249 | namespace = "#{opt[:namespace]}_" |
|---|
| | 250 | name = "/#{opt[:namespace]}" |
|---|
| | 251 | else |
|---|
| | 252 | namespace = '' |
|---|
| | 253 | name = '' |
|---|
| | 254 | end |
|---|
| | 255 | add namespace + res, name + "#{opt[:prefix]}/#{res}" |
|---|
| | 256 | add namespace + res_singular, name + "#{opt[:prefix]}/#{res}/:id" |
|---|
| | 257 | add namespace + "new_#{res_singular}", name + "#{opt[:prefix]}/#{res}/new" |
|---|
| | 258 | add namespace + "custom_new_#{res_singular}", name + "#{opt[:prefix]}/#{res}/new/:action" |
|---|
| | 259 | add namespace + "edit_#{res_singular}", name + "#{opt[:prefix]}/#{res}/:id/edit" |
|---|
| | 260 | if mem = opt[:member] |
|---|
| | 261 | mem.keys.sort_by{|x| "#{x}"}.each {|action| |
|---|
| | 262 | add namespace + "#{action}_#{res_singular}", name + "#{opt[:prefix]}/#{res}/:id/#{action}" |
|---|
| | 263 | } |
|---|
| | 264 | end |
|---|
| | 265 | if coll = opt[:collection] |
|---|
| | 266 | coll.keys.sort_by{|x| "#{x}"}.each {|action| |
|---|
| | 267 | add namespace + "#{action}_#{res_singular}", name + "#{opt[:prefix]}/#{res}/#{action}" |
|---|
| | 268 | } |
|---|
| | 269 | end |
|---|
| | 270 | end |
|---|
| | 271 | end |
|---|
| | 272 | |
|---|
| | 273 | class Resource |
|---|
| | 274 | |
|---|
| | 275 | def initialize(resource, procs=[], opts={}) |
|---|
| | 276 | @resource, @procs, @opts = resource, procs, opts |
|---|
| | 277 | @procs << proc do |
|---|
| | 278 | [::Merb::Router.matcher, ::Merb::Router.generator].each {|r| r.generate_resources_routes(@resource, @opts) } |
|---|
| | 279 | end |
|---|
| | 280 | end |
|---|
| | 281 | |
|---|
| | 282 | def resources(res, opts={}) |
|---|
| | 283 | (opts[:prefix]||='') << "/#{@resource}/:#{@resource.to_s.singularize}_id" |
|---|
| | 284 | |
|---|
| | 285 | opts[:prefix] = @opts[:prefix] + opts[:prefix] |
|---|
| 253 | | new_behavior = self.class.new({}, params, self) |
|---|
| 254 | | yield new_behavior if block_given? |
|---|
| 255 | | new_behavior |
|---|
| 256 | | else |
|---|
| 257 | | to_route(params).register |
|---|
| 258 | | end |
|---|
| 259 | | end |
|---|
| 260 | | |
|---|
| 261 | | # Takes a block and stores it for defered conditional routes. The block takes the |
|---|
| 262 | | # +request+ object and the +params+ hash as parameters and should return a hash of params. |
|---|
| 263 | | # For example: |
|---|
| 264 | | # r.defer_to do |request, params} |
|---|
| 265 | | # params.merge(:controller => 'here', :action => 'there') if External.says_so? |
|---|
| 266 | | # end |
|---|
| 267 | | def defer_to(params = {}, &conditional_block) |
|---|
| 268 | | Router.routes << (route = to_route(params, &conditional_block)) |
|---|
| 269 | | route |
|---|
| 270 | | end |
|---|
| 271 | | |
|---|
| 272 | | def default_routes(params = {}, &block) |
|---|
| 273 | | match(%r[/:controller(/:action(/:id)?)?(\.:format)?]).to(params, &block) |
|---|
| 274 | | end |
|---|
| 275 | | |
|---|
| 276 | | def to_resources(params = {}, &block) |
|---|
| 277 | | many_behaviors_to(resources_behaviors, params, &block) |
|---|
| 278 | | end |
|---|
| 279 | | |
|---|
| 280 | | def resources(name, options = {}) |
|---|
| 281 | | # singular = name.singularize |
|---|
| 282 | | next_level = match("/#{name}") |
|---|
| 283 | | behaviors = [] |
|---|
| 284 | | |
|---|
| 285 | | # Add optional member actions |
|---|
| 286 | | options[:member].each_pair do |action, methods| |
|---|
| 287 | | conditions = {:path => %r[^/:id[/;]#{action}$], :method => /^(#{[methods].flatten.join("|")})$/} |
|---|
| 288 | | behaviors << Behavior.new(conditions, {:action => action.to_s}, next_level) |
|---|
| 289 | | end if options[:member] |
|---|
| 290 | | |
|---|
| 291 | | # Add optional collection actions |
|---|
| 292 | | options[:collection].each_pair do |action, methods| |
|---|
| 293 | | conditions = {:path => %r[^[/;]#{action}$], :method => /^(#{[methods].flatten.join("|")})$/} |
|---|
| 294 | | behaviors << Behavior.new(conditions, {:action => action.to_s}, next_level) |
|---|
| 295 | | end if options[:collection] |
|---|
| 296 | | |
|---|
| 297 | | controller = options[:controller] || merged_params[:controller] || name.to_s |
|---|
| 298 | | routes = many_behaviors_to(behaviors + next_level.send(:resources_behaviors), :controller => controller) |
|---|
| 299 | | |
|---|
| 300 | | singular = name.to_s.singularize |
|---|
| 301 | | |
|---|
| 302 | | # Add names to some routes |
|---|
| 303 | | next_level.match("").to_route.name(:"#{name}") |
|---|
| 304 | | next_level.match("/:id").to_route.name(:"#{singular}") |
|---|
| 305 | | next_level.match("/new").to_route.name(:"new_#{singular}") |
|---|
| 306 | | next_level.match("/:id/edit").to_route.name(:"edit_#{singular}") |
|---|
| 307 | | next_level.match("/:action/:id").to_route.name(:"custom_#{singular}") |
|---|
| 308 | | |
|---|
| 309 | | yield next_level.match("/:#{singular}_id") if block_given? |
|---|
| 310 | | routes |
|---|
| 311 | | end |
|---|
| 312 | | |
|---|
| 313 | | def to_resource(params = {}, &block) |
|---|
| 314 | | many_behaviors_to(resource_behaviors, params, &block) |
|---|
| 315 | | end |
|---|
| 316 | | |
|---|
| 317 | | def resource(name, options = {}) |
|---|
| 318 | | next_level = match("/#{name}") |
|---|
| 319 | | |
|---|
| 320 | | controller = options[:controller] || merged_params[:controller] || name.to_s |
|---|
| 321 | | routes = next_level.to_resource(:controller => controller) |
|---|
| 322 | | |
|---|
| 323 | | next_level.match("").to_route.name(:"#{name}") |
|---|
| 324 | | next_level.match("/new").to_route.name(:"new_#{name}") |
|---|
| 325 | | next_level.match("/edit").to_route.name(:"edit_#{name}") |
|---|
| 326 | | |
|---|
| 327 | | yield next_level if block_given? |
|---|
| 328 | | routes |
|---|
| 329 | | end |
|---|
| 330 | | |
|---|
| 331 | | def merged_original_conditions |
|---|
| 332 | | if parent.nil? |
|---|
| 333 | | @original_conditions |
|---|
| 334 | | else |
|---|
| 335 | | merged_so_far = parent.merged_original_conditions |
|---|
| 336 | | path = Behavior.concat_without_endcaps(merged_so_far[:path], @original_conditions[:path]) |
|---|
| 337 | | path ? |
|---|
| 338 | | merged_so_far.merge(@original_conditions).merge(:path => path) : |
|---|
| 339 | | merged_so_far.merge(@original_conditions) |
|---|
| 340 | | end |
|---|
| 341 | | end |
|---|
| 342 | | |
|---|
| 343 | | def merged_conditions |
|---|
| 344 | | if parent.nil? |
|---|
| 345 | | @conditions |
|---|
| 346 | | else |
|---|
| 347 | | merged_so_far = parent.merged_conditions |
|---|
| 348 | | path = Behavior.concat_without_endcaps(merged_so_far[:path], @conditions[:path]) |
|---|
| 349 | | path ? |
|---|
| 350 | | merged_so_far.merge(@conditions).merge(:path => path) : |
|---|
| 351 | | merged_so_far.merge(@conditions) |
|---|
| 352 | | end |
|---|
| 353 | | end |
|---|
| 354 | | |
|---|
| 355 | | def merged_params |
|---|
| 356 | | if parent.nil? |
|---|
| 357 | | @params |
|---|
| 358 | | else |
|---|
| 359 | | parent.merged_params.merge(@params) |
|---|
| 360 | | end |
|---|
| 361 | | end |
|---|
| 362 | | |
|---|
| 363 | | def merged_placeholders |
|---|
| 364 | | placeholders = {} |
|---|
| 365 | | (ancestors.reverse + [self]).each do |a| |
|---|
| 366 | | a.placeholders.each_pair do |key, pair| |
|---|
| 367 | | param, place = pair |
|---|
| 368 | | placeholders[key] = [param, place + (param == :path ? a.total_previous_captures : 0)] |
|---|
| 369 | | end |
|---|
| 370 | | end |
|---|
| 371 | | placeholders |
|---|
| 372 | | end |
|---|
| 373 | | |
|---|
| 374 | | def inspect |
|---|
| 375 | | "[captures: #{path_captures}, conditions: #{@original_conditions.inspect}, params: #{@params.inspect}, placeholders: #{@placeholders.inspect}]" |
|---|
| 376 | | end |
|---|
| 377 | | |
|---|
| 378 | | def regexp? |
|---|
| 379 | | @conditions_have_regexp |
|---|
| 380 | | end |
|---|
| 381 | | |
|---|
| 382 | | protected |
|---|
| 383 | | def resources_behaviors(parent = self) |
|---|
| 384 | | [ |
|---|
| 385 | | Behavior.new({:path => %r[^/?(\.:format)?$], :method => :get}, {:action => "index"}, parent), |
|---|
| 386 | | Behavior.new({:path => %r[^/index(\.:format)?$], :method => :get}, {:action => "index"}, parent), |
|---|
| 387 | | Behavior.new({:path => %r[^/new$], :method => :get}, {:action => "new"}, parent), |
|---|
| 388 | | Behavior.new({:path => %r[^/?(\.:format)?$], :method => :post}, {:action => "create"}, parent), |
|---|
| 389 | | Behavior.new({:path => %r[^/:id(\.:format)?$], :method => :get}, {:action => "show"}, parent), |
|---|
| 390 | | Behavior.new({:path => %r[^/:id[;/]edit$], :method => :get}, {:action => "edit"}, parent), |
|---|
| 391 | | Behavior.new({:path => %r[^/:id(\.:format)?$], :method => :put}, {:action => "update"}, parent), |
|---|
| 392 | | Behavior.new({:path => %r[^/:id(\.:format)?$], :method => :delete}, {:action => "destroy"}, parent) |
|---|
| 393 | | ] |
|---|
| 394 | | end |
|---|
| 395 | | |
|---|
| 396 | | def resource_behaviors(parent = self) |
|---|
| 397 | | [ |
|---|
| 398 | | Behavior.new({:path => %r{^[;/]new$}, :method => :get}, {:action => "new"}, parent), |
|---|
| 399 | | Behavior.new({:path => %r{^/?(\.:format)?$}, :method => :post}, {:action => "create"}, parent), |
|---|
| 400 | | Behavior.new({:path => %r{^/?(\.:format)?$}, :method => :get}, {:action => "show"}, parent), |
|---|
| 401 | | Behavior.new({:path => %r{^[;/]edit$}, :method => :get}, {:action => "edit"}, parent), |
|---|
| 402 | | Behavior.new({:path => %r{^/?(\.:format)?$}, :method => :put}, {:action => "update"}, parent), |
|---|
| 403 | | Behavior.new({:path => %r{^/?(\.:format)?$}, :method => :delete}, {:action => "destroy"}, parent) |
|---|
| 404 | | ] |
|---|
| 405 | | end |
|---|
| 406 | | |
|---|
| 407 | | # Creates a series of routes from an array of Behavior objects. |
|---|
| 408 | | # You can pass in optional +params+, and an optional block that will be |
|---|
| 409 | | # passed along to the #to method. |
|---|
| 410 | | def many_behaviors_to(behaviors, params = {}, &conditional_block) |
|---|
| 411 | | routes = [] |
|---|
| 412 | | behaviors.each { |b| routes << b.to(params, &conditional_block) } |
|---|
| 413 | | routes |
|---|
| 414 | | end |
|---|
| 415 | | |
|---|
| 416 | | # Convert conditions to regular expression string sources for consistency |
|---|
| 417 | | def stringify_conditions |
|---|
| 418 | | @conditions_have_regexp = false |
|---|
| 419 | | @conditions.each_pair do |key, value| |
|---|
| 420 | | # TODO: Other Regexp special chars |
|---|
| 421 | | @conditions[key] = case value |
|---|
| 422 | | when String then "^" + value.escape_regexp + "$" |
|---|
| 423 | | when Symbol then "^" + value.to_s + "$" |
|---|
| 424 | | when Regexp: |
|---|
| 425 | | @conditions_have_regexp = true |
|---|
| 426 | | value.source |
|---|
| 427 | | end |
|---|
| 428 | | end |
|---|
| 429 | | end |
|---|
| 430 | | |
|---|
| 431 | | def copy_original_conditions |
|---|
| 432 | | @original_conditions = {} |
|---|
| 433 | | @conditions.each_pair do |key, value| |
|---|
| 434 | | @original_conditions[key] = value.dup |
|---|
| 435 | | end |
|---|
| 436 | | @original_conditions |
|---|
| 437 | | end |
|---|
| 438 | | |
|---|
| 439 | | def deduce_placeholders |
|---|
| 440 | | @conditions.each_pair do |match_key, source| |
|---|
| 441 | | while match = SEGMENT_REGEXP.match(source) |
|---|
| 442 | | source.sub!(SEGMENT_REGEXP, PARENTHETICAL_SEGMENT_STRING) |
|---|
| 443 | | unless match[2] == ":" # No need to store anonymous place holders |
|---|
| 444 | | placeholder_key = match[2].intern |
|---|
| 445 | | @params[placeholder_key] = "#{match[1]}" |
|---|
| 446 | | @placeholders[placeholder_key] = [match_key, Behavior.count_parens_up_to(source, match.offset(1)[0])] |
|---|
| 447 | | end |
|---|
| 448 | | end |
|---|
| 449 | | end |
|---|
| 450 | | end |
|---|
| 451 | | |
|---|
| 452 | | def ancestors(list = []) |
|---|
| 453 | | if parent.nil? |
|---|
| 454 | | list |
|---|
| 455 | | else |
|---|
| 456 | | list.push parent |
|---|
| 457 | | parent.ancestors(list) |
|---|
| 458 | | list |
|---|
| 459 | | end |
|---|
| 460 | | end |
|---|
| 461 | | |
|---|
| 462 | | # Count the number of regexp captures in the :path condition |
|---|
| 463 | | def path_captures |
|---|
| 464 | | return 0 unless conditions[:path] |
|---|
| 465 | | Behavior.count_parens_up_to(conditions[:path], conditions[:path].size) |
|---|
| 466 | | end |
|---|
| 467 | | |
|---|
| 468 | | def total_previous_captures |
|---|
| 469 | | ancestors.map{|a| a.path_captures}.inject(0){|sum, n| sum + n} |
|---|
| 470 | | end |
|---|
| 471 | | |
|---|
| 472 | | # def merge_with_ancestors |
|---|
| 473 | | # self.class.new(merged_conditions, merged_params) |
|---|
| 474 | | # end |
|---|
| 475 | | |
|---|
| 476 | | def compiled_conditions(conditions = merged_conditions) |
|---|
| 477 | | compiled = {} |
|---|
| 478 | | conditions.each { |key, value| compiled[key] = Regexp.new(value) } |
|---|
| 479 | | compiled |
|---|
| 480 | | end |
|---|
| 481 | | |
|---|
| 482 | | # Compiles the params hash into 'eval'-able form. |
|---|
| 483 | | # For example: |
|---|
| 484 | | # @params = {:controller => "admin/:controller"} |
|---|
| 485 | | # Could become: |
|---|
| 486 | | # {:controller => "'admin/' + matches[:path][1]"} |
|---|
| 487 | | # |
|---|
| 488 | | def compiled_params(params = merged_params, placeholders = merged_placeholders) |
|---|
| 489 | | compiled = {} |
|---|
| 490 | | params.each_pair do |key, value| |
|---|
| 491 | | raise ArgumentError, "param value must be string (#{value.inspect})" unless value.is_a? String |
|---|
| 492 | | result = [] |
|---|
| 493 | | value = value.dup |
|---|
| 494 | | match = true |
|---|
| 495 | | while match |
|---|
| 496 | | if match = SEGMENT_REGEXP_WITH_BRACKETS.match(value) |
|---|
| 497 | | result << match.pre_match unless match.pre_match.empty? |
|---|
| 498 | | ph_key = match[1][1..-1].intern |
|---|
| 499 | | if match[2] # has brackets, e.g. :path[2] |
|---|
| 500 | | result << :"#{ph_key}#{match[3]}" |
|---|
| 501 | | else # no brackets, e.g. a named placeholder such as :controller |
|---|
| 502 | | if place = placeholders[ph_key] |
|---|
| 503 | | result << :"#{place[0]}#{place[1]}" |
|---|
| 504 | | else |
|---|
| 505 | | raise "Placeholder not found while compiling routes: :#{ph_key}" |
|---|
| 506 | | end |
|---|
| 507 | | end |
|---|
| 508 | | value = match.post_match |
|---|
| 509 | | elsif match = JUST_BRACKETS.match(value) |
|---|
| 510 | | # This is a reference to :path |
|---|
| 511 | | result << match.pre_match unless match.pre_match.empty? |
|---|
| 512 | | result << :"path#{match[1]}" |
|---|
| 513 | | value = match.post_match |
|---|
| 514 | | else |
|---|
| 515 | | result << value unless value.empty? |
|---|
| 516 | | end |
|---|
| 517 | | end |
|---|
| 518 | | compiled[key] = Behavior.array_to_code(result).gsub("\\_", "_") |
|---|
| 519 | | end |
|---|
| 520 | | compiled |
|---|
| 521 | | end |
|---|
| 522 | | |
|---|
| 523 | | public |
|---|
| 524 | | # Count the number of open parentheses in +string+, up to and including +pos+ |
|---|
| 525 | | def self.count_parens_up_to(string, pos) |
|---|
| 526 | | string[0..pos].gsub(/[^\(]/, "").size |
|---|
| 527 | | end |
|---|
| 528 | | |
|---|
| 529 | | # Concatenate strings and remove regexp end caps |
|---|
| 530 | | def self.concat_without_endcaps(string1, string2) |
|---|
| 531 | | return nil if !string1 and !string2 |
|---|
| 532 | | return string1 if string2.nil? |
|---|
| 533 | | return string2 if string1.nil? |
|---|
| 534 | | s1 = string1[-1] == ?$ ? string1[0..-2] : string1 |
|---|
| 535 | | s2 = string2[0] == ?^ ? string2[1..-1] : string2 |
|---|
| 536 | | s1 + s2 |
|---|
| 537 | | end |
|---|
| 538 | | |
|---|
| 539 | | # Join an array's elements into a string using " + " as a joiner, and |
|---|
| 540 | | # surround string elements in quotes. |
|---|
| 541 | | def self.array_to_code(arr) |
|---|
| 542 | | code = "" |
|---|
| 543 | | arr.each_with_index do |part, i| |
|---|
| 544 | | code << " + " if i > 0 |
|---|
| 545 | | case part |
|---|
| 546 | | when Symbol |
|---|
| 547 | | code << "#{part}" |
|---|
| 548 | | when String |
|---|
| 549 | | code << "\"" + part + "\"" |
|---|
| 550 | | else |
|---|
| 551 | | raise "Don't know how to compile array part: #{part.class} [#{i}]" |
|---|
| 552 | | end |
|---|
| 553 | | end |
|---|
| 554 | | code |
|---|
| 555 | | end |
|---|
| 556 | | end # Behavior |
|---|
| 557 | | |
|---|
| | 287 | yield self.class.new(res, @procs, opts) |
|---|
| | 288 | else |
|---|
| | 289 | @procs << proc do |
|---|
| | 290 | [::Merb::Router.matcher, ::Merb::Router.generator].each {|r| r.generate_resources_routes(res, opts) } |
|---|
| | 291 | end |
|---|
| | 292 | end |
|---|
| | 293 | end |
|---|
| | 294 | |
|---|
| | 295 | def resource(res, opts={}) |
|---|
| | 296 | (opts[:prefix]||='') << "/#{@resource}/:#{@resource.to_s.singularize}_id" |
|---|
| | 297 | |
|---|
| | 298 | opts[:prefix] = @opts[:prefix] + opts[:prefix] |
|---|
| | 299 | if block_given? |
|---|
| | 300 | yield self.class.new(res, @procs, opts) |
|---|
| | 301 | else |
|---|
| | 302 | @procs << proc do |
|---|
| | 303 | [::Merb::Router.matcher, ::Merb::Router.generator].each { |r| r.generate_singleton_routes(res, opts) } |
|---|
| | 304 | end |
|---|
| | 305 | end |
|---|
| | 306 | end |
|---|
| | 307 | end |
|---|