projectdx-subdomain_routes 0.3.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.gitignore +7 -0
- data/.rvmrc +1 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +40 -0
- data/LICENSE +20 -0
- data/README.textile +421 -0
- data/Rakefile +59 -0
- data/VERSION.yml +5 -0
- data/history.txt +18 -0
- data/lib/subdomain_routes.rb +13 -0
- data/lib/subdomain_routes/assertions.rb +68 -0
- data/lib/subdomain_routes/config.rb +5 -0
- data/lib/subdomain_routes/mapper.rb +43 -0
- data/lib/subdomain_routes/request.rb +11 -0
- data/lib/subdomain_routes/resources.rb +49 -0
- data/lib/subdomain_routes/routes.rb +97 -0
- data/lib/subdomain_routes/split_host.rb +32 -0
- data/lib/subdomain_routes/url_writer.rb +92 -0
- data/lib/subdomain_routes/validations.rb +42 -0
- data/rails/init.rb +1 -0
- data/spec/assertions_spec.rb +193 -0
- data/spec/extraction_spec.rb +73 -0
- data/spec/mapping_spec.rb +168 -0
- data/spec/recognition_spec.rb +78 -0
- data/spec/resources_spec.rb +33 -0
- data/spec/routes_spec.rb +13 -0
- data/spec/spec_helper.rb +77 -0
- data/spec/test_unit_matcher.rb +46 -0
- data/spec/url_writing_spec.rb +262 -0
- data/spec/validations_spec.rb +27 -0
- data/subdomain_routes.gemspec +85 -0
- metadata +124 -0
data/.document
ADDED
data/.gitignore
ADDED
data/.rvmrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rvm --create --install 1.8.7-p302@subdomain_routes
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
GEM
|
2
|
+
remote: http://rubygems.org/
|
3
|
+
specs:
|
4
|
+
actionmailer (2.3.9)
|
5
|
+
actionpack (= 2.3.9)
|
6
|
+
actionpack (2.3.9)
|
7
|
+
activesupport (= 2.3.9)
|
8
|
+
rack (~> 1.1.0)
|
9
|
+
activerecord (2.3.9)
|
10
|
+
activesupport (= 2.3.9)
|
11
|
+
activeresource (2.3.9)
|
12
|
+
activesupport (= 2.3.9)
|
13
|
+
activesupport (2.3.9)
|
14
|
+
gemcutter (0.6.1)
|
15
|
+
git (1.2.5)
|
16
|
+
jeweler (1.4.0)
|
17
|
+
gemcutter (>= 0.1.0)
|
18
|
+
git (>= 1.2.5)
|
19
|
+
rubyforge (>= 2.0.0)
|
20
|
+
json_pure (1.4.6)
|
21
|
+
rack (1.1.0)
|
22
|
+
rails (2.3.9)
|
23
|
+
actionmailer (= 2.3.9)
|
24
|
+
actionpack (= 2.3.9)
|
25
|
+
activerecord (= 2.3.9)
|
26
|
+
activeresource (= 2.3.9)
|
27
|
+
activesupport (= 2.3.9)
|
28
|
+
rake (>= 0.8.3)
|
29
|
+
rake (0.8.7)
|
30
|
+
rspec (1.3.0)
|
31
|
+
rubyforge (2.0.4)
|
32
|
+
json_pure (>= 1.1.7)
|
33
|
+
|
34
|
+
PLATFORMS
|
35
|
+
ruby
|
36
|
+
|
37
|
+
DEPENDENCIES
|
38
|
+
jeweler
|
39
|
+
rails (= 2.3.9)
|
40
|
+
rspec
|
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,421 @@
|
|
1
|
+
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.
|
2
|
+
|
3
|
+
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).
|
4
|
+
|
5
|
+
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.
|
6
|
+
|
7
|
+
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 subdomain routes which are keyed to a model (user, category, etc.) stored in your database.
|
8
|
+
|
9
|
+
h2. Installation
|
10
|
+
|
11
|
+
The gem is called <code>SubdomainRoutes</code>, and is easy to install from Gemcutter (you only need to add Gemcutter as a source once):
|
12
|
+
|
13
|
+
<pre>
|
14
|
+
gem sources -a http://gemcutter.org
|
15
|
+
sudo gem install subdomain_routes
|
16
|
+
</pre>
|
17
|
+
|
18
|
+
In your Rails app, make sure to specify the gem dependency in environment.rb:
|
19
|
+
|
20
|
+
<pre>
|
21
|
+
config.gem "subdomain_routes", :source => "http://gemcutter.org"
|
22
|
+
</pre>
|
23
|
+
|
24
|
+
[ *UPDATE:* You can also install the gem as a plugin: <code>script/plugin install git://github.com/mholling/subdomain_routes.git</code> ]
|
25
|
+
|
26
|
+
(Note that the SubdomainRoutes gem requires Rails 2.2 or later to run since it changes <code>ActionController::Resources::INHERITABLE_OPTIONS</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
|
+
[ *UPDATE*: If you're using your domain without any subdomain, you may need to set the domain to ".yourdomain.com" (with leading period). Also, you may first need to set <code>config.action_controller.session ||= {}</code> in your environment file, in case the session configuration variable has not already been set. ]
|
39
|
+
|
40
|
+
h2. Mapping a Single Subdomain
|
41
|
+
|
42
|
+
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:
|
43
|
+
|
44
|
+
<pre>
|
45
|
+
ActionController::Routing::Routes.draw do |map|
|
46
|
+
map.subdomain :support do |support|
|
47
|
+
support.resources :tickets
|
48
|
+
...
|
49
|
+
end
|
50
|
+
end
|
51
|
+
</pre>
|
52
|
+
|
53
|
+
What does this achieve? A few things. For routes defined within the subdomain block:
|
54
|
+
|
55
|
+
* named routes have a <code>support_</code> prefix;
|
56
|
+
* their controllers will have a <code>Support::</code> namespace;
|
57
|
+
* they will only be recognised if the host subdomain is _support_; and
|
58
|
+
* 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.
|
59
|
+
|
60
|
+
This is just what you want for a subdomain-qualified route. Rails will recognize _support.mysite.com/tickets_, but not _www.mysite.com/tickets_.
|
61
|
+
|
62
|
+
Let's take a look at the flip-side of route recognition - path and URL generation. The subdomain restrictions are also applied here:
|
63
|
+
|
64
|
+
<pre>
|
65
|
+
# when the current host is support.mysite.com:
|
66
|
+
support_tickets_path
|
67
|
+
=> "/tickets"
|
68
|
+
|
69
|
+
# when the current host is www.mysite.com:
|
70
|
+
support_tickets_path
|
71
|
+
=> "http://support.mysite.com/tickets"
|
72
|
+
|
73
|
+
# overriding the subdomain won't work:
|
74
|
+
support_tickets_path(:subdomain => :www)
|
75
|
+
# ActionController::RoutingError: Route failed to generate
|
76
|
+
# (expected subdomain in ["support"], instead got subdomain "www")
|
77
|
+
</pre>
|
78
|
+
|
79
|
+
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!
|
80
|
+
|
81
|
+
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.
|
82
|
+
|
83
|
+
h2. Mapping Multiple Subdomains
|
84
|
+
|
85
|
+
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:
|
86
|
+
|
87
|
+
<pre>
|
88
|
+
ActionController::Routing::Routes.draw do |map|
|
89
|
+
map.subdomain :dvd, :game, :book, :cd, :name => :media do |media|
|
90
|
+
media.resources :reviews
|
91
|
+
...
|
92
|
+
end
|
93
|
+
end
|
94
|
+
</pre>
|
95
|
+
|
96
|
+
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.)
|
97
|
+
|
98
|
+
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.
|
99
|
+
|
100
|
+
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:
|
101
|
+
|
102
|
+
<pre>
|
103
|
+
# when the current host is dvd.reviews.com:
|
104
|
+
media_reviews_path
|
105
|
+
=> "/reviews"
|
106
|
+
|
107
|
+
# when the current host is www.reviews.com:
|
108
|
+
media_reviews_path
|
109
|
+
# ActionController::RoutingError: Route failed to generate (expected
|
110
|
+
# subdomain in ["dvd", "game", "book", "cd"], instead got subdomain "www")
|
111
|
+
|
112
|
+
media_reviews_path(:subdomain => :book)
|
113
|
+
=> "http://book.reviews.com/reviews"
|
114
|
+
</pre>
|
115
|
+
|
116
|
+
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.
|
117
|
+
|
118
|
+
h2. Mapping the Nil Subdomain
|
119
|
+
|
120
|
+
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.
|
121
|
+
|
122
|
+
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):
|
123
|
+
|
124
|
+
<pre>
|
125
|
+
SubdomainRoutes::Config.domain_length = 2
|
126
|
+
</pre>
|
127
|
+
|
128
|
+
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.)
|
129
|
+
|
130
|
+
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_).
|
131
|
+
|
132
|
+
(Note that, in your controllers, your request will now have a <code>subdomain</code> method which returns the subdomain extracted in this way.)
|
133
|
+
|
134
|
+
Here's an example of how you might want to use a nil subdomain:
|
135
|
+
|
136
|
+
<pre>
|
137
|
+
ActionController::Routing::Routes.draw do |map|
|
138
|
+
map.subdomain nil, :www do |www|
|
139
|
+
www.resource :home
|
140
|
+
...
|
141
|
+
end
|
142
|
+
end
|
143
|
+
</pre>
|
144
|
+
|
145
|
+
All the routes within the subdomain block will resolve under both _www.example.com_ and just _example.com_.
|
146
|
+
|
147
|
+
(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.)
|
148
|
+
|
149
|
+
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.
|
150
|
+
|
151
|
+
h2. Nested Resources under a Subdomain
|
152
|
+
|
153
|
+
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.
|
154
|
+
|
155
|
+
Subdomain routes work transparently with RESTful routes - any routes nested under a resource will inherit the subdomain conditions of that resource:
|
156
|
+
|
157
|
+
<pre>
|
158
|
+
ActionController::Routing::Routes.draw do |map|
|
159
|
+
map.subdomain :admin do |admin|
|
160
|
+
admin.resources :roles, :has_many => :users
|
161
|
+
...
|
162
|
+
end
|
163
|
+
end
|
164
|
+
</pre>
|
165
|
+
|
166
|
+
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.
|
167
|
+
|
168
|
+
h2. Defining Model-Based Subdomain Routes
|
169
|
+
|
170
|
+
The idea here is to have the subdomain of the URL keyed to an ActiveRecord model. Let's take a hypothetical example of a site which lists items under different categories, each category being represented under its own subdomain. Assume our <code>Category</code> model has a <code>subdomain</code> attribute which contains the category'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>:model</code> option:
|
171
|
+
|
172
|
+
<pre>
|
173
|
+
ActionController::Routing::Routes.draw do |map|
|
174
|
+
map.subdomain :model => :category do |category|
|
175
|
+
category.resources :items
|
176
|
+
...
|
177
|
+
end
|
178
|
+
end
|
179
|
+
</pre>
|
180
|
+
|
181
|
+
As before, the namespace and name prefix for all the nested routes will default to the name of the model (you can override these in the options). The routes will match under any subdomain, and that subdomain will be passed to the controller in the <code>params</code> hash as <code>params[:category_id]</code>. For example, a GET request to _dvds.example.com/items_ will go to the <code>Category::ItemsController#index</code> action with <code>params[:category_id]</code> set to <code>"dvds"</code>.
|
182
|
+
|
183
|
+
h2. Generating Model-Based Subdomain URLs
|
184
|
+
|
185
|
+
So how does URL _generation_ work with these routes? That's the best bit: just the same way as you're used to! The routes are fully integrated with your named routes, as well as the <code>form_for</code>, <code>redirect_to</code> and <code>polymorphic_path</code> helpers. The only thing you have to do is make sure your model's <code>to_param</code> returns the subdomain field for the user:
|
186
|
+
|
187
|
+
<pre>
|
188
|
+
class Category < ActiveRecord::Base
|
189
|
+
...
|
190
|
+
alias_method :to_param, :subdomain
|
191
|
+
...
|
192
|
+
end
|
193
|
+
</pre>
|
194
|
+
|
195
|
+
Now, in the above example, let's say our site has _dvd_ and _cd_ su categories, with subdomains _dvds_ and _cds_. In our controller:
|
196
|
+
|
197
|
+
<pre>
|
198
|
+
@dvd
|
199
|
+
=> #<Category id: 1, subdomain: "dvds", ... >
|
200
|
+
|
201
|
+
@cd
|
202
|
+
=> #<Category id: 2, subdomain: "cds", ... >
|
203
|
+
|
204
|
+
# when the current host is dvds.example.com:
|
205
|
+
category_items_path(@dvd)
|
206
|
+
=> "/items"
|
207
|
+
|
208
|
+
polymorphic_path [ @dvd, @dvd.items.first ]
|
209
|
+
=> "/items/2"
|
210
|
+
|
211
|
+
category_items_path(@cd)
|
212
|
+
=> "http://cds.example.com/items"
|
213
|
+
|
214
|
+
polymorphic_path [ @cd, @cd.items.first ]
|
215
|
+
=> "http://cds.example.com/items/10"
|
216
|
+
</pre>
|
217
|
+
|
218
|
+
As you can see, the first argument for the named routes (and polymorphic paths) feeds directly into the subdomain for the URL. No more passing <code>:subdomain</code> options. Nice!
|
219
|
+
|
220
|
+
h2. ActiveRecord Validations
|
221
|
+
|
222
|
+
SubdomainRoutes also gives you a couple of utility validations for your ActiveRecord models:
|
223
|
+
|
224
|
+
* <code>validates_subdomain_format_of</code> ensures a subdomain field uses only legal characters in an allowed format; and
|
225
|
+
* <code>validates_subdomain_not_reserved</code> ensures the field does not take a value already in use by your fixed-subdomain routes.
|
226
|
+
|
227
|
+
(Undoubtedly, you'll want to throw in a <code>validates_uniqueness_of</code> as well.)
|
228
|
+
|
229
|
+
Let's take an example of a site where each user gets a dedicated subdomain. Validations for the <code>subdomain</code> attribute of the <code>User</code> model would be:
|
230
|
+
|
231
|
+
<pre>
|
232
|
+
class User < ActiveRecord::Base
|
233
|
+
...
|
234
|
+
validates_subdomain_format_of :subdomain
|
235
|
+
validates_subdomain_not_reserved :subdomain
|
236
|
+
validates_uniqueness_of :subdomain
|
237
|
+
...
|
238
|
+
end
|
239
|
+
</pre>
|
240
|
+
|
241
|
+
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.
|
242
|
+
|
243
|
+
h2. Using Fixed and Model-Based Subdomain Routes Together
|
244
|
+
|
245
|
+
Let's try using fixed and model-based 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:
|
246
|
+
|
247
|
+
<pre>
|
248
|
+
ActionController::Routing::Routes.draw do |map|
|
249
|
+
map.subdomain :support do |support|
|
250
|
+
...
|
251
|
+
end
|
252
|
+
|
253
|
+
map.subdomain :admin do |admin|
|
254
|
+
...
|
255
|
+
end
|
256
|
+
|
257
|
+
map.subdomain :model => :user do |user|
|
258
|
+
...
|
259
|
+
end
|
260
|
+
end
|
261
|
+
</pre>
|
262
|
+
|
263
|
+
These routes will co-exist quite happily together. We've made sure our static subdomain routes are listed first though, so that they get matched first. In the <code>User</code> model we'd add the validations above, which in this case would prevent users from taking _www_ or _support_ as a subdomain. (We could also validate for a minimum and maximum length using <code>validates_length_of</code>.)
|
264
|
+
|
265
|
+
h2. Setting Up Your Development Environment
|
266
|
+
|
267
|
+
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:
|
268
|
+
|
269
|
+
<pre>
|
270
|
+
127.0.0.1 reviews.local
|
271
|
+
127.0.0.1 www.reviews.local
|
272
|
+
127.0.0.1 dvd.reviews.local
|
273
|
+
127.0.0.1 game.reviews.local
|
274
|
+
127.0.0.1 book.reviews.local
|
275
|
+
127.0.0.1 cd.reviews.local
|
276
|
+
</pre>
|
277
|
+
|
278
|
+
You'll need to flush your DNS cache for these changes to take effect:
|
279
|
+
|
280
|
+
<pre>
|
281
|
+
dscacheutil -flushcache
|
282
|
+
</pre>
|
283
|
+
|
284
|
+
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.)
|
285
|
+
|
286
|
+
If you're using model-based subdomain routes (covered next), 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:
|
287
|
+
|
288
|
+
# 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.
|
289
|
+
# Set up a local DNS server with an A record for the domain. This may be a bit involved.
|
290
|
+
|
291
|
+
h2. Testing with Subdomain Routes
|
292
|
+
|
293
|
+
Testing routes and controllers with the SubdomainRoutes gem may require a little extra work. Here's a simple <code>routes.rb</code> as an example:
|
294
|
+
|
295
|
+
<pre>
|
296
|
+
map.subdomain :admin do |admin|
|
297
|
+
admin.resources :users
|
298
|
+
end
|
299
|
+
|
300
|
+
map.subdomain :model => :city do |city|
|
301
|
+
city.resources :reviews, :only => [ :index, :show ]
|
302
|
+
end
|
303
|
+
</pre>
|
304
|
+
|
305
|
+
(This connects a fixed _admin_ subdomain to a <code>Admin::UsersController</code>, and a model-based _city_ subdomain to a <code>City::ReviewsController</code>.)
|
306
|
+
|
307
|
+
h2. Testing Controllers
|
308
|
+
|
309
|
+
A simple test for the <code>Admin::UsersController#show</code> action would go along these lines:
|
310
|
+
|
311
|
+
<pre>
|
312
|
+
class Admin::UsersControllerTest < ActionController::TestCase
|
313
|
+
test "show action" do
|
314
|
+
get :show, :id => "1", :subdomains => [ "admin" ]
|
315
|
+
assert_response :success # or whatever
|
316
|
+
end
|
317
|
+
end
|
318
|
+
</pre>
|
319
|
+
|
320
|
+
Notice the <code>:subdomains => [ "admin" ]</code> in the hash passed to the <code>get</code> method. This is the additional requirement for testing controller actions which lie under a subdomain route. Your tests won't work without it. The same applies for <code>post</code>, <code>put</code> and <code>delete</code>.
|
321
|
+
|
322
|
+
(For testing the model-based subdomain routes, <code>:subdomains => :city_id</code> and <code>:city_id => "..."</code> would be added to the route's options hash. Check the specs for more examples, if you need them.)
|
323
|
+
|
324
|
+
It's a little bit ugly (and not too DRY) to have to list the subdomains for the route in the test. Want to change the actual subdomains you are using? You'll have to change your tests as well. But that's the way it goes. (One way to avoid this brittleness, at least, would be to assign the subdomains to a constant and use the constant in your routes and tests.)
|
325
|
+
|
326
|
+
It's easy to figure out what <code>:subdomain</code> option you should pass. Just look it up in your routes by typing <code>rake routes</code> at the console:
|
327
|
+
|
328
|
+
<pre>
|
329
|
+
admin_users GET /users(.:format) {:action=>"index", :subdomains=>["admin"], :controller=>"admin/users"}
|
330
|
+
POST /users(.:format) {:action=>"create", :subdomains=>["admin"], :controller=>"admin/users"}
|
331
|
+
new_admin_user GET /users/new(.:format) {:action=>"new", :subdomains=>["admin"], :controller=>"admin/users"}
|
332
|
+
edit_admin_user GET /users/:id/edit(.:format) {:action=>"edit", :subdomains=>["admin"], :controller=>"admin/users"}
|
333
|
+
admin_user GET /users/:id(.:format) {:action=>"show", :subdomains=>["admin"], :controller=>"admin/users"}
|
334
|
+
PUT /users/:id(.:format) {:action=>"update", :subdomains=>["admin"], :controller=>"admin/users"}
|
335
|
+
DELETE /users/:id(.:format) {:action=>"destroy", :subdomains=>["admin"], :controller=>"admin/users"}
|
336
|
+
city_reviews GET /reviews(.:format) {:action=>"index", :subdomains=>:city_id, :controller=>"city/reviews"}
|
337
|
+
city_review GET /reviews/:id(.:format) {:action=>"show", :subdomains=>:city_id, :controller=>"city/reviews"}
|
338
|
+
</pre>
|
339
|
+
|
340
|
+
The <code>:subdomain</code> option you need to use is listed right there in the right-most column of each route.
|
341
|
+
|
342
|
+
h2. Testing Subdomain Routes
|
343
|
+
|
344
|
+
An underlying assumption in the Rails routing code is that the _path_ is all that's needed to specify an URL, since the host is assumed to be fixed and irrelevant. In some parts of the routing assertions code, this assumption is fairly tightly entangled in the code. Obviously, for subdomain routes, it's an invalid assumption.
|
345
|
+
|
346
|
+
Augmenting Rails' <code>assert_generates</code> and <code>assert_recognizes</code> methods to allow for a changeable host is not really a practical or sensible option. Instead, SubdomainRoutes adds some new assertions specifically for testing subdomain routes.
|
347
|
+
|
348
|
+
h3. Testing Recognition
|
349
|
+
|
350
|
+
The signature for Rails' traditional <code>assert_recognizes</code> method looks like this:
|
351
|
+
|
352
|
+
<pre>
|
353
|
+
def assert_recognizes(expected_options, path, extras={}, message=nil)
|
354
|
+
</pre>
|
355
|
+
|
356
|
+
The <code>expected_options</code> path is options hash describing the route that should be recognised (always including <code>:controller</code> and <code>:action</code>, as well as any other parameters that the route might produce). The <code>path</code> can either be a string representing the path, or a hash with <code>:path</code> and <code>:method</code> values (if you need to specify an HTTP method other than GET).
|
357
|
+
|
358
|
+
For <code>assert_recognizes_with_host</code> the same arguments are kept, since the <code>:host</code> can be passed as another option in the <code>path</code> hash. The <code>:host</code> option represents what host should be set in the <code>TestRequest</code> that's used to recognise the path. (Unlike traditional routes, the subdomain, and hence the host, is required for recognition of the route.)
|
359
|
+
|
360
|
+
So a typical passing recognition test for the user index route would be:
|
361
|
+
|
362
|
+
<pre>
|
363
|
+
test "admin_users route recognition" do
|
364
|
+
assert_recognizes_with_host(
|
365
|
+
{ :controller => "admin/users", :action => "index", :subdomains => [ "admin" ] },
|
366
|
+
{ :path => "/users", :host => "admin.example.com" })
|
367
|
+
end
|
368
|
+
</pre>
|
369
|
+
|
370
|
+
Notice the correct subdomain for this route is specified in the host. Note also the annoying <code>:subdomains</code> value in the first options hash. It needs to be there as well, to specify the route.
|
371
|
+
|
372
|
+
h3. Testing Generation
|
373
|
+
|
374
|
+
Testing route generation is a little more involved. The Rails assertion is as follows:
|
375
|
+
|
376
|
+
<pre>
|
377
|
+
def assert_generates(expected_path, options, defaults={}, extras={}, message=nil)
|
378
|
+
</pre>
|
379
|
+
|
380
|
+
This method asserts that <code>expected_path</code> (a string) is the path generated by the route <code>options</code>. But with subdomain routes, the generated route may also depend on the current host - if the subdomain for the route is different than the current host, the host will be forced to the new subdomain.
|
381
|
+
|
382
|
+
To allow testing of this behaviour, the <code>assert_generates_with_host</code> method is introduced. This assertion allows you to specify the current host, as well as the host that the new route should have (if different):
|
383
|
+
|
384
|
+
<pre>
|
385
|
+
def assert_generates_with_host(expected_path, options, host, defaults={}, extras={}, message=nil)
|
386
|
+
</pre>
|
387
|
+
|
388
|
+
Notice the additional third argument, <code>host</code>, which you should set to the current host (i.e. the host under which the route is being generated).
|
389
|
+
|
390
|
+
Now to test an example route. First, test the case where the host doesn't change:
|
391
|
+
|
392
|
+
<pre>
|
393
|
+
test "admin_users route generation for the same subdomain" do
|
394
|
+
assert_generates_with_host(
|
395
|
+
"/users",
|
396
|
+
{ :controller => "admin/users", :action => "index", :subdomains => [ "admin" ] },
|
397
|
+
"admin.example.com")
|
398
|
+
end
|
399
|
+
</pre>
|
400
|
+
|
401
|
+
The assertion in this test is saying that, for _admin.example.com_, the index route should generate <code>"/users"</code> as the path, and not change the host. The test passes as this is expected behaviour.
|
402
|
+
|
403
|
+
The second test case covers the case of generating the route from a host with a different subdomain:
|
404
|
+
|
405
|
+
<pre>
|
406
|
+
test "admin_users route generation for a different subdomain" do
|
407
|
+
assert_generates_with_host(
|
408
|
+
{ :path => "/users", :host => "admin.example.com" },
|
409
|
+
{ :controller => "admin/users", :action => "index", :subdomains => [ "admin" ] },
|
410
|
+
"www.example.com")
|
411
|
+
end
|
412
|
+
</pre>
|
413
|
+
|
414
|
+
Here the usage diverges from <code>assert_generates</code>: instead of passing a string as the expected path, a hash is passed. As with <code>assert_recognizes</code>, the hash is used to specify both the <code>:path</code> and the <code>:host</code> that the route should generate. The above test passes because the route changes the subdomain from _www_ to _admin_. (This only occurs in a single-subdomain route, of course.)
|
415
|
+
|
416
|
+
h3. Use with RSpec
|
417
|
+
|
418
|
+
The subdomain routing assertions won't help you much if you're using RSpec or some other testing framework. Your best bet is to wrap each assertion up in its own class, just as RSpec does with <code>assert_recognizes</code> in its <code>route_for</code> method (check the rspec-rails source code). This shouldn't be too hard to do.
|
419
|
+
|
420
|
+
|
421
|
+
Copyright (c) 2009 Matthew Hollingworth. See LICENSE for details.
|