Dynamic Rails Routes Based on Database Models

Dynamic Rails routes based on database models

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

Rails - dynamic routes inside an each block

I would suggest an alternative solution where you "unnest" the cars resource - and just provide a index route for the cars belonging to a certain user:

# routes.rb
resources :cars
resources :users do
resources :cars, module: 'users', only: [:index]
end

# app/controller/cars_controller.rb
class CarsController < ApplicationController
# GET /cars
def index
@cars = Car.all
end

# show, create, delete, new...
end

# app/controller/users/cars_controller.rb
class Users::CarsController < ApplicationController
# GET /users/:user_id/cars
def index
@user = User.includes(:cars).find(params[:user_id])
@cars = @user.cars
end
end

Depending on the context you can move more of the collection routes (new, create) to Users::CarsController if you for example are able to create cars for other users. Nesting member routes (that act on a single record)
is seldom necessary. You can avoid it by using the shallow: true option:

resources :users do
resources :cars, shallow: true
end

This lets you route to a car by simply doing:

link_to(@car.name, car_path(@car))
# or
link_to(@car.name, @car)

If you decide to keep your current setup you route to nested resource by using an array or keywords:

link_to(@car.name, user_car_path(user: @user, id: @car))
# or
link_to(@car.name, [@user, @car])

rails 5 dynamic routing redirections based on model attributes

Found a solution on this post here.

I need to restart the server so newly created route get loaded after a Place object creation. Not sure if I am doing it right though but it works..

#config/route.rb

Rails.application.routes.draw do
Place.all.each do |pl|
get "/#{pl.shortlink}" => redirect("/users/sign_up?q=#{pl.id}&t=#{pl.token}")
end
end

In order to load the newly created route when adding a Place, I added

#models/place.rb

after_create :add_shortlink
...
def add_shortlink
Rails.application.reload_routes!
end

This will not work on Heroku, issue addressed is here

Dynamic routes on runtime in Rails

I needed to figure out building routes off a database model myself in a Rails 4 application (which is called "ComingSoon" in the examples below. I wanted pages that could be edited on the back-end and given a user-friendly name, which is stored in the Page#name field. So "About Us" titled page typically becomes "about_us" name, which leads to "http://localhost:3000/about_us" The following is the technique I came up with:

Create a new model in app/models/dynamic_router.rb

class DynamicRouter
def self.load
ComingSoon::Application.routes.draw do
Page.all.each do |pg|
get "/#{pg.name}", :to => "pages#show", defaults: { id: pg.id }, as: "pages_#{pg.name}"
end
end
end

def self.reload
ComingSoon::Application.routes_reloader.reload!
end
end

The key above is that I pass the page's id as one of the parameters, so look up is still on the Page#id field, which is, IMHO, a lot better than using the friendly plugin or lookups on slugerized values.

Add the following line to your config/routes.rb

ComingSoon::Application.routes.draw do

# ...

DynamicRouter.load
end

Finally, when the Page is updated, we need to reload the routes, so add an after_safe callback on the Page model:

class Page < ActiveRecord::Base
after_save :reload_routes

def reload_routes
DynamicRouter.reload
end
end

I plan to refine this further to only reload routes if the name attribute is changed and perhaps simply edit the existing route rather than reloading everything if performance proves to be an issue (which at the moment, its not).

Rails: Create Dynamic routes, controllers


Is there a way, probably by using Meta Programming or something else we are able to query the database to get the list of views and generate the necessary routes and code to return a valid response.

Yes, but the actual implementation depends on the database in use. On Postgres you can get a list of the views by querying pg_catalog.pg_views:

pg_views = Arel::Table.new('pg_catalog.pg_views')
query = pg_views.project(:schemaname, :viewname)
.where(
pg_views[:schemaname].in('pg_catalog', 'information_schema').not
)

result = ActiveRecord::Base.connection.execute(query)
# ...

But a framework change is in order here. Does a view necissarily need to correspond to its own route or could you create a better RESTful design?

If you are for example listing by year/month you could easily setup a single route which covers it:

namespace :api do
namespace :v1 do
resources :leases do
get '/leases/by_month/:year/:month', as: :by_month, action: :by_month
end
end
end

Can you setup a model with metaprogramming?

Absolutely. Classes in Ruby are first-class objects and you can create them with Class.new:

# Setup a base class for shared behavior 
class ApplicationView < ActiveRecord::Base
self.abstract_class = true
end

str = 'Foo'
model = Class.new(ApplicationView) do |klass|
# do your model specific thing here...
end

# Classes get their name by being assigned to a constant
ApplicationView.const_set(str, model)
ApplicationView::Foo.all # SELECT * FROM foos;

ActiveRecord and ActiveModel don't really like anonymous classes (classes that are not assigned to a constant) since they do a bunch of assumptions based on the class name. Here we are nesting the constants in ApplicationView simply to avoid namespace crashes.

Another methods thats sometimes used in libary code is to create a string containing the code to define the class and eval it. ​

You can also setup a single model that queries different tables/views.

Can you setup controllers and views (as in MVC) with metaprogramming?

Yes. But you shouldn't need it. You can simply create generic controllers that can handle a variety of models. Remember that the idea that a controller corresponds to a single model is just a convention that applies in trivial cases.

How do you name rails routes based on a model?

You can use wildcards in your url to set this up. In your config/routes.rb put:

get '/:city', to: 'cities#show'

And then you can access the wildcard element in your show method with

params[:city]

for example

def show
@city = City.find_by_name(params[:city])
end


Related Topics



Leave a reply



Submit