Service Objects with ActiveModel and SimpleDelegator

Login case

> Users can login with password and email
> Users cannot login with wrong password / email
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
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

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
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
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

Conclusion

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Alireza Bashiri

Alireza Bashiri

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