Service Objects with ActiveModel and SimpleDelegator

One of the worst practices in Ruby On Rails is to clutter the Gemfile to do basics as authentication, authorization, validation, etc. considering the fact that they are practiced for so long and they can be accomplished using just the batteries already included in Ruby On Rails; service objects are no different for which developers often end up using different gems to do such a simple pattern.

In the favour of simplicity and the fact that I never trust dependencies due to to the complexity they bring in I’ve been using ActiveModel and SimpleDelegator to apply service objects through a consistent interface across the controllers as well as avoiding dependency clutter and here’s the results.

Login case

> Users can login with password and email
> Users cannot login with wrong password / email

So we have 2 cases which we have to cover in our implementation to do that I use ActiveModel::Validations to validate the parameters I receive from the controller and using save method to keep interface consistency.

class Login
include ActiveModel::Model
attr_accessor :email, :password, :user
validates :email, presence: true
validates :password, presence: true
validates :user, presence: {message: "email or password is wrong"}
def initialize(params)
super(params.require(:user).permit(:email, :password))
@user = User.find_by(email: email, status: :active)&.authenticate(password)
end
def save
return if invalid?
# This will generate and refresh user's token and return it
Tokenizer.generate_and_refresh!(@user)
end
end

Here if no user found it will set @user variable to nil and user presence validation will handle the error which keep us from writing an error handling part except a message.

And the controller would be like;

class LoginController < ApplicationController
skip_before_action :authenticate
INCLUDED = %i[positions]
def create
login = Login.new(params)
if (user = login.save)
render json: user, include: INCLUDED
else
render json: login, status: :bad_request
end
end
private def serializer
UserSerializer
end
def user_params
params.require(:user).permit(:email, :password)
end
end

This is already battle-tested in production and it works like a charm.

Approval flow

ActiveRecord callbacks

Contextual validations with on: :context

validate :order_is_picked_before_packing, on: :packing

Decorators + State machines

> MVPs cannot be approved before they're submitted
> MVPs cannot be approved more than once by same user
> MVPs cannot be approved unless they have enough approvals from different users

Let’s take look at the code;

class ApprovedMvp < SimpleDelegator
include ActiveModel::Validations
def save(user)
return unless valid?
if in_state?(:draft)
errors.add(:minimal_viable_product, "it should be submitted first")
return
end
if approvals.find_by(user: user)
errors.add(:minimal_viable_product, "already been approved by user")
return
else
approvals.create!(user: user)
end
if has_enough_approvals?
transition_to!(:approved)
else
super()
end
end
end

Here we populate a decorated MVP with contextual errors and pass it up to the controller

class UseCases::MinimalViableProductsController < ApplicationController
INCLUDED = UseCasesController::INCLUDED
include UseCaseScoped def approve
result = ApprovedMvp.new(minimal_viable_product)
if result.save(@current_user)
render json: result.use_case, include: INCLUDED
else
render json: result
end
end

In which will be handled by a ErrorSerializer and a concern called ActsAsJSONAPI which I’ve written to handle different cases of serialization and error handling without dependencies and a lot of complexity which you can checkout here.

Conclusion

And don’t get me wrong I don’t mean to reinvent the wheel but don’t be afraid to build your own tool since each app has different needs and not all gems/libraries are going to fit yours. Bad cases that you should never implement by yourself except for practice is encryption or an premature abstraction (framework, etc) that’s takes your more than 1 day or 2.

I hope you’ve enjoyed it

THIS IS COPY OF MY OWN PERSONAL BLOG POST

Feel free to checkout my personal blog here
http://blog.alirezabashiri.com/

∞ Travel | Coding | Lifestyle ⦿ Bangkok | ✧ #freelancer ▷ Got a project? ⭣ www.upwork.com/fl/al3rez