CommandServiceObject
Rails Generator for command service object.
Theory
Command Design Pattern consists of Command Object
and Service Object
(Executor), Command object is responsible for containing Client
requests and run input validations on it to ensure that the request is valid and set default values, then Service Object
applies the business logic on that command.
Implementation
Service consists of several objects { Command Object
Usecase Object
And Error Object
(business logic error) }.
-
Command: the object that responsible for containing
Client
requests and run input validations it's implemented using Virtus gem and can useactiverecord
for validations and it's existed undercommands
dir. -
Usecase: this object responsible for executing the business logic, Every
usecase
should execute one command type only so that command name should be the same as usecase object name, usecase object existed under 'usecases` dir. - Micros: Small reusable logic under the same service.
- Externals: Simple ruby module works as a service interface whenever you wanna call any external service or even service that lives under the same project you should use it.
- Queries: This dir is the only entry point for you to get any data form a service.
- Listeners: An event listener that waits for an event outside the service to occur.
- Entities: Many objects are not fundamentally defined by their attributes, but rather by a thread of continuity and identity.
Result Object
In case of successful or failure ApplicationService
the responsible object for all services will return service_result
object this object contain value!
method containing successful call result, and errors
method containing failure errors
objects.
Helpers:
- Fail: You can use
fail!
helper to raise business logic failures, ex:fail!('user should not have any active cards')
. - Check: To do business logic validations you can use
check!
helper ex:check!('user should not have any active cards') { user.active_cards.empty? }
, if the given block returns false then it will raise fail! with the given message.
You can check if the result successful or not by using
ok?
method.
Installation
Add this line to your application's Gemfile:
gem 'command_service_object'
And then execute:
bundle
Or install it yourself as:
gem install command_service_object
Next, you need to run the generator:
rails generate service:install
Usage
$ rails g service [service_name] [usecases usecases]
Generate Service ex
$ rails g service auth login output
app/services/
├── application_service.rb
├── auth_service
│ ├ external/
│ ├── commands
│ │ └── login.rb
│ └── usecases
│ ├── login.rb
│ └── micros
├── case_base.rb
└── service_result.rb
Generate micros ex
$ rails g service:micro auth generate_jwt_token_for
app/services/
├── auth_service
│ └── usecases
│ └── micros
│ └── generate_jwt_token_for.rb
# app/services/auth_service/usecases/micros/generate_jwt_token_for.rb
module PaymentService::Usecases::Micros
class GenerateJwtTokenFor < CaseBase
def call
# <Payload>
end
end
end
then you can edit command params
you can read Virtus gem docs for more info.
# app/services/auth_service/commands/login.rb
# frozen_string_literal: true
module AuthService::Commands
class Login < CommandBase
# You can read Virtus gem doc for more info.
# https://github.com/solnic/virtus
# Attributes
# attribute :REPLACE_ME, String
# Validations
# validates :REPLACE_ME, presence: true
end
end
and then add your business logic
# app/services/auth_service/usecases/login.rb
# frozen_string_literal: true
module AuthService::Usecases
class Login < CaseBase
include CommandServiceObject::Hooks
micros :generate_jwt_token_for
#
# Your business logic goes here, keep [call] method clean by using private
# methods for Business logic.
#
def call
token = generate_jwt_token_for(cmd.user)
replace_me
output
end
def output
# return entity object
end
# This method will run if call method raise error
def rollback
# rollback logic
end
def allowed?
# policies loginc for issuer
# ex:
#
# return false if issuer.role != :admin
true
end
private
def replace_me
# [business logic]
end
end
end
External APIs or Services
You can wrap external apis or services under external/
dir
ex
module External
module StripeService
extend self
def charge(customer:, amount:, currency:, description: nil)
Stripe::Charge.create(
customer: customer.id,
amount: (round_up(amount, currency) * 100).to_i,
description: description || customer.email,
currency: currency
)
end
end
end
usage from controller
class AuthenticationController < ApplicationController
default_service :auth_service
def Login
cmd = command.new(params) # AuthService::Commands::Login.new
result = execute(cmd)
if result.ok?
render json: result.value!.as_json, status: 201
else
render json: { message: result.error }, status: 422
end
end
end
Development
After checking out the repo, run bin/setup
to install dependencies. Then, run rake spec
to run the tests. You can also run bin/console
for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install
. To release a new version, update the version number in version.rb
, and then run bundle exec rake release
, which will create a git tag for the version, push git commits and tags, and push the .gem
file to rubygems.org.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/adham90/command_service_object. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct.
License
The gem is available as open source under the terms of the MIT License.
Code of Conduct
Everyone interacting in the CommandServiceObject project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.