Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Dynamic Rails routes based on database models

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 language route /ruby to see top ruby resources and also go to /books to see top books in all languages

I tried routes like this

get '/:language', to: "top_voted#language"
get '/:category', to: "top_voted#category"

the problem with that was the logic could not figure out the difference between the two and caused some conflicts on the back end

I also tried this

Language.all.each do |language|
  get "#{language.name}", to: "top_voted#language", :language => language.name
end

Category.all.each do |category|
  get "#{category.name}", to: "top_voted#category", :category => category.name
end

However the problem is Heroku where we are deploying this does not allow database calls in the routes. Is there an easier way to do this? We need to be able to dynamically generate these routes somehow.

like image 283
Joel Smith Avatar asked Mar 06 '14 21:03

Joel Smith


People also ask

How many types of routes are there in Rails?

Rails RESTful Design which creates seven routes all mapping to the user controller. Rails also allows you to define multiple resources in one line.

How do Rails routes work?

Rails routing is a two-way piece of machinery – rather as if you could turn trees into paper, and then turn paper back into trees. Specifically, it both connects incoming HTTP requests to the code in your application's controllers, and helps you generate URLs without having to hard-code them as strings.

What are Rails resource routes?

Resource routing allows you to quickly declare all of the common routes for a given resourceful controller. A single call to resources can declare all of the necessary routes for your index , show , new , edit , create , update , and destroy actions.


1 Answers

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
like image 163
fiedl Avatar answered Oct 21 '22 00:10

fiedl