Skip to content

c3gdlk/simple_resource_controller

Repository files navigation

SimpleResourceController

The simple_resource_controller gem is a lightweight analog of the good old inherited_resources. The main purpose is to save developers time for writing simple CRUD controllers.

Important! Please remember, that this tool was created to help you and not vice versa. Do not try to fight with it. At the end, it’s just a stupid code of the stupid crud controllers.

The main ideas

  1. This gem allows you to create CRUD controller with minimum configuration
  2. You do not need to cover these controllers with any tests because gem already covered well
  3. It just a simple tool with a set of important configurations. It does not want to be a God tool and cover all your use cases
  4. After reading this documentation you will get a clear understanding how it works under the hood.
  5. The best practices guide

The last point is the most important. When a tool makes so many “magic” it should provide a best practices guide.

Installation

Add this line to your application's Gemfile:

gem 'simple_resource_controller'

And then execute:

$ bundle

Or install it yourself as:

$ gem install simple_resource_controller

Require it in your config/application.rb

require 'simple_resource_controller'

Usage

A short example

class AnotherArticlesController < ApplicationController
  resource_actions :crud
  resource_class 'Article'
  paginate_collection 10

  private

  def after_save_redirect_path
    admin_articles_path
  end
  alias :after_destroy_redirect_path :after_save_redirect_path

  def after_save_messages
    { notice: 'Saved!' }
  end

  def permitted_params
    params.require(:article).permit(:title)
  end
end

API examples

Gem suppports jbuilder and activemodel_serializer as API serializers

JBuilder

class AnotherArticlesController < ApplicationController
  resource_actions :crud
  resource_api jbuilder: true

  private

  def permitted_params
    params.require(:article).permit(:title)
  end
end

But you will also need to add all view files for your actions, including new and edit.

You can find example inside tests.

ActiveModel::Serializer

Minimum config:

class AnotherArticlesController < ApplicationController
  resource_actions :crud
  resource_api activemodel_serializer: { }

  private

  def permitted_params
    params.require(:article).permit(:title)
  end
end

All options:

class AnotherArticlesController < ApplicationController
  resource_actions :crud
  resource_api activemodel_serializer: { collection_serializer: MyCollectionSerializer, resource_serializer: MyArticleSerializer, error_serializer: MyErrorSerializer }

  private

  def permitted_params
    params.require(:article).permit(:title)
  end
end

With redefined options:

Important note: you should always set the error_serializer

class AnotherArticlesController < ApplicationController
  resource_actions :crud
  resource_api activemodel_serializer: { collection_serializer: MyCollectionSerializer, resource_serializer: MyArticleSerializer, error_serializer: MyErrorSerializer }

  # will use the AnotherArticleSerializer when success
  # will use the MyErrorSerializer when failure
  def update
    update! serializer: AnotherArticleSerializer
  end

  private

  def permitted_params
    params.require(:article).permit(:title)
  end
end

You can find example inside tests.

Controller configuration

The first required configuration is an action name.

Please note! The actions definition are inherited, so be careful with the base classes.

resource_actions :index, :show, :new, :create
resource_actions :crud # an alias for all actions

Other settings

resource_class ‘User’

Allow specifying the model class. Will get from controller name by default.

paginate_collection 10

Will use the kaminari gem for pagination. It will add page(params[:page]).per(params[:per_page] || 10) to your scopes.

Of course, I could make this config more flexible, but it contradicts the gem philosophy. Keep it as simple as possible. Creating controllers with that tool should be simple and new developers should not spend much time trying to understand what all these configs mean.

resource_context :current_user

The last and more complex setting is the resource_context. Please avoid it if it isn’t clear to you how it works inside.

But before describing it I would like to talk about some issues of the inherited_resources gem. The Inherited resources was one of my favorite gems for a long time. But when my team grows I got that for Rails newcomers is so hard to dive in code written with it. Hard to understand what all these begin_of_association_chain, end_of_association_chain and the other methods do. How to combine them together and use it properly. At the end it’s just a stupid CRUD controller, developers should not spend much time to figure out how it works. But this is the issue both gems are trying to solve - do not spend any time on simple things.

So, let's back to the resource_context option. It the most “magic” config, but it has only two possible usages.

  1. There is a single parameter. In that case, the method with parameter name be the base and gem will build the relation from the controller name
  2. There are several parameters. In this case, it also uses the method with parameter name as a base. But there is no any additional magic here. All next params will specify the chain (In the first case it was defined by controller name)

It’s hard to get it from the description, but the examples below are pretty clear. Please note, that the commented part is the code generated by the gem.

class ArticlesController < ApplicationController
  resource_actions :index

  #def index
    #@articles = Article.all
  #end
end
class ArticlesController < ApplicationController
  resource_actions :index
  resource_context :current_user

  # current_user - from params
  # .articles - from controller name
  #def index
    #@articles = current_user.articles
  #end
end
class ArticlesController < ApplicationController
  resource_actions :index
  resource_context :current_user, :articles, :recent

  # no magic with controller name, explicit chain
  #def index
    #@articles = current_user.articles.recent
  #end
end
class ArticlesController < ApplicationController
  resource_actions :index
  resource_context :category, :articles

  #def index
    #@articles = category.articles
  #end

  private

  def category
    current_user.categories.find(params[:category_id])
  end
end

If the method doesn’t exist it could try to generate it by supposing that this is a case of the nested resources.

class ArticlesController < ApplicationController
  resource_actions :index
  resource_context :category, :articles

  #def index
    #@articles = Category.find(params[:category_id]).articles
  #end
end

The last example details

class ArticlesController < ApplicationController
  resource_actions :index
  resource_context :category, :articles
  #def index
    #@articles = Category.find(params[:category_id]).articles
  #end
end

Because the ArticlesController doesn’t define a def category; end method gem will try to build the class name and find the record by params[:category_id] `

The has_scope gem also will automatically work if any scopes specified.

class ArticlesController < ApplicationController
  resource_actions :index
  resource_context :category, :articles
  paginage_collection 10
  has_scope :recent, type: :boolean
  #def index
    #@articles = apply_scopes(Category.find(params[:category_id]).articles).page(params[:page]).per(params[:per_page] || per_page)
  #end
end

That's all. There are no other available settings. Really simple, isn’t it?

The template methods

Gem was developed with the Template pattern, so there are a few methods you can override your needs.

The required overrides.

You need to define next methods only if you use related actions

def after_save_redirect_path
  raise 'Not Implemented'
end

def after_destroy_redirect_path
  raise 'Not Implemented'
end

def permitted_params
  raise 'Not Implemented'
end

Flash messages

By default there are no any messages

def after_create_messages
  after_save_messages
end

def after_update_messages
  after_save_messages
end

def after_destroy_messages
  nil
end

def after_save_messages
  nil
end

You can define any of these methods, They should return a Hash with types and messages

def after_destroy_messages
  { success: 'Record was successfully deleted' }
end

Fetch data methods

If the resource_context is not enough or you find it too complex, just redefine the basic methods collection and resource. Both already defined as helper methods and could be used inside views. Don’t forget about the memoization.

def resource
  @article ||= Article.find(params[:id])
end

def collection
  @articles ||= Article.all
end

Controller actions

Gem based on the responders one, so you can easy redefine the basic methods. It has the same interface as the inherited_resources.

class ArticlesController < ApplicationController
  resource_actions :update

  def update
    @article = Article.find(params[:id])
    @article.category = Category.find(params[:category_id])

    update!
  end
end
class ArticlesController < ApplicationController
  resource_actions :update

  def update
    update! do |format|
      unless resource.errors.empty? # failure
        format.html { redirect_to project_url(resource) }
      end
    end
  end
end
class ArticlesController < ApplicationController
  resource_actions :update

  def update
    update! do |success, failure|
      failure.html { redirect_to project_url(resourcet) }
    end
  end
end

A bit more about flashes and redirects

Instead of

class ArticlesController < ApplicationController
  resource_actions :create
  private
  def after_create_messages
    {notice: ‘Some message’}
  end
 end

You can write

class ArticlesController < ApplicationController

  resource_actions :create

  def create
    create! notice: ‘Some message’
  end
end

The second example looks more elegant, but it’s much easy to write tests for the first one. Because for the first example you just need to ensure that the after_create_messages return a correct hash. But for the second one, you will need to write a test for the create method from scratch because you redefine it and can’t rely on the gem tests.

Possible issues

I’ve used a contract development to raise exceptions if something configured wrong. Please check the exceptions below you can get.

Not Implemented

You didn’t define the template methods. Please check the documentation above.

Messages should be specified with Hash

Flash messages should be configured as a hash

Please install Kaminari

You are trying to use pagination but gem kaminary is missing

#{context} hasn't relation #{scope}

Wrong usage of the resource_context. Please remember that there is only single parameter it will build the second one from the controller name

class ArticlesController < ApplicationController
  resource_actions :index
  resource_context :current_user

  #def index
    #@articles = current_user.articles
  #end
end

So, the User model should have has_many : articles relation

Undefined context method #{context_method}

Wrong usage of the resource_context. If there is no method with the same name or relevant param - it will cause that exception. Please check the documentation about resource_context

Could not find model #{class_name} by param #{context_method}_id

The similar exception to the previous one, but in that case, there is a relevant parameter, but there is no a Rails model with relevant name.

class ArticlesController < ApplicationController
  resource_actions :index
  resource_context :category, :articles

  #def index
    #@articles = Category.find(params[:category_id]).articles
  #end
end

You will get this exception if the Category model is missing.

Too many Rails magic. Please check your code

This is my favorite one. The responders gem follows the next logic - if a record is valid it means that it was saved to the database. But there is a case when too many Rails magic can cause valid records which weren’t saved to the DB. Yeah, sometimes Rails hurts =) If you will get this exception - check your ActiveRecord callbacks, your code has some smells.

error_serializer should be configured for the activemodel API

You are trying to use the resource_api DSL to setup the activemodel_serializer, faced with the validation error in create or update action. But didn't configured an error serializer: resource_api activemodel_serializer: { error_serializer: MyErrorSerializer }

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/c3gdlk/simple_resource_controller. This project is intended to be a safe, welcoming space for collaboration.

Not work for now =((

bundle exec mutant --include lib --require simple_resource_controller --use rspec "SimpleResourceController*"

License

The gem is available as open source under the terms of the MIT License.

About

A small gem to replace the inherited resources

Resources

License

Code of conduct

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages