# Lambdas with object instances require 'rubygems' require 'serverside' class Proc def proc_tag 'proc_' + object_id.to_s(36).sub('-', '_') end end class Object def const_tag 'C' + object_id.to_s(36).upcase.sub('-', '_') end end class RoutingConnection < ServerSide::Connection::Base # Converts a proc into a method, returning the method's name (as a symbol) def self.define_proc(&block) tag = block.proc_tag define_method(tag.to_sym, &block) unless instance_methods.include?(tag) tag.to_sym end # Converts a value into a local constant and freezes it. Returns the # constant's tag name def self.cache_constant(value) tag = value.const_tag class_eval "#{tag} = #{value.inspect}.freeze" rescue nil tag end # Sets the default handler for incoming requests. def self.route_default(&block) define_method(:default_handler, &block) end # Adds a routing rule. The normalized rule is a hash containing keys (acting # as instance variable names) with patterns as values. If the rule is not a # hash, it is normalized into a pattern checked against the request path. # Pattern values can also be arrays, in any array member is checked as a pattern. def self.route(rule, &block) @@rules ||= [] rule = {:path => rule} unless Hash === rule @@rules.unshift [rule, block] compile_rules end # Pattern for finding parameters inside patterns. Parameters are parts of the # pattern, which the routing pre-processor turns into sub-regexp that are # used to extract parameter values from the pattern. # # For example, matching '/controller/show' against '/controller/:action' will # give us @parameters[:action] #=> "show" ParamRegexp = /(?::([a-z]+))/ # Returns the condition part for the key and value specified. The key is the # name of an instance variable and the value is a pattern to match against. # If the pattern contains parameters (for example, /controller/:action,) the # method creates a lambda for extracting the parameter values. def self.condition_part(key, value) p_parse, p_count = '', 0 while (String === value) && (value =~ ParamRegexp) p_name = $1 p_count += 1 value.sub!(ParamRegexp, '(.*)') p_parse << "@parameters[:#{p_name}] = $#{p_count}\n" end cond = "(@#{key} =~ #{cache_constant(Regexp.new(value))})" if p_count == 0 cond else tag = define_proc(&eval( "lambda {if #{cond}\n#{p_parse}true\nelse\nfalse\nend}")) "(#{tag})" end end # Converts a rule into an if statement. All keys in the rule are matched # against their respective values. def self.rule_to_statement(rule, block) proc_tag = define_proc(&block) if Proc === rule cond = define_proc(&rule).to_s else cond = rule.to_a.map {|kv| if Array === kv[1] '(' + kv[1].map {|v| condition_part(kv[0], v)}.join('||') + ')' else condition_part(kv[0], kv[1]) end }.join('&&') end "return #{proc_tag} if #{cond}\n" end # Compiles all rules into a respond method that is invoked when a request # is received. def self.compile_rules code = @@rules.inject('lambda {') {|m, r| m << rule_to_statement(r[0], r[1])} code << 'default_handler}' define_method(:respond, &eval(code)) end def unhandled send_response(403, 'text', 'No handler found.') end alias_method :default_handler, :unhandled end module Response Text = 'text'.freeze Hello = 'Hello there!'.freeze Brilliant = 'Brilliant!'.freeze end RoutingConnection.route_default do send_response(200, Response::Text, 'Hey, I don\'t know what to do with this request.') end RoutingConnection.route(:path => '/hello$') do send_response(200, Response::Text, Response::Hello) end RoutingConnection.route(:path => '/paula') do send_response(200, Response::Text, Response::Brilliant) end RoutingConnection.route(['/controller/:action/:id', '/controller/:action']) do send_response(200, Response::Text, "Action: #{@parameters[:action]}\nId: #{@parameters[:id]}") end ServerSide::Server.new('0.0.0.0', 8000, RoutingConnection)