So I\'m building a Rails site that needs routes based on two different types
I have a Language model and a Category model
So I need to be able to go to a la
Two routes get '/:language' and get '/:category' are exactly same for rails.
Rails router can't differentiate between /books
and /ruby
.
In both cases rails would just look for a route in routes.rb
which looks something like /something
, it will pick the first match and dispatches the route to the specified controller's action.
In your case,
all the requests with /something
format
would be matched to
get '/:language', to: "top_voted#language"
There is a nice solution to that problem using routes constraints.
As the rails routing guide suggests, you could define routes constraints in a way that they check if a path belongs to a language or a category.
# config/routes.rb
# ...
get ':language', to: 'top_voted#language', constraints: lambda { |request| Language.where(name: request[:language]).any? }
get ':category', to: 'top_voted#category', constraints: lambda { |request| Category.where(name: request[:category]).any? }
The order defines the priority. In the above example, if a language and a category have the same name, the language wins as its route is defined above the category route.
If you want to make sure, all paths are uniqe, an easy way would be to define a Permalink
model and using a validation there.
Generate the database table: rails generate model Permalink path:string reference_type:string reference_id:integer && rails db:migrate
And define the validation in the model:
class Permalink < ApplicationRecord
belongs_to :reference, polymorphic: true
validates :path, presence: true, uniqueness: true
end
And associate it with the other object types:
class Language < ApplicationRecord
has_many :permalinks, as: :reference, dependent: :destroy
end
This also allows you to define several permalink paths for a record.
rails_category.permalinks.create path: 'rails'
rails_category.permalinks.create path: 'ruby-on-rails'
With this solution, the routes file has to look like this:
# config/routes.rb
# ...
get ':language', to: 'top_voted#language', constraints: lambda { |request| Permalink.where(reference_type: 'Language', path: request[:language]).any? }
get ':category', to: 'top_voted#category', constraints: lambda { |request| Permalink.where(reference_type: 'Category', path: request[:category]).any? }
And, as a side note for other users using the cancan gem and load_and_authorize_resource
in the controller: You have to load the record by permalink before calling load_and_authorize_resource
:
class Category < ApplicationRecord
before_action :find_resource_by_permalink, only: :show
load_and_authorize_resource
private
def find_resource_by_permalink
@category ||= Permalink.find_by(path: params[:category]).try(:reference)
end
end
This sounds like an architecture issue. If the clean urls are important to you, here's how I would set this up:
Create a new model called Page
, which will belong to a specific resource (either a Category or a Language).
class Page < ActiveRecord::Base
belongs_to :resource, polymorphic: true
end
The database columns would be id
, resource_type
, resource_id
, path
, and whatever else you want to hang on there.
Your other models would have the reciprocal relationship:
has_many :pages, as: :resource
Now you can route using a single path, but still have access to the resources from different classes.
Router:
resources :pages, id: /[0-9a-z]/
Controller:
class PagesController
def show
@page = Page.find_by_path(params[:id])
end
end
In the view, set up partials for your resource models, and then render them in pages/show
:
=render @page.resource
An example page would be #<Page path: 'ruby', resource: #<Language name: "Ruby">>
, which would be available at /pages/ruby
. You could probably route it such that /ruby
routes to PagesController
, but then you're severely limiting the number of routes you can use elsewhere in the app.
Since I'm a few months late, you've probably already figured something out, but for the future people, constraints might be what you're looking for. You can either set up a lambda that decides based on the request object, or you can set up a class that implements a matches?
method for the router to call.
http://guides.rubyonrails.org/routing.html#advanced-constraints