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 ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Matthew Hollingworth
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.textile ADDED
@@ -0,0 +1,323 @@
1
+ h1. SubdomainRoutes - Adding Subdomains to Rails Routing
2
+
3
+ The Rails "routing system":http://api.rubyonrails.org/classes/ActionController/Routing.html is a pretty impressive piece of code. There's a fair bit of magic going on to make your routes so easy to define and use in the rest of your Rails application. One area in which the routing system is limited however is its use of subdomains: it's pretty much assumed that your site will be using a single, fixed domain.
4
+
5
+ There are times where it is preferable to spread a website over multiple subdomains. One common idiom in URL schemes is to separate aspects of the site under different subdomains, representative of those aspect. It many cases a simple, fixed subdomain scheme is desirable: _support.whatever.com_, _forums.whatever.com_, _gallery.whatever.com_ and so on. On some international sites, the subdomain is used to select the language and localization: _en.wikipedia.org_, _fr.wikipedia.org_, _ja.wikipedia.org_. Other schemes allocate each user of the site their own subdomain, so as to personalise the experience (_blogspot.com_ is a good example of this).
6
+
7
+ A couple of plugins currently exists for Rails developers wishing to incorporate subdomains into their routes. The de facto standard is "SubdomainFu":http://www.intridea.com/2008/6/23/subdomainfu-a-new-way-to-tame-the-subdomain. (I'll admit - I haven't actually used this plugin myself.) There's also "SubdomainAccount":http://github.com/shuber/subdomain_account/tree/master.
8
+
9
+ I've recently completed work on a subdomain library which fully incorporates subdomains into the rails routing environment - in URL generation, route recognition *and* in route definition, something I don't believe is currently available. As an added bonus, if offers the ability to define routes for _dynamic subdomains_, which are recognised based on the contents of your database, or on other dynamic conditions.
10
+
11
+ h2. Installation
12
+
13
+ The gem is called <code>SubdomainRoutes</code>, and is easy to install from GitHub (you only need to add GitHub as a source once):
14
+
15
+ <pre>
16
+ gem sources -a http://gems.github.com
17
+ sudo gem install mholling-active_url
18
+ </pre>
19
+
20
+ In your Rails app, make sure to specify the gem dependency in environment.rb:
21
+
22
+ <pre>
23
+ config.gem "mholling-subdomain_routes", :lib => "subdomain_routes", :source => "http://gems.github.com"
24
+ </pre>
25
+
26
+ (Note that the SubdomainRoutes gem requires Rails 2.2 or later to run since it uses <code>ActiveSupport::Memoizable</code>. If you're on an older version of Rails, you need to get with the program. ;)
27
+
28
+ Finally, you'll probably want to configure your session to work across all your subdomains. You can do this in your environment files:
29
+
30
+ <pre>
31
+ # in environment/development.rb:
32
+ config.action_controller.session[:session_domain] = "yourdomain.local" # or whatever
33
+
34
+ # in environment/production.rb:
35
+ config.action_controller.session[:session_domain] = "yourdomain.com" # or whatever
36
+ </pre>
37
+
38
+ h2. Mapping a Single Subdomain
39
+
40
+ Let's start with a simple example. Say we have a site which offers a support section, where users submit and view support tickets for problems they're having. It'd be nice to have that under a separate subdomain, say _support.mysite.com_. With subdomain routes we'd map that as follows:
41
+
42
+ <pre>
43
+ ActionController::Routing::Routes.draw do |map|
44
+ map.subdomain :support do |support|
45
+ support.resources :tickets
46
+ ...
47
+ end
48
+ end
49
+ </pre>
50
+
51
+ What does this achieve? A few things. For routes defined within the subdomain block:
52
+
53
+ * named routes have a <code>support_</code> prefix;
54
+ * their controllers will have a <code>Support::</code> namespace;
55
+ * they will only be recognised if the host subdomain is _support_; and
56
+ * paths and URLs generated for them by <code>url_for</code> and named routes will force the _support_ subdomain if the current host subdomain is different.
57
+
58
+ This is just what you want for a subdomain-qualified route. Rails will recognize _support.mysite.com/tickets_, but not _www.mysite.com/tickets_.
59
+
60
+ Let's take a look at the flip-side of route recognition - path and URL generation. The subdomain restrictions are also applied here:
61
+
62
+ <pre>
63
+ # when the current host is support.mysite.com:
64
+ support_tickets_path
65
+ => "/tickets"
66
+
67
+ # when the current host is www.mysite.com:
68
+ support_tickets_path
69
+ => "http://support.mysite.com/tickets"
70
+
71
+ # overriding the subdomain won't work:
72
+ support_tickets_path(:subdomain => :www)
73
+ # ActionController::RoutingError: Route failed to generate
74
+ # (expected subdomain in ["support"], instead got subdomain "www")
75
+ </pre>
76
+
77
+ Notice that, by necessity, requesting a path still results in an URL if the subdomain of the route is different. If you try and override the subdomain manually, you'll get an error, because the resulting URL would be invalid and would not be recognized. This is a good thing - you don't want to be linking to invalid URLs by mistake!
78
+
79
+ In other words, <code>url_for</code> and your named routes will *never* generate an invalid URL. This is one major benefit of the SubdomainRoutes gem: it offers a smart way of switching subdomains, requiring them to be specified manually only when absolutely necessary. (It gets even better with dynamic subdomains, which I will describe later.)
80
+
81
+ h2. Mapping Multiple Subdomains
82
+
83
+ Subdomain routes can be set on multiple subdomains too. Let's take another example. Say we have a review site, _reviews.com_, which has reviews of titles in several different media (say, DVDs, games, books and CDs). We want to key the media type to the URL subdomain, so the user knows by the URL what section of the site they're in. (I use this scheme on my "swapping site":http://things.toswap.com.au.) A subdomain route map for such a scheme could be as follows:
84
+
85
+ <pre>
86
+ ActionController::Routing::Routes.draw do |map|
87
+ map.subdomain :dvd, :game, :book, :cd, :name => :media do |media|
88
+ media.resources :reviews
89
+ ...
90
+ end
91
+ end
92
+ </pre>
93
+
94
+ Notice that we've specified a generic name (_media_) for our subdomain, so that our namespace and named route prefix become <code>Media::</code> and <code>media_</code>, respectively. (We could also set the <code>:name</code> to nil, or override <code>:namespace</code> or <code>:name_prefix</code> individually.)
95
+
96
+ Recognition of these routes will work in the same way as before. The URL _dvd.reviews.com/reviews_ will be recognised, as will _game.reviews.com/reviews_, and so on. No luck with _concert.reviews.com/reviews_, as that subdomain is not listed in the <code>subdomain</code> mapping.
97
+
98
+ URL generation may behave differently however. If the URL is being generated with current host _www.reviews.com_, there is no way for Rails to know which of the subdomains to use, so you must specify it in the call to <code>url_for</code> or the named route. On the other hand, if the current host is _dvd.reviews.com_ the URL or path will just generate with the current host unless you explicitly override the subdomain. Check it:
99
+
100
+ <pre>
101
+ # when the current host is dvd.reviews.com:
102
+ media_reviews_path
103
+ => "/reviews"
104
+
105
+ # when the current host is www.reviews.com:
106
+ media_reviews_path
107
+ # ActionController::RoutingError: Route failed to generate (expected
108
+ # subdomain in ["dvd", "game", "book", "cd"], instead got subdomain "www")
109
+
110
+ media_reviews_path(:subdomain => :book)
111
+ => "http://book.reviews.com/reviews"
112
+ </pre>
113
+
114
+ Again, requesting a path may result in an URL or a path, depending on whether the subdomain of the current host needs to be changed. And again, the URL-writing system will not generate any URL that will not in turn be recognised by the app.
115
+
116
+ h2. Mapping the Nil Subdomain
117
+
118
+ SubdomainRoutes allows you to specify routes for the "nil subdomain" - for example, URLs using _example.com_ instead of _www.example.com_. To do this though, you'll need to configure the gem.
119
+
120
+ By default, SubdomainRoutes just extracts the first part of the host as the subdomain, which is fine for most situations. But in the example above, _example.com_ would have a subdomain of _example_; obviously, not what you want. You can change this behaviour by setting a configuration option (you can put this in an initializer file in your Rails app):
121
+
122
+ <pre>
123
+ SubdomainRoutes::Config.domain_length = 2
124
+ </pre>
125
+
126
+ With this set, the subdomain for _example.com_ will be <code>""</code>, the empty string. (You can also use nil to specify this in your routes.)
127
+
128
+ If you're on a country-code top-level domain (e.g. _toswap.com.au_), you'd set the domain length to three. You may even need to set it to four (e.g. for nested government and education domains such as _health.act.gov.au_).
129
+
130
+ (Note that, in your controllers, your request will now have a <code>subdomain</code> method which returns the subdomain extracted in this way.)
131
+
132
+ Here's an example of how you might want to use a nil subdomain:
133
+
134
+ <pre>
135
+ ActionController::Routing::Routes.draw do |map|
136
+ map.subdomain nil, :www do |www|
137
+ www.resource :home
138
+ ...
139
+ end
140
+ end
141
+ </pre>
142
+
143
+ All the routes within the subdomain block will resolve under both _www.example.com_ and just _example.com_.
144
+
145
+ (As an aside, this is not actually an approach I would recommend taking; you should probably not have the same content mirrored under two different URLs. Instead, set up your server to redirect to your preferred host, be it with the _www_ or without.)
146
+
147
+ Finally, for the nil subdomain, there is some special behaviour. Specifically, the namespace and name prefix for the routes will default to the first non-nil subdomain (or to nothing if _only_ the nil subdomain is specified). You can override this behaviour by passing a <code>:name</code> option.
148
+
149
+ h2. Nested Resources under a Subdomain
150
+
151
+ REST is awesome. If you're not using "RESTful routes":http://api.rubyonrails.org/classes/ActionController/Resources.html in your Rails apps, you should be. It offers a disciplined way to design your routes, and this flows through to the design of the rest of your app, encouraging you to capture pretty much all your application logic in models and leaving your controllers as generic and "skinny":http://weblog.jamisbuck.org/2006/10/18/skinny-controller-fat-model as can be.
152
+
153
+ Subdomain routes work transparently with RESTful routes - any routes nested under a resource will inherit the subdomain conditions of that resource:
154
+
155
+ <pre>
156
+ ActionController::Routing::Routes.draw do |map|
157
+ map.subdomain :admin do |admin|
158
+ admin.resources :roles, :has_many => :users
159
+ ...
160
+ end
161
+ end
162
+ </pre>
163
+
164
+ Your <code>admin_role_users_path(@role)</code> will automatically generate with the correct _admin_ subdomain if required, and paths such as _/roles/1/users_ will only be recognised when under the _admin_ subdomain. Note that both the block form and the <code>:has_many</code> form of nested resources will work in this manner. (In fact, under the hood, the latter just falls through to the former.) Any other (non-resource) routes you nest under a resource will also inherit the subdomain conditions.
165
+
166
+ h2. Setting Up Your Development Environment
167
+
168
+ To develop your app using SudomainRoutes, you'll need to set up your machine to point some test domains to the server on your machine (i.e. to the local loopback address, 127.0.0.1). On a Mac, you can do this by editing <code>/etc/hosts</code>. Let's say you want to use the subdomains _www_, _dvd_, _game_, _book_ and _cd_, with a domain of _reviews.local_. Adding these lines to <code>/etc/hosts</code> will do the trick:
169
+
170
+ <pre>
171
+ 127.0.0.1 reviews.local
172
+ 127.0.0.1 www.reviews.local
173
+ 127.0.0.1 dvd.reviews.local
174
+ 127.0.0.1 game.reviews.local
175
+ 127.0.0.1 book.reviews.local
176
+ 127.0.0.1 cd.reviews.local
177
+ </pre>
178
+
179
+ You'll need to flush your DNS cache for these changes to take effect:
180
+
181
+ <pre>
182
+ dscacheutil -flushcache
183
+ </pre>
184
+
185
+ Then fire up your <code>script/server</code>, point your browser to _www.reviews.local:3000_ and your app should be up and running. If you're using "Passenger":http://www.modrails.com to "serve your apps in development":http://www.google.com/search?q=rails+passenger+development (and I highly recommend that you do), you'll need to add a Virtual Host to your Apache .conf file. (Don't forget to alias all the subdomains and restart the server.)
186
+
187
+ If you're using dynamic subdomain routes (covered in my next article), you may want to use a catch-all (wildcard) subdomain. Setting this up is not so easy, since wildcards (like _*.reviews.local_) won't work in your <code>/etc/hosts</code> file. There are a couple of work-around for this:
188
+
189
+ # Use a "proxy.pac":http://en.wikipedia.org/wiki/Proxy.pac file in your browser so that it proxies _*.reviews.local_ to localhost. How to do this will depend on the browser you're using.
190
+ # Set up a local DNS server with an A record for the domain. This may be a bit involved.
191
+
192
+ h2. Dynamic Subdomain Recognition
193
+
194
+ SubdomainRoutes also offers a full complement of features for recognizing subdomain URLS which are not known in advance. (I'll refer to these cases as _dynamic subdomains_.) For example, if you are keying each user of your website to their own subdomain, your routes will need to recognise new subdomains as new users sign up. Another example is if you want to key your URL subdomains to a set of categories (language, city, product type, etc.) which may expand as your site grows. In each case, your subdomains will depend on records in the database.
195
+
196
+ Let's take a hypothetical example of a blogging site which lets users show and manage their blog under their own subdomains (think _blogspot.com_). Assume our <code>User</code> model has a <code>subdomain</code> attribute which contains the user's custom subdomain. In our routes we'll still use the <code>subdomain</code> mapper, but instead of specifying one or more subdomains, we just specify a <code>:proc</code> option:
197
+
198
+ <pre>
199
+ ActionController::Routing::Routes.draw do |map|
200
+ map.subdomain :proc => :blog do |blog|
201
+ blog.resources :articles, :has_many => :comments
202
+ ...
203
+ end
204
+ end
205
+
206
+ ActionController::Routing::Routes.recognize_subdomain :blog do |subdomain|
207
+ User.exists?(:username => subdomain)
208
+ end
209
+ </pre>
210
+
211
+ Note the last few lines, which are crucial. Here we are defining a method for recognizing blog subdomains. This is done using a new method, <code>recognize_subdomain</code>, which the gem adds to <code>ActionController::Routing::Routes</code>.
212
+
213
+ As arguments, the <code>recognize_subdomain</code> method takes a subdomain label, as used in the route (in this case, <code>:blog</code>), and a block. The block should take a single argument (the subdomain to be recognised), and should return true or false depending on whether the subdomain should be recognised for that route. In this case, we simply look up the subdomain in the <code>User</code> model to see if it's in use.
214
+
215
+ That's all there is to it! Now our blog routes will recognize more subdomains as more users sign up.
216
+
217
+ How does URL _generation_ work with these routes? Pretty much as you'd expect. Let's say our awesome new blogging site, _awesomeblogs.com_, has only two users, who've selected subdomains _mholling_ and _rails_. In controllers and views, our named routes will work as follows:
218
+
219
+ <pre>
220
+ # when the current host is mholling.awesomeblogs.com:
221
+ blog_articles_path
222
+ => "/articles"
223
+
224
+ blog_articles_path(:subdomain => "rails")
225
+ => "http://rails.awesomblogs.com/articles"
226
+
227
+ blog_articles_path(:subdomain => "www")
228
+ # ActionController::RoutingError: Route failed to generate
229
+ # (subdomain "www" is invalid)
230
+
231
+ # when the host is www.awesomeblogs.com:
232
+ blog_articles_path(:subdomain => "rails")
233
+ => "http://rails.awesomblogs.com/articles"
234
+
235
+ blog_articles_path
236
+ # ActionController::RoutingError: Route failed to generate
237
+ # (subdomain "www" is invalid)
238
+ </pre>
239
+
240
+ As you can see, the subdomain recognition method that we defined is being used to check for an invalid subdomain when the URL is being written. Nice!
241
+
242
+ h3. Caching of Dynamic Subdomain Recognition
243
+
244
+ Internally, the results of the subdomain recognition block are cached. If this weren't done, the block could be run many time per request (once for each route until one is recognised). That would be bad, since in most cases the database will be hit each time the block is called. (Sure, Rails will probably cache the database queries, but you may as well cache as high up the chain as you can.)
245
+
246
+ The problem then becomes - when does the cache get flushed? By default, this will occur once per request, which should be fine for many applications. If that's the case, you don't need to do anything.
247
+
248
+ To optimise performance however, you can disable the per-request cache flush and do it manually. Set this option in your configuration:
249
+
250
+ <pre>
251
+ SubdomainRoutes::Config.manual_flush = true
252
+ </pre>
253
+
254
+ In the above example we might want to choose this route, so that we can flush the cache only when a user is created or destroyed (or changes the subdomain). We could do this easily using an observer:
255
+
256
+ <pre>
257
+ class UserObserver < ActiveRecord::Observer
258
+ observe :user
259
+
260
+ after_save(user)
261
+ if user.subdomain_changed?
262
+ ActionController::Routing::Routes.subdomain_procs.flush!
263
+ end
264
+ end
265
+
266
+ after_destroy(user)
267
+ ActionController::Routing::Routes.subdomain_procs.flush!
268
+ end
269
+ end
270
+
271
+ # and in config/environment.rb:
272
+ config.active_record.observers = [ :user_observer ]
273
+ </pre>
274
+
275
+ Another way to reduce the number of calls to the subdomain recognition block is by defining the dynamic subdomain route as late in <code>routes.rb</code> as possible. That way, the block will only be called if none of the other routes have resolved.
276
+
277
+ h3. ActiveRecord Validations
278
+
279
+ SubdomainRoutes also gives you a utility validation, <code>validates_subdomain_format_of</code>, for your ActiveRecord models. If you're storing a subdomain in a model (as we have been with our <code>User</code> example), you can use this validation to make sure that a subdomain field uses only legal characters and is in an allowed format. (Undoubtedly, you'll want to throw in a <code>validates_uniqueness_of</code> as well.)
280
+
281
+ <pre>
282
+ class User < ActiveRecord::Base
283
+ ...
284
+ validates_subdomain_format_of :subdomain
285
+ validates_uniqueness_of :subdomain
286
+ ...
287
+ end
288
+ </pre>
289
+
290
+ The library currently uses a simple regexp to limit subdomains to lowercase alphanumeric characters and dashes (except on either end). If you want to conform more precisely to the URI specs, you can override the <code>SubdomainRoutes.valid_subdomain?</code> method and implement your own.
291
+
292
+ h2. Using Static and Dynamic Subdomain Routes Together
293
+
294
+ Let's try using static and dynamic subdomain routes together. Say we want to reserve some subdomains (say _support_ and _admin_) for administrative functions, with the remainder keyed to user accounts. Our routes:
295
+
296
+ <pre>
297
+ ActionController::Routing::Routes.draw do |map|
298
+ map.subdomain :support do |support|
299
+ ...
300
+ end
301
+ map.subdomain :admin do |admin|
302
+ ...
303
+ end
304
+ map.subdomain :proc => :user do |user|
305
+ ...
306
+ end
307
+ end
308
+
309
+ ActionController::Routing::Routes.recognize_subdomain :blog do |subdomain|
310
+ User.exists?(:username => subdomain)
311
+ end
312
+ </pre>
313
+
314
+ These routes will co-exist quite happily together. (We've made sure our static subdomain routes are listed first though.) In the User model we'll add an extra validation to prevent users from taking the reserved subdomains:
315
+
316
+ <pre>
317
+ validates_exclusion_of :subdomain, :in => %w{support admin www}
318
+ </pre>
319
+
320
+ (We could also validate for a minimum and maximum length.)
321
+
322
+
323
+ Copyright (c) 2009 Matthew Hollingworth. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,57 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "subdomain_routes"
8
+ gem.summary = %Q{A Rails library for incorporating subdomains into route generation and recognition.}
9
+ gem.description = <<-EOF
10
+ SubdomainRoutes add subdomain conditions to the Rails routing system. Routes may be restricted to
11
+ one or many specified subdomains. An URL will be recognised only if the host subdomain matches the
12
+ subdomain specified in the route. Route generation is also enhanced, so that the subdomain of a
13
+ generated URL (or path) will be changed if the requested route has a different subdomain to that of
14
+ the current request. Dynamic subdomain routes can also be defined.
15
+ EOF
16
+ gem.email = "mdholling@gmail.com"
17
+ gem.homepage = "http://github.com/mholling/subdomain_routes"
18
+ gem.authors = ["Matthew Hollingworth"]
19
+ gem.add_dependency 'actionpack', ">= 2.2.1" # requires ActiveSupport::Memoizable
20
+ gem.has_rdoc = false
21
+
22
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
23
+ end
24
+ rescue LoadError
25
+ puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
26
+ end
27
+
28
+ require 'spec/rake/spectask'
29
+ Spec::Rake::SpecTask.new(:spec) do |spec|
30
+ spec.libs << 'lib' << 'spec'
31
+ spec.spec_files = FileList['spec/**/*_spec.rb']
32
+ end
33
+
34
+ Spec::Rake::SpecTask.new(:rcov) do |spec|
35
+ spec.libs << 'lib' << 'spec'
36
+ spec.pattern = 'spec/**/*_spec.rb'
37
+ spec.rcov = true
38
+ end
39
+
40
+
41
+ task :default => :spec
42
+
43
+ require 'rake/rdoctask'
44
+ Rake::RDocTask.new do |rdoc|
45
+ if File.exist?('VERSION.yml')
46
+ config = YAML.load(File.read('VERSION.yml'))
47
+ version = "#{config[:major]}.#{config[:minor]}.#{config[:patch]}"
48
+ else
49
+ version = ""
50
+ end
51
+
52
+ rdoc.rdoc_dir = 'rdoc'
53
+ rdoc.title = "subdomain_routes #{version}"
54
+ rdoc.rdoc_files.include('README*')
55
+ rdoc.rdoc_files.include('lib/**/*.rb')
56
+ end
57
+
data/VERSION.yml ADDED
@@ -0,0 +1,4 @@
1
+ ---
2
+ :patch: 0
3
+ :major: 0
4
+ :minor: 1
@@ -0,0 +1,6 @@
1
+ module SubdomainRoutes
2
+ module Config
3
+ mattr_accessor :domain_length
4
+ mattr_accessor :manual_flush
5
+ end
6
+ end
@@ -0,0 +1,17 @@
1
+ module SubdomainRoutes
2
+ module MailerMethods
3
+ def self.included(base)
4
+ base.alias_method_chain :create!, :subdomains
5
+ end
6
+
7
+ def create_with_subdomains!(*args)
8
+ ActionController::Routing::Routes.subdomain_procs.flush! unless SubdomainRoutes::Config.manual_flush
9
+ create_without_subdomains!(*args)
10
+ end
11
+ end
12
+ end
13
+
14
+ if defined? ActionMailer::Base
15
+ ActionMailer::Base.send :include, SubdomainRoutes::MailerMethods
16
+ end
17
+
@@ -0,0 +1,40 @@
1
+ module SubdomainRoutes
2
+ module Routing
3
+ module RouteSet
4
+ module Mapper
5
+ def subdomain(*subdomains, &block)
6
+ options = subdomains.extract_options!
7
+ name = nil
8
+ if subdomains.empty?
9
+ if subdomain = options.delete(:proc)
10
+ subdomain_options = { :subdomains => { :proc => subdomain } }
11
+ name = subdomain
12
+ else
13
+ raise ArgumentError, "Please specify at least one subdomain!"
14
+ end
15
+ else
16
+ subdomains.map!(&:to_s)
17
+ subdomains.map!(&:downcase)
18
+ subdomains.uniq!
19
+ subdomains.compact.each do |subdomain|
20
+ raise ArgumentError, "Illegal subdomain format: #{subdomain.inspect}" unless subdomain.blank? || SubdomainRoutes.valid_subdomain?(subdomain)
21
+ end
22
+ if subdomains.include? ""
23
+ raise ArgumentError, "Can't specify a nil subdomain unless you set Config.domain_length!" unless Config.domain_length
24
+ end
25
+ name = subdomains.reject(&:blank?).first
26
+ subdomain_options = { :subdomains => subdomains }
27
+ end
28
+ name = options.delete(:name) if options.has_key?(:name)
29
+ name = name.to_s.downcase.gsub(/[^(a-z0-9)]/, ' ').squeeze(' ').strip.gsub(' ', '_') unless name.blank?
30
+ subdomain_options.merge! :name_prefix => "#{name}_", :namespace => "#{name}/" unless name.blank?
31
+ with_options(subdomain_options.merge(options), &block)
32
+ end
33
+ alias_method :subdomains, :subdomain
34
+ end
35
+ end
36
+ end
37
+ end
38
+
39
+ ActionController::Routing::RouteSet::Mapper.send :include, SubdomainRoutes::Routing::RouteSet::Mapper
40
+
@@ -0,0 +1,33 @@
1
+ module SubdomainRoutes
2
+ class ProcSet
3
+ extend ActiveSupport::Memoizable
4
+
5
+ def initialize
6
+ clear!
7
+ end
8
+
9
+ def add_recognizer(name, &block)
10
+ @recognizers[name] = block
11
+ end
12
+
13
+ def recognizes?(name)
14
+ @recognizers.has_key?(name)
15
+ end
16
+
17
+ def recognize(name, subdomain)
18
+ @recognizers[name].call(subdomain) if recognizes?(name)
19
+ end
20
+
21
+ memoize :recognize
22
+ private :flush_cache
23
+
24
+ def flush!
25
+ flush_cache :recognize
26
+ end
27
+
28
+ def clear!
29
+ @recognizers = {}
30
+ flush!
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,12 @@
1
+ module SubdomainRoutes
2
+ module Request
3
+ include SplitHost
4
+
5
+ def subdomain
6
+ subdomain_for_host(host)
7
+ # TODO: catch HostNotSupplied and TooManySubdomains and deal with them somehow!
8
+ end
9
+ end
10
+ end
11
+
12
+ ActionController::Request.send :include, SubdomainRoutes::Request
@@ -0,0 +1,49 @@
1
+ module SubdomainRoutes
2
+ module Resources
3
+ def self.included(base)
4
+ base.alias_method_chain :action_options_for, :subdomains
5
+ base::INHERITABLE_OPTIONS << :subdomains
6
+ end
7
+
8
+ def action_options_for_with_subdomains(action, resource, *args)
9
+ action_options_for_without_subdomains(action, resource, *args).merge(:subdomains => resource.options[:subdomains])
10
+ end
11
+ private :action_options_for_with_subdomains
12
+ end
13
+ end
14
+
15
+ ActionController::Resources.send :include, SubdomainRoutes::Resources
16
+
17
+
18
+
19
+
20
+ # #
21
+ # # This alternative also works. It duplicates code in add_route_with_subdomains (in routing.rb) so is not
22
+ # # so good that way, but has the benefit of not modifying ActionController::Resources#action_options_for,
23
+ # # which is a private method.
24
+ # #
25
+ # module SubdomainRoutes
26
+ # module Resources
27
+ # module Resource
28
+ # def self.included(base)
29
+ # base.alias_method_chain :conditions, :subdomains
30
+ # base.alias_method_chain :requirements, :subdomains
31
+ # end
32
+ #
33
+ # def conditions_with_subdomains
34
+ # @options[:subdomains] ?
35
+ # conditions_without_subdomains.merge(:subdomains => @options[:subdomains]) :
36
+ # conditions_without_subdomains
37
+ # end
38
+ #
39
+ # def requirements_with_subdomains(with_id = false)
40
+ # @options[:subdomains] ?
41
+ # requirements_without_subdomains(with_id).merge(:subdomains => @options[:subdomains]) :
42
+ # requirements_without_subdomains(with_id)
43
+ # end
44
+ # end
45
+ # end
46
+ # end
47
+ #
48
+ # ActionController::Resources::INHERITABLE_OPTIONS << :subdomains
49
+ # ActionController::Resources::Resource.send :include, SubdomainRoutes::Resources::Resource
@@ -0,0 +1,66 @@
1
+ module SubdomainRoutes
2
+ module Routing
3
+ module RouteSet
4
+ include SplitHost
5
+
6
+ def self.included(base)
7
+ [ :extract_request_environment, :add_route, :clear!, :call ].each { |method| base.alias_method_chain method, :subdomains }
8
+ end
9
+
10
+ def extract_request_environment_with_subdomains(request)
11
+ extract_request_environment_without_subdomains(request).merge(:subdomain => subdomain_for_host(request.host))
12
+ end
13
+
14
+ def add_route_with_subdomains(*args)
15
+ options = args.extract_options!
16
+ if subdomains = options.delete(:subdomains)
17
+ options[:conditions] ||= {}
18
+ options[:conditions][:subdomains] = subdomains
19
+ options[:requirements] ||= {}
20
+ options[:requirements][:subdomains] = subdomains
21
+ end
22
+ with_options(options) { |routes| routes.add_route_without_subdomains(*args) }
23
+ end
24
+
25
+ def subdomain_procs
26
+ @subdomain_procs ||= SubdomainRoutes::ProcSet.new
27
+ end
28
+
29
+ def recognize_subdomain(name, &block)
30
+ subdomain_procs.add_recognizer(name, &block)
31
+ end
32
+
33
+ def clear_with_subdomains!
34
+ subdomain_procs.clear!
35
+ clear_without_subdomains!
36
+ end
37
+
38
+ def call_with_subdomains(*args)
39
+ subdomain_procs.flush! unless SubdomainRoutes::Config.manual_flush
40
+ call_without_subdomains(*args)
41
+ end
42
+ end
43
+
44
+ module Route
45
+ def self.included(base)
46
+ base.alias_method_chain :recognition_conditions, :subdomains
47
+ end
48
+
49
+ def recognition_conditions_with_subdomains
50
+ result = recognition_conditions_without_subdomains
51
+ case conditions[:subdomains]
52
+ when Array
53
+ result << "conditions[:subdomains].include?(env[:subdomain])"
54
+ when Hash
55
+ if subdomain = conditions[:subdomains][:proc]
56
+ result << %Q{ActionController::Routing::Routes.subdomain_procs.recognize(#{subdomain.inspect}, env[:subdomain])}
57
+ end
58
+ end
59
+ result
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+ ActionController::Routing::RouteSet.send :include, SubdomainRoutes::Routing::RouteSet
66
+ ActionController::Routing::Route.send :include, SubdomainRoutes::Routing::Route