Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@jtrupiano
Created October 13, 2009 02:33
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save jtrupiano/208916 to your computer and use it in GitHub Desktop.
Save jtrupiano/208916 to your computer and use it in GitHub Desktop.
A rack middleware for defining and applying rewrite rules. In many cases you can get away with rack-rewrite instead of writing Apache mod_rewrite rules.
# This is actually available as a gem: gem install rack-rewrite
# Full source code including tests is on github: http://github.com/jtrupiano/rack-rewrite
module Rack
# A rack middleware for defining and applying rewrite rules. In many cases you
# can get away with rack-rewrite instead of writing Apache mod_rewrite rules.
class Rewrite
def initialize(app, &rule_block)
@app = app
@rule_set = RuleSet.new
@rule_set.instance_eval(&rule_block) if block_given?
end
def call(env)
if matched_rule = find_first_matching_rule(env)
rack_response = matched_rule.apply!(env)
# Don't invoke the app if applying the rule returns a rack response
return rack_response unless rack_response === true
end
@app.call(env)
end
private
def find_first_matching_rule(env) #:nodoc:
@rule_set.rules.detect { |rule| rule.matches?(env['REQUEST_URI']) }
end
class RuleSet
attr_reader :rules
def initialize #:nodoc:
@rules = []
end
protected
# We're explicitly defining private functions for our DSL rather than
# using method_missing
# Creates a rewrite rule that will simply rewrite the REQUEST_URI,
# PATH_INFO, and QUERYSTRING headers of the Rack environment. The
# user's browser will continue to show the initially requested URL.
#
# rewrite '/wiki/John_Trupiano', '/john'
# rewrite %r{/wiki/(\w+)_\w+}, '/$1'
def rewrite(from, to)
@rules << Rule.new(:rewrite, from, to)
end
# Creates a redirect rule that will send a 301 when matching.
#
# r301 '/wiki/John_Trupiano', '/john'
# r301 '/contact-us.php', '/contact-us'
def r301(from, to)
@rules << Rule.new(:r301, from, to)
end
# Creates a redirect rule that will send a 302 when matching.
#
# r302 '/wiki/John_Trupiano', '/john'
# r302 '/wiki/(.*)', 'http://www.google.com/?q=$1'
def r302(from, to)
@rules << Rule.new(:r302, from, to)
end
end
# TODO: Break rules into subclasses
class Rule #:nodoc:
attr_reader :rule_type, :from, :to
def initialize(rule_type, from, to) #:nodoc:
@rule_type, @from, @to = rule_type, from, to
end
def matches?(path) #:nodoc:
case self.from
when Regexp
path =~ self.from
when String
path == self.from
else
false
end
end
# Either (a) return a Rack response (short-circuiting the Rack stack), or
# (b) alter env as necessary and return true
def apply!(env) #:nodoc:
interpreted_to = self.send(:interpret_to, env['REQUEST_URI'])
case self.rule_type
when :r301
[301, {'Location' => interpreted_to}, ['Redirecting...']]
when :r302
[302, {'Location' => interpreted_to}, ['Redirecting...']]
when :rewrite
# return [200, {}, {:content => env.inspect}]
env['REQUEST_URI'] = interpreted_to
if q_index = interpreted_to.index('?')
env['PATH_INFO'] = interpreted_to[0..q_index-1]
env['QUERYSTRING'] = interpreted_to[q_index+1..interpreted_to.size-1]
else
env['PATH_INFO'] = interpreted_to
env['QUERYSTRING'] = ''
end
true
else
raise Exception.new("Unsupported rule: #{self.rule_type}")
end
end
private
# is there a better way to do this?
def interpret_to(path) #:nodoc:
if self.from.is_a?(Regexp)
if from_match_data = self.from.match(path)
computed_to = self.to.dup
(from_match_data.size - 1).downto(1) do |num|
computed_to.gsub!("$#{num}", from_match_data[num])
end
return computed_to
end
end
self.to
end
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment