mholling-subdomain_routes 0.1.0
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/LICENSE +20 -0
- data/README.textile +323 -0
- data/Rakefile +57 -0
- data/VERSION.yml +4 -0
- data/lib/subdomain_routes/config.rb +6 -0
- data/lib/subdomain_routes/mailer.rb +17 -0
- data/lib/subdomain_routes/mapper.rb +40 -0
- data/lib/subdomain_routes/proc_set.rb +33 -0
- data/lib/subdomain_routes/request.rb +12 -0
- data/lib/subdomain_routes/resources.rb +49 -0
- data/lib/subdomain_routes/routes.rb +66 -0
- data/lib/subdomain_routes/split_host.rb +35 -0
- data/lib/subdomain_routes/url_writer.rb +86 -0
- data/lib/subdomain_routes/validations.rb +28 -0
- data/lib/subdomain_routes.rb +14 -0
- data/spec/extraction_spec.rb +83 -0
- data/spec/mailer_spec.rb +15 -0
- data/spec/proc_set_spec.rb +61 -0
- data/spec/recognition_spec.rb +89 -0
- data/spec/resources_spec.rb +38 -0
- data/spec/routes_spec.rb +248 -0
- data/spec/spec_helper.rb +68 -0
- data/spec/url_writing_spec.rb +177 -0
- data/spec/validations_spec.rb +12 -0
- metadata +94 -0
@@ -0,0 +1,35 @@
|
|
1
|
+
module SubdomainRoutes
|
2
|
+
class TooManySubdomains < StandardError
|
3
|
+
# TODO: should this just be an ActionController::RoutingError instead? Any benefit to having a separate error type?
|
4
|
+
# OK, keep the special codes, but catch and re-case them in UrlWriter.
|
5
|
+
# (The errors are also raised in extract_request_environment...)
|
6
|
+
end
|
7
|
+
|
8
|
+
module SplitHost
|
9
|
+
private
|
10
|
+
|
11
|
+
def split_host(host)
|
12
|
+
raise HostNotSupplied, "No host supplied!" if host.blank?
|
13
|
+
raise HostNotSupplied, "Can't set subdomain for an IP address!" if host =~ /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/
|
14
|
+
parts = host.split('.')
|
15
|
+
if Config.domain_length
|
16
|
+
domain_parts = [ ]
|
17
|
+
Config.domain_length.times { domain_parts.unshift parts.pop }
|
18
|
+
if parts.size > 1
|
19
|
+
raise TooManySubdomains, "Multiple subdomains found: #{parts.join('.')}. (Have you set SubdomainRoutes::Config.domain_length correctly?)"
|
20
|
+
end
|
21
|
+
[ parts.pop.to_s, domain_parts.join('.') ]
|
22
|
+
else
|
23
|
+
[ parts.shift.to_s, parts.join('.') ]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def domain_for_host(host)
|
28
|
+
split_host(host).last
|
29
|
+
end
|
30
|
+
|
31
|
+
def subdomain_for_host(host)
|
32
|
+
split_host(host).first
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module SubdomainRoutes
|
2
|
+
class HostNotSupplied < StandardError
|
3
|
+
# TODO: should this just be an ActionController::RoutingError instead? Any benefit to having a separate error type?
|
4
|
+
end
|
5
|
+
|
6
|
+
module RewriteSubdomainOptions
|
7
|
+
include SplitHost
|
8
|
+
|
9
|
+
def subdomain_procs
|
10
|
+
ActionController::Routing::Routes.subdomain_procs
|
11
|
+
end
|
12
|
+
|
13
|
+
def rewrite_subdomain_options(options, host)
|
14
|
+
if subdomains = options[:subdomains]
|
15
|
+
old_subdomain, domain = split_host(host)
|
16
|
+
new_subdomain = options.has_key?(:subdomain) ? options[:subdomain].to_s.downcase : old_subdomain
|
17
|
+
begin
|
18
|
+
case subdomains
|
19
|
+
when Array
|
20
|
+
unless subdomains.include? new_subdomain
|
21
|
+
if subdomains.size > 1 || options.has_key?(:subdomain)
|
22
|
+
raise ActionController::RoutingError, "expected subdomain in #{subdomains.inspect}, instead got subdomain #{new_subdomain.inspect}"
|
23
|
+
else
|
24
|
+
new_subdomain = subdomains.first
|
25
|
+
end
|
26
|
+
end
|
27
|
+
when Hash
|
28
|
+
unless subdomain_procs.recognize(subdomains[:proc], new_subdomain) && (new_subdomain.blank? || SubdomainRoutes.valid_subdomain?(new_subdomain))
|
29
|
+
raise ActionController::RoutingError, "subdomain #{new_subdomain.inspect} is invalid"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
rescue ActionController::RoutingError => e
|
33
|
+
raise ActionController::RoutingError, "Route for #{options.inspect} failed to generate (#{e.message})"
|
34
|
+
end
|
35
|
+
unless new_subdomain == old_subdomain
|
36
|
+
options[:only_path] = false
|
37
|
+
options[:host] = [ new_subdomain, domain ].reject(&:blank?).join('.')
|
38
|
+
end
|
39
|
+
options.delete(:subdomains)
|
40
|
+
options.delete(:subdomain)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
module UrlWriter
|
46
|
+
include RewriteSubdomainOptions
|
47
|
+
|
48
|
+
def self.included(base)
|
49
|
+
base.alias_method_chain :url_for, :subdomains
|
50
|
+
end
|
51
|
+
|
52
|
+
def url_for_with_subdomains(options)
|
53
|
+
host = options[:host] || default_url_options[:host]
|
54
|
+
if options[:subdomains] && host.blank?
|
55
|
+
raise HostNotSupplied, "Missing host to link to! Please provide :host parameter or set default_url_options[:host]"
|
56
|
+
end
|
57
|
+
rewrite_subdomain_options(options, host)
|
58
|
+
url_for_without_subdomains(options)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
module UrlRewriter
|
63
|
+
include RewriteSubdomainOptions
|
64
|
+
|
65
|
+
def self.included(base)
|
66
|
+
base.alias_method_chain :rewrite, :subdomains
|
67
|
+
base::RESERVED_OPTIONS << :subdomain
|
68
|
+
end
|
69
|
+
|
70
|
+
def rewrite_with_subdomains(options)
|
71
|
+
host = options[:host] || @request.host
|
72
|
+
if options[:subdomains] && host.blank?
|
73
|
+
raise HostNotSupplied, "Missing host to link to!"
|
74
|
+
end
|
75
|
+
rewrite_subdomain_options(options, host)
|
76
|
+
rewrite_without_subdomains(options)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
ActionController::UrlWriter.send :include, SubdomainRoutes::UrlWriter
|
82
|
+
ActionController::UrlRewriter.send :include, SubdomainRoutes::UrlRewriter
|
83
|
+
|
84
|
+
if defined? ActionMailer::Base
|
85
|
+
ActionMailer::Base.send :include, ActionController::UrlWriter
|
86
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module SubdomainRoutes
|
2
|
+
def self.valid_subdomain?(subdomain)
|
3
|
+
subdomain.to_s =~ /^([a-z]|[a-z][a-z0-9]|[a-z]([a-z0-9]|\-[a-z0-9])*)$/
|
4
|
+
# # TODO: could we use URI::parse instead?:
|
5
|
+
# URI.parse "http://#{subdomain}.example.com"
|
6
|
+
# rescue URI::InvalidURIError
|
7
|
+
# false
|
8
|
+
end
|
9
|
+
|
10
|
+
module Validations
|
11
|
+
module ClassMethods
|
12
|
+
def validates_subdomain_format_of(*attr_names)
|
13
|
+
configuration = { :on => :save }
|
14
|
+
configuration.update(attr_names.extract_options!)
|
15
|
+
|
16
|
+
validates_each(attr_names, configuration) do |record, attr_name, value|
|
17
|
+
unless SubdomainRoutes.valid_subdomain?(value)
|
18
|
+
record.errors.add(attr_name, :not_a_valid_subdomain, :default => configuration[:message], :value => value)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
if defined? ActiveRecord::Base
|
27
|
+
ActiveRecord::Base.send :extend, SubdomainRoutes::Validations::ClassMethods
|
28
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'active_support'
|
3
|
+
require 'action_controller'
|
4
|
+
|
5
|
+
require 'subdomain_routes/config'
|
6
|
+
require 'subdomain_routes/split_host'
|
7
|
+
require 'subdomain_routes/mapper'
|
8
|
+
require 'subdomain_routes/proc_set'
|
9
|
+
require 'subdomain_routes/routes'
|
10
|
+
require 'subdomain_routes/resources'
|
11
|
+
require 'subdomain_routes/url_writer'
|
12
|
+
require 'subdomain_routes/request'
|
13
|
+
require 'subdomain_routes/validations'
|
14
|
+
require 'subdomain_routes/mailer'
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe "subdomain extraction" do
|
4
|
+
before(:each) do
|
5
|
+
ActionController::Routing::Routes.clear!
|
6
|
+
end
|
7
|
+
|
8
|
+
include SubdomainRoutes::SplitHost
|
9
|
+
|
10
|
+
it "should add a subdomain method to requests" do
|
11
|
+
request = ActionController::TestRequest.new
|
12
|
+
request.host = "admin.example.com"
|
13
|
+
request.subdomain.should == "admin"
|
14
|
+
end
|
15
|
+
|
16
|
+
describe "configuration" do
|
17
|
+
it "should have a default domain length of nil" do
|
18
|
+
SubdomainRoutes::Config.domain_length.should be_nil
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should raise an error if no host is supplied" do
|
23
|
+
lambda { split_host(nil) }.should raise_error(SubdomainRoutes::HostNotSupplied)
|
24
|
+
end
|
25
|
+
|
26
|
+
context "when the domain length is not set" do
|
27
|
+
before(:each) do
|
28
|
+
SubdomainRoutes::Config.stub!(:domain_length).and_return(nil)
|
29
|
+
end
|
30
|
+
|
31
|
+
it "should always find a subdomain and a domain" do
|
32
|
+
split_host("example.com").should == [ "example", "com" ]
|
33
|
+
split_host("www.example.com").should == [ "www", "example.com" ]
|
34
|
+
split_host("blah.www.example.com").should == [ "blah", "www.example.com" ]
|
35
|
+
end
|
36
|
+
|
37
|
+
it "should raise an error if a nil subdomain is mapped" do
|
38
|
+
lambda { map_subdomain(nil) }.should raise_error(ArgumentError)
|
39
|
+
lambda { map_subdomain(nil, :www) }.should raise_error(ArgumentError)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
context "when domain length is set" do
|
44
|
+
before(:each) do
|
45
|
+
SubdomainRoutes::Config.stub!(:domain_length).and_return(2)
|
46
|
+
end
|
47
|
+
|
48
|
+
it "should find the domain" do
|
49
|
+
domain_for_host("www.example.com").should == "example.com"
|
50
|
+
end
|
51
|
+
|
52
|
+
it "should find the subdomain when it is present" do
|
53
|
+
subdomain_for_host("www.example.com").should == "www"
|
54
|
+
end
|
55
|
+
|
56
|
+
it "should return an empty string when subdomain is absent" do
|
57
|
+
subdomain_for_host("example.com").should == ""
|
58
|
+
end
|
59
|
+
|
60
|
+
context "and multi-level subdomains are found" do
|
61
|
+
before(:each) do
|
62
|
+
@host = "blah.www.example.com"
|
63
|
+
end
|
64
|
+
|
65
|
+
it "should raise an error" do
|
66
|
+
lambda { subdomain_for_host(@host) }.should raise_error(SubdomainRoutes::TooManySubdomains)
|
67
|
+
end
|
68
|
+
|
69
|
+
it "should raise an error when generating URLs" do
|
70
|
+
map_subdomain(:admin) { |admin| admin.resources :users }
|
71
|
+
with_host(@host) do
|
72
|
+
lambda { admin_users_path }.should raise_error(SubdomainRoutes::TooManySubdomains)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
it "should raise an error when recognising URLs" do
|
77
|
+
request = ActionController::TestRequest.new
|
78
|
+
request.host = @host
|
79
|
+
lambda { recognize_path(request) }.should raise_error(SubdomainRoutes::TooManySubdomains)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
data/spec/mailer_spec.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
describe "ActionMailer::Base" do
|
2
|
+
before(:each) do
|
3
|
+
@mailer_class = Class.new(ActionMailer::Base) { def test; body "test"; end }
|
4
|
+
end
|
5
|
+
it "should flush the subdomain procs cache each time a mailer is created" do
|
6
|
+
ActionController::Routing::Routes.subdomain_procs.should_receive(:flush!)
|
7
|
+
@mailer_class.create_test
|
8
|
+
end
|
9
|
+
|
10
|
+
it "should not flush the subdomain procs cache if SubdomainRoutes::Config.manual_flush is set" do
|
11
|
+
SubdomainRoutes::Config.stub!(:manual_flush).and_return(true)
|
12
|
+
ActionController::Routing::Routes.subdomain_procs.should_not_receive(:flush!)
|
13
|
+
@mailer_class.create_test
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe "subdomain proc set" do
|
4
|
+
before(:each) do
|
5
|
+
@proc_set = SubdomainRoutes::ProcSet.new
|
6
|
+
end
|
7
|
+
|
8
|
+
context "with a subdomain recognizer" do
|
9
|
+
before(:each) do
|
10
|
+
@city_block = lambda { |city| } # this recognizer block will be stubbed out
|
11
|
+
@proc_set.add_recognizer(:city, &@city_block)
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should indicate what subdomain it recognizes" do
|
15
|
+
@proc_set.recognizes?(:city).should be_true
|
16
|
+
@proc_set.recognizes?(:user).should be_false
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should run the recognizer" do
|
20
|
+
@city_block.should_receive(:call).with("boston").and_return(true)
|
21
|
+
@proc_set.recognize(:city, "boston").should == true
|
22
|
+
end
|
23
|
+
|
24
|
+
it "should raise any error that the recognizer raises" do
|
25
|
+
error = StandardError.new
|
26
|
+
@city_block.stub!(:call).and_raise(error)
|
27
|
+
lambda { @proc_set.recognize(:city, "hobart") }.should raise_error { |e| e.should == error }
|
28
|
+
end
|
29
|
+
|
30
|
+
it "should return nil if it can't recognize the name" do
|
31
|
+
@proc_set.recognize(:user, "mholling").should be_nil
|
32
|
+
end
|
33
|
+
|
34
|
+
it "should call the recognize proc only once for multiple recognitions" do
|
35
|
+
@city_block.should_receive(:call).with("boston").once
|
36
|
+
2.times { @proc_set.recognize(:city, "boston") }
|
37
|
+
end
|
38
|
+
|
39
|
+
it "should return the cached value according to the arguments" do
|
40
|
+
@city_block.should_receive(:call).with("boston").once.and_return(true)
|
41
|
+
@city_block.should_receive(:call).with("hobart").once.and_return(false)
|
42
|
+
2.times do
|
43
|
+
@proc_set.recognize(:city, "boston").should == true
|
44
|
+
@proc_set.recognize(:city, "hobart").should == false
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
it "should call the recognize proc again once the cache is flushed" do
|
49
|
+
@city_block.should_receive(:call).with("boston").twice
|
50
|
+
5.times { @proc_set.recognize(:city, "boston") }
|
51
|
+
@proc_set.flush!
|
52
|
+
5.times { @proc_set.recognize(:city, "boston") }
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
it "can be cleared of its procs" do
|
57
|
+
@proc_set.add_recognizer(:city) { |city| }
|
58
|
+
@proc_set.clear!
|
59
|
+
@proc_set.recognizes?(:city).should be_false
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe "subdomain route recognition" do
|
4
|
+
before(:each) do
|
5
|
+
ActionController::Routing::Routes.clear!
|
6
|
+
SubdomainRoutes::Config.stub!(:domain_length).and_return(2)
|
7
|
+
@request = ActionController::TestRequest.new
|
8
|
+
@request.host = "www.example.com"
|
9
|
+
@request.request_uri = "/items/2"
|
10
|
+
end
|
11
|
+
|
12
|
+
it "should add the host's subdomain to the request environment" do
|
13
|
+
request_environment = ActionController::Routing::Routes.extract_request_environment(@request)
|
14
|
+
request_environment[:subdomain].should == "www"
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should add an empty subdomain to the request environment if the host has no subdomain" do
|
18
|
+
@request.host = "example.com"
|
19
|
+
request_environment = ActionController::Routing::Routes.extract_request_environment(@request)
|
20
|
+
request_environment[:subdomain].should == ""
|
21
|
+
end
|
22
|
+
|
23
|
+
context "for a single specified subdomain" do
|
24
|
+
it "should recognise a route if the subdomain matches" do
|
25
|
+
map_subdomain(:www) { |www| www.resources :items }
|
26
|
+
params = recognize_path(@request)
|
27
|
+
params[:controller].should == "www/items"
|
28
|
+
params[:action].should == "show"
|
29
|
+
params[:id].should == "2"
|
30
|
+
end
|
31
|
+
|
32
|
+
it "should not recognise a route if the subdomain doesn't match" do
|
33
|
+
map_subdomain("admin") { |admin| admin.resources :items }
|
34
|
+
lambda { recognize_path(@request) }.should raise_error(ActionController::RoutingError)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
context "for a nil or blank subdomain" do
|
39
|
+
[ nil, "" ].each do |subdomain|
|
40
|
+
it "should recognise a route if there is no subdomain present" do
|
41
|
+
map_subdomain(subdomain) { |map| map.resources :items }
|
42
|
+
@request.host = "example.com"
|
43
|
+
lambda { recognize_path(@request) }.should_not raise_error
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
context "for multiple specified subdomains" do
|
49
|
+
it "should recognise a route if the subdomain matches" do
|
50
|
+
map_subdomain(:www, :admin, :name => nil) { |map| map.resources :items }
|
51
|
+
lambda { recognize_path(@request) }.should_not raise_error
|
52
|
+
end
|
53
|
+
|
54
|
+
it "should not recognise a route if the subdomain doesn't match" do
|
55
|
+
map_subdomain(:support, :admin, :name => nil) { |map| map.resources :items }
|
56
|
+
lambda { recognize_path(@request) }.should raise_error(ActionController::RoutingError)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
context "for a :proc subdomain" do
|
61
|
+
before(:each) do
|
62
|
+
@user_block = lambda { |user| } # this block will be stubbed
|
63
|
+
ActionController::Routing::Routes.recognize_subdomain(:user, &@user_block)
|
64
|
+
map_subdomain(:proc => :user) { |user| user.resources :articles }
|
65
|
+
@request.request_uri = "/articles"
|
66
|
+
@request.host = "mholling.example.com"
|
67
|
+
end
|
68
|
+
|
69
|
+
it "should match the route if the recognize proc returns true or an object" do
|
70
|
+
[ true, Object.new ].each do |value|
|
71
|
+
ActionController::Routing::Routes.subdomain_procs.should_receive(:recognize).any_number_of_times.with(:user, "mholling").and_return(value)
|
72
|
+
lambda { recognize_path(@request) }.should_not raise_error
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
it "should not match the route if the recognize proc returns false or nil" do
|
77
|
+
[ false, nil ].each do |value|
|
78
|
+
ActionController::Routing::Routes.subdomain_procs.should_receive(:recognize).any_number_of_times.with(:user, "mholling").and_return(value)
|
79
|
+
lambda { recognize_path(@request) }.should raise_error(ActionController::RoutingError)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
it "should raise any error that the recognize proc raises" do
|
84
|
+
error = StandardError.new
|
85
|
+
ActionController::Routing::Routes.subdomain_procs.should_receive(:recognize).any_number_of_times.with(:user, "mholling").and_raise(error)
|
86
|
+
lambda { recognize_path(@request) }.should raise_error { |e| e.should == error }
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe "resource" do
|
4
|
+
before(:each) do
|
5
|
+
ActionController::Routing::Routes.clear!
|
6
|
+
SubdomainRoutes::Config.stub!(:domain_length).and_return(2)
|
7
|
+
end
|
8
|
+
|
9
|
+
describe "mappings" do
|
10
|
+
it "should pass the specified subdomains to any nested routes" do
|
11
|
+
map_subdomain(:admin) do |admin|
|
12
|
+
admin.resources(:users) { |user| user.options[:subdomains].should == [ "admin" ] }
|
13
|
+
admin.resource(:config) { |config| config.options[:subdomains].should == [ "admin" ] }
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
describe "routes" do
|
19
|
+
before(:each) do
|
20
|
+
map_subdomain(:admin) do |admin|
|
21
|
+
admin.resources :users
|
22
|
+
admin.resource :config
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
it "should include the subdomains in the routing conditions" do
|
27
|
+
ActionController::Routing::Routes.routes.each do |route|
|
28
|
+
route.conditions[:subdomains].should == [ "admin" ]
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
it "should include the subdomains in the routing conditions" do
|
33
|
+
ActionController::Routing::Routes.routes.each do |route|
|
34
|
+
route.requirements[:subdomains].should == [ "admin" ]
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|