rack-rewrite-matches 1.3.3

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.
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.3.3
@@ -0,0 +1 @@
1
+ require 'rack/rewrite'
@@ -0,0 +1,28 @@
1
+ module Rack
2
+ autoload :RuleSet, 'rack/rewrite/rule'
3
+ autoload :VERSION, 'rack/rewrite/version'
4
+
5
+ # A rack middleware for defining and applying rewrite rules. In many cases you
6
+ # can get away with rack-rewrite instead of writing Apache mod_rewrite rules.
7
+ class Rewrite
8
+ def initialize(app, &rule_block)
9
+ @app = app
10
+ @rule_set = RuleSet.new
11
+ @rule_set.instance_eval(&rule_block) if block_given?
12
+ end
13
+
14
+ def call(env)
15
+ if matched_rule = find_first_matching_rule(env)
16
+ rack_response = matched_rule.apply!(env)
17
+ # Don't invoke the app if applying the rule returns a rack response
18
+ return rack_response unless rack_response === true
19
+ end
20
+ @app.call(env)
21
+ end
22
+
23
+ private
24
+ def find_first_matching_rule(env) #:nodoc:
25
+ @rule_set.rules.detect { |rule| rule.matches?(env) }
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,247 @@
1
+ require 'rack/mime'
2
+
3
+ module Rack
4
+ class Rewrite
5
+ class RuleSet
6
+ attr_reader :rules
7
+ def initialize #:nodoc:
8
+ @rules = []
9
+ end
10
+
11
+ protected
12
+ # We're explicitly defining private functions for our DSL rather than
13
+ # using method_missing
14
+
15
+ # Creates a rewrite rule that will simply rewrite the REQUEST_URI,
16
+ # PATH_INFO, and QUERY_STRING headers of the Rack environment. The
17
+ # user's browser will continue to show the initially requested URL.
18
+ #
19
+ # rewrite '/wiki/John_Trupiano', '/john'
20
+ # rewrite %r{/wiki/(\w+)_\w+}, '/$1'
21
+ # rewrite %r{(.*)}, '/maintenance.html', :if => lambda { File.exists?('maintenance.html') }
22
+ def rewrite(*args)
23
+ add_rule :rewrite, *args
24
+ end
25
+
26
+ # Creates a redirect rule that will send a 301 when matching.
27
+ #
28
+ # r301 '/wiki/John_Trupiano', '/john'
29
+ # r301 '/contact-us.php', '/contact-us'
30
+ #
31
+ # You can use +moved_permanently+ or just +p+ instead of +r301+.
32
+ def r301(*args)
33
+ add_rule :r301, *args
34
+ end
35
+
36
+ alias :moved_permanently :r301
37
+ alias :p :r301
38
+
39
+ # Creates a redirect rule that will send a 302 when matching.
40
+ #
41
+ # r302 '/wiki/John_Trupiano', '/john'
42
+ # r302 '/wiki/(.*)', 'http://www.google.com/?q=$1'
43
+ #
44
+ # You can use +found+ instead of +r302+.
45
+ def r302(*args)
46
+ add_rule :r302, *args
47
+ end
48
+
49
+ alias :found :r302
50
+
51
+ # Creates a redirect rule that will send a 303 when matching.
52
+ #
53
+ # r303 '/wiki/John_Trupiano', '/john'
54
+ # r303 '/wiki/(.*)', 'http://www.google.com/?q=$1'
55
+ #
56
+ # You can use +see_other+ instead of +r303+.
57
+ def r303(*args)
58
+ add_rule :r303, *args
59
+ end
60
+
61
+ alias :see_other :r303
62
+
63
+ # Creates a redirect rule that will send a 307 when matching.
64
+ #
65
+ # r307 '/wiki/John_Trupiano', '/john'
66
+ # r307 '/wiki/(.*)', 'http://www.google.com/?q=$1'
67
+ #
68
+ # You can use +temporary_redirect+ or +t+ instead of +r307+.
69
+ def r307(*args)
70
+ add_rule :r307, *args
71
+ end
72
+
73
+ alias :temporary_redirect :r307
74
+ alias :t :r307
75
+
76
+ # Creates a rule that will render a file if matched.
77
+ #
78
+ # send_file /*/, 'public/system/maintenance.html',
79
+ # :if => Proc.new { File.exists?('public/system/maintenance.html') }
80
+ def send_file(*args)
81
+ add_rule :send_file, *args
82
+ end
83
+
84
+ # Creates a rule that will render a file using x-send-file
85
+ # if matched.
86
+ #
87
+ # x_send_file /*/, 'public/system/maintenance.html',
88
+ # :if => Proc.new { File.exists?('public/system/maintenance.html') }
89
+ def x_send_file(*args)
90
+ add_rule :x_send_file, *args
91
+ end
92
+
93
+ private
94
+ def add_rule(method, from, to, options = {}) #:nodoc:
95
+ @rules << Rule.new(method.to_sym, from, to, options)
96
+ end
97
+
98
+ end
99
+
100
+ # TODO: Break rules into subclasses
101
+ class Rule #:nodoc:
102
+ attr_reader :rule_type, :from, :to, :options
103
+ def initialize(rule_type, from, to, options={}) #:nodoc:
104
+ @rule_type, @from, @to, @options = rule_type, from, to, normalize_options(options)
105
+ end
106
+
107
+ def matches?(rack_env) #:nodoc:
108
+ return false if options[:if].respond_to?(:call) && !options[:if].call(rack_env)
109
+ path = build_path_from_env(rack_env)
110
+
111
+ self.match_options?(rack_env) && string_matches?(path, self.from)
112
+ end
113
+
114
+ # Either (a) return a Rack response (short-circuiting the Rack stack), or
115
+ # (b) alter env as necessary and return true
116
+ def apply!(env) #:nodoc:
117
+ interpreted_to = self.interpret_to(env)
118
+ additional_headers = {}
119
+ if @options[:headers]
120
+ if @options[:headers].respond_to?(:call)
121
+ additional_headers = @options[:headers].call(*@matches) || {}
122
+ else
123
+ additional_headers = @options[:headers] || {}
124
+ end
125
+ end
126
+ status = @options[:status] || 200
127
+ case self.rule_type
128
+ when :r301
129
+ [301, {'Location' => interpreted_to, 'Content-Type' => Rack::Mime.mime_type(::File.extname(interpreted_to))}.merge!(additional_headers), [redirect_message(interpreted_to)]]
130
+ when :r302
131
+ [302, {'Location' => interpreted_to, 'Content-Type' => Rack::Mime.mime_type(::File.extname(interpreted_to))}.merge!(additional_headers), [redirect_message(interpreted_to)]]
132
+ when :r303
133
+ [303, {'Location' => interpreted_to, 'Content-Type' => Rack::Mime.mime_type(::File.extname(interpreted_to))}.merge!(additional_headers), [redirect_message(interpreted_to)]]
134
+ when :r307
135
+ [307, {'Location' => interpreted_to, 'Content-Type' => Rack::Mime.mime_type(::File.extname(interpreted_to))}.merge!(additional_headers), [redirect_message(interpreted_to)]]
136
+ when :rewrite
137
+ # return [200, {}, {:content => env.inspect}]
138
+ env['REQUEST_URI'] = interpreted_to
139
+ if q_index = interpreted_to.index('?')
140
+ env['PATH_INFO'] = interpreted_to[0..q_index-1]
141
+ env['QUERY_STRING'] = interpreted_to[q_index+1..interpreted_to.size-1]
142
+ else
143
+ env['PATH_INFO'] = interpreted_to
144
+ env['QUERY_STRING'] = ''
145
+ end
146
+ true
147
+ when :send_file
148
+ [status, {
149
+ 'Content-Length' => ::File.size(interpreted_to).to_s,
150
+ 'Content-Type' => Rack::Mime.mime_type(::File.extname(interpreted_to))
151
+ }.merge!(additional_headers), [::File.read(interpreted_to)]]
152
+ when :x_send_file
153
+ [status, {
154
+ 'X-Sendfile' => interpreted_to,
155
+ 'Content-Length' => ::File.size(interpreted_to).to_s,
156
+ 'Content-Type' => Rack::Mime.mime_type(::File.extname(interpreted_to))
157
+ }.merge!(additional_headers), []]
158
+ else
159
+ raise Exception.new("Unsupported rule: #{self.rule_type}")
160
+ end
161
+ end
162
+
163
+ protected
164
+ def interpret_to(env) #:nodoc:
165
+ path = build_path_from_env(env)
166
+ return interpret_to_proc(path, env) if self.to.is_a?(Proc)
167
+ return computed_to(path) if compute_to?(path)
168
+ self.to
169
+ end
170
+
171
+ def is_a_regexp?(obj)
172
+ obj.is_a?(Regexp) || (Object.const_defined?(:Oniguruma) && obj.is_a?(Oniguruma::ORegexp))
173
+ end
174
+
175
+ def match_options?(env, path = build_path_from_env(env))
176
+ matches = []
177
+ request = Rack::Request.new(env)
178
+
179
+ # negative matches
180
+ matches << !string_matches?(path, options[:not]) if options[:not]
181
+
182
+ # possitive matches
183
+ matches << string_matches?(env['REQUEST_METHOD'], options[:method]) if options[:method]
184
+ matches << string_matches?(request.host, options[:host]) if options[:host]
185
+ matches << string_matches?(request.scheme, options[:scheme]) if options[:scheme]
186
+
187
+ matches.all?
188
+ end
189
+
190
+ private
191
+ def normalize_options(arg)
192
+ options = arg.respond_to?(:call) ? {:if => arg} : arg
193
+ options.symbolize_keys! if options.respond_to? :symbolize_keys!
194
+ options.freeze
195
+ end
196
+
197
+ def interpret_to_proc(path, env)
198
+ return self.to.call(match(path), env) if self.from.is_a?(Regexp)
199
+ self.to.call(self.from, env)
200
+ end
201
+
202
+ def compute_to?(path)
203
+ self.is_a_regexp?(from) && match(path)
204
+ end
205
+
206
+ def match(path)
207
+ self.from.match(path)
208
+ end
209
+
210
+ def string_matches?(string, matcher)
211
+ if self.is_a_regexp?(matcher)
212
+ string =~ matcher
213
+ elsif matcher.is_a?(String)
214
+ string == matcher
215
+ elsif matcher.is_a?(Symbol)
216
+ string.downcase == matcher.to_s.downcase
217
+ else
218
+ false
219
+ end
220
+ end
221
+
222
+ def computed_to(path)
223
+ # is there a better way to do this?
224
+ computed_to = self.to.dup
225
+ computed_to.gsub!("$&",match(path).to_s)
226
+ @matches = []
227
+ (match(path).size - 1).downto(1) do |num|
228
+ m = match(path)[num].to_s
229
+ @matches << m
230
+ computed_to.gsub!("$#{num}", m)
231
+ end
232
+ return computed_to
233
+ end
234
+
235
+ # Construct the URL (without domain) from PATH_INFO and QUERY_STRING
236
+ def build_path_from_env(env)
237
+ path = env['PATH_INFO'] || ''
238
+ path += "?#{env['QUERY_STRING']}" unless env['QUERY_STRING'].nil? || env['QUERY_STRING'].empty?
239
+ path
240
+ end
241
+
242
+ def redirect_message(location)
243
+ %Q(Redirecting to <a href="#{location}">#{location}</a>)
244
+ end
245
+ end
246
+ end
247
+ end
@@ -0,0 +1,5 @@
1
+ module Rack
2
+ class Rewrite
3
+ VERSION = File.read File.join(File.expand_path("..", __FILE__), "..", "..", "..", "VERSION")
4
+ end
5
+ end
@@ -0,0 +1,54 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'rack-rewrite-matches'
3
+ s.version = File.read('VERSION')
4
+
5
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
6
+ s.authors = ["Travis Jeffery", "John Trupiano"]
7
+ s.date = Date.today.to_s
8
+ s.description = %q{A rack middleware for enforcing rewrite rules. In many cases you can get away with rack-rewrite instead of writing Apache mod_rewrite rules.}
9
+ s.email = %q{travisjeffery@gmail.com}
10
+ s.extra_rdoc_files = [
11
+ "LICENSE",
12
+ "History.rdoc",
13
+ ]
14
+ s.files = [
15
+ "History.rdoc",
16
+ "LICENSE",
17
+ "README.markdown",
18
+ "Rakefile",
19
+ "VERSION",
20
+ "Gemfile",
21
+ "lib/rack-rewrite.rb",
22
+ "lib/rack/rewrite.rb",
23
+ "lib/rack/rewrite/rule.rb",
24
+ "lib/rack/rewrite/version.rb",
25
+ "rack-rewrite.gemspec",
26
+ "test/geminstaller.yml",
27
+ "test/rack-rewrite_test.rb",
28
+ "test/rule_test.rb",
29
+ "test/test_helper.rb"
30
+ ]
31
+ s.homepage = %q{http://github.com/jtrupiano/rack-rewrite}
32
+ s.rdoc_options = ["--charset=UTF-8"]
33
+ s.require_paths = ["lib"]
34
+ s.rubyforge_project = %q{johntrupiano}
35
+ s.rubygems_version = %q{1.3.7}
36
+ s.summary = %q{A rack middleware for enforcing rewrite rules}
37
+ s.test_files = [
38
+ "test/rack-rewrite_test.rb",
39
+ "test/geminstaller.yml",
40
+ "test/rack-rewrite_test.rb",
41
+ "test/rule_test.rb",
42
+ "test/test_helper.rb"
43
+ ]
44
+
45
+ s.add_development_dependency 'bundler'
46
+ s.add_development_dependency 'shoulda', '~> 2.10.2'
47
+ s.add_development_dependency 'mocha', '~> 0.9.7'
48
+ s.add_development_dependency 'rack'
49
+
50
+ if s.respond_to? :specification_version then
51
+ s.specification_version = 3
52
+ end
53
+ end
54
+
@@ -0,0 +1,9 @@
1
+ gems:
2
+ - name: shoulda
3
+ version: '= 2.10.3'
4
+ - name: mocha
5
+ version: '= 0.9.8'
6
+ - name: rack
7
+ version: '= 1.1.0'
8
+ # - name: oniguruma
9
+ # version: '= 1.1.0'
@@ -0,0 +1,136 @@
1
+ require 'test_helper'
2
+
3
+ class RackRewriteTest < Test::Unit::TestCase
4
+
5
+ def call_args(overrides={})
6
+ {'REQUEST_URI' => '/wiki/Yair_Flicker', 'PATH_INFO' => '/wiki/Yair_Flicker', 'QUERY_STRING' => ''}.merge(overrides)
7
+ end
8
+
9
+ def call_args_no_req(overrides={})
10
+ {'PATH_INFO' => '/wiki/Yair_Flicker', 'QUERY_STRING' => ''}.merge(overrides)
11
+ end
12
+
13
+ def self.should_not_halt
14
+ should "not halt the rack chain" do
15
+ @app.expects(:call).once
16
+ @rack.call(call_args)
17
+ end
18
+ end
19
+
20
+ def self.should_be_a_rack_response
21
+ should 'be a rack a response' do
22
+ ret = @rack.call(call_args)
23
+ assert ret.is_a?(Array), 'return value is not a valid rack response'
24
+ assert_equal 3, ret.size, 'should have 3 arguments'
25
+ end
26
+ end
27
+
28
+ def self.should_halt
29
+ should "should halt the rack chain" do
30
+ @app.expects(:call).never
31
+ @rack.call(call_args)
32
+ end
33
+ should_be_a_rack_response
34
+ end
35
+
36
+ def self.should_location_redirect_to(location, code)
37
+ should "respond with http status code #{code}" do
38
+ ret = @rack.call(call_args)
39
+ assert_equal code, ret[0]
40
+ end
41
+ should 'send a location header' do
42
+ ret = @rack.call(call_args)
43
+ assert_equal location, ret[1]['Location'], 'Location is incorrect'
44
+ end
45
+ end
46
+
47
+ context 'Given an app' do
48
+ setup do
49
+ @app = Class.new { def call(app); true; end }.new
50
+ end
51
+
52
+ context 'when no rewrite rule matches' do
53
+ setup {
54
+ @rack = Rack::Rewrite.new(@app)
55
+ }
56
+ should_not_halt
57
+ end
58
+
59
+ [301, 302, 303, 307].each do |status|
60
+ context "when a #{status} rule matches" do
61
+ setup {
62
+ @rack = Rack::Rewrite.new(@app) do
63
+ send("r#{status}", '/wiki/Yair_Flicker', '/yair')
64
+ end
65
+ }
66
+ should_halt
67
+ should_location_redirect_to('/yair', status)
68
+ end
69
+ end
70
+
71
+ [[:p, 301], [:moved_permanently, 301], [:found, 302], [:see_other, 303], [:t, 307], [:temporary_redirect, 307]].each do |rule|
72
+ context "when a #{rule.first} rule matches" do
73
+ setup {
74
+ @rack = Rack::Rewrite.new(@app) do
75
+ send(rule.first, '/wiki/Yair_Flicker', '/yair')
76
+ end
77
+ }
78
+ should_halt
79
+ should_location_redirect_to('/yair', rule.last)
80
+ end
81
+ end
82
+
83
+ context 'when a rewrite rule matches' do
84
+ setup {
85
+ @rack = Rack::Rewrite.new(@app) do
86
+ rewrite '/wiki/Yair_Flicker', '/john'
87
+ end
88
+ }
89
+ should_not_halt
90
+
91
+ context 'the env' do
92
+ setup do
93
+ @initial_args = call_args.dup
94
+ @rack.call(@initial_args)
95
+ end
96
+
97
+ should "set PATH_INFO to '/john'" do
98
+ assert_equal '/john', @initial_args['PATH_INFO']
99
+ end
100
+ should "set REQUEST_URI to '/john'" do
101
+ assert_equal '/john', @initial_args['REQUEST_URI']
102
+ end
103
+ should "set QUERY_STRING to ''" do
104
+ assert_equal '', @initial_args['QUERY_STRING']
105
+ end
106
+ end
107
+ end
108
+
109
+ context 'when a rewrite rule matches but there is no REQUEST_URI set' do
110
+ setup {
111
+ @rack = Rack::Rewrite.new(@app) do
112
+ rewrite '/wiki/Yair_Flicker', '/john'
113
+ end
114
+ }
115
+ should_not_halt
116
+
117
+ context 'the env' do
118
+ setup do
119
+ @initial_args = call_args_no_req.dup
120
+ @rack.call(@initial_args)
121
+ end
122
+
123
+ should "set PATH_INFO to '/john'" do
124
+ assert_equal '/john', @initial_args['PATH_INFO']
125
+ end
126
+ should "set REQUEST_URI to '/john'" do
127
+ assert_equal '/john', @initial_args['REQUEST_URI']
128
+ end
129
+ should "set QUERY_STRING to ''" do
130
+ assert_equal '', @initial_args['QUERY_STRING']
131
+ end
132
+ end
133
+ end
134
+
135
+ end
136
+ end