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.
@@ -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
@@ -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