Dynamic Rails routes based on database models

主宰稳场 提交于 2019-11-30 10:03:39

There is a nice solution to that problem using routes constraints.

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.

Using a Permalink model

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

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"

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!