Nifty Services comes to solve your Ruby applications(including but not limited to Rails, Grape, Sinatra, and plain Ruby) code mess with simplicity in mind!
NiftyServices provides a very nifty, simple & clear API to organize and reuse your application domain logic in plain Ruby Services Objects turning your codebase in a very extensible, standardized and reusable components.
Most important: You and your team win what I consider the best benefit when using Nifty Services: Easily and scalable maintained code. Believe me, you'll fall in π with this small piece of code, keep reading!
This gem was designed and conventioned to be used specially with Web API applications, but this is just a convention, you can use it even with shoes (for desktop apps) applications if you want, for example.
As you can see, this README needs some time to be full read, but is very difficulty to explain all things, concepts and philosophy of this gem without writing a lot, we can't escape this :(
But remember one thing: This is a tecnical documentation, not a blog post, I'm pretty sure you can take about 30 minutes + some cups of β to better understand all NiftyServices can do for you and your project. Good reading, and if you have some question, please let me know.
- Dafuck is this gem
- Conventions
- Installation
- Usage
- Basic Service Markup
- How a Service must be created
- Services API
- Handling Success & Error Responses
- CRUD Services
- I18n Setup
- Callbacks
- Configuration
- Web Frameworks integration
- Basic Services class Markups
- CLI Generators
- Roadmap
- Development
- Contributing
- License - MIT
Below, some very importants things about conventions for this cute π :)
Each service class is responsible for perform exactly one single task, say goodbye for code (most important: logic) duplication in your code. Beside this, one of the aim of NiftyServices is to provide a very standardized code architecture, allowing developers to quickly develop and implement new features keeping the application codebase organized and stable.
Each service object must respond to #execute
instance method, which is allowed to be called just one time per instance.
#execute
method is responsible to perform code validation(parameter validation, access level control), execution(send mail, register users) and fire callbacks so you can execute hooks actions after/before success or execution fail.
When dealing with services objects, you will get a very rich objects to work with, forgot about getting only true or false
return values, one of the main purpose of objects it's to keep your code domain logic accessible and reusable, so your application can really take the best approach when responding to actions.
Think and implement security rules from the first minutes of live in your applications! NiftyServices strongly rely on Access Control Level(ACL) to perform actions, in other words, you will only allow authorized users to read, create, update or delete records in your database!
Now you know the basic concepts and philosophy of NiftyServices
, lets start working with this candy library?
Add this line to your application's Gemfile:
gem 'nifty_services', '~> 0.0.5'
And then execute:
$ bundle
Or install it yourself as:
$ gem install nifty_services
NiftyServices provide a start basic service class for generic code which is NiftyServices::BaseService
, the very basic service markup is demonstrated below:
class SemanticServiceName < NiftyServices::BaseService
def execute
execute_action do
success_response if do_something_complex
end
end
def do_something_complex
# (...) some complex bussiness logic
return true
end
private
def can_execute?
return forbidden_error!('errors.message_key') if some_condition
return not_found_error!('errors.message_key') if another_condition
return unprocessable_entity_error!('errors.message_key') if other_condition
# ok, this service can be executed
return true
end
end
service = SemanticServiceName.new(options)
service.execute
Lets work with a real and a little more complex example, an Service responsible to send daily news mail to users. The code below shows basically everything you need to know about services structure, such: entry point, callbacks, authorization, error and success response handling, so after understanding this little piece of code, you will be ready to code your own services!
class DailyNewsMailSendService < NiftyServices::BaseService
before_execute do
log.info('Routine started at: %s' % Time.now)
end
after_execute do
log.info('Routine ended at: %s' % Time.now)
end
after_initialize do
user_data = [@user.name, @user.email]
log.info('Routine Details: Send daily news email to user %s(%s)' % user_data)
end
after_success do
log.info('Success sent daily news feed email to user')
end
before_error do
log.warn('Something went wrong')
end
after_error do
log.error('Error sending email to user. See details below :(')
log.error(errors)
end
attr_reader :user
def initialize(user, options = {})
@user = user
super(options)
end
def execute
execute_action do
success_response if send_mail_to_user
end
end
private
def can_execute?
unless valid_user?
# returns false
return not_found_error!('users.not_found')
end
unless @user.abble_to_receive_daily_news_mail?
# returns false
return forbidden_error!('users.already_received_daily_news_mail')
end
return true
end
def send_mail_to_user
# just to fake, a real implementation could be something like:
# @user.send_daily_news_mail!
return true
end
def valid_user?
# check if object is valid and is a User class type
valid_object?(@user, User)
end
# you can use `default_options` method to add default { keys => values } to @options
# so you can use the option_enabled?(key) to verify if option is enabled
# or option_disabled?(key) to verify is option is disabled
# This default values can be override when creating new instance of Service, eg:
# DailyNewsMailSendService.new(User.last, validate_api_key: false)
def default_options
{ validate_api_key: true }
end
end
class User < Struct.new(:name, :email)
# just to play around with results
def abble_to_receive_daily_news_mail?
rand(10) < 5
end
end
user = User.new('Rafael Fidelis', 'rafa_fidelis@yahoo.com.br')
# Default logger is NiftyService.config.logger = Logger.new('/dev/null')
service = DailyNewsMailSendService.new(user, logger: Logger.new('daily_news.log'))
service.execute
I, [2016-07-15T17:13:40.092854 #2480] INFO -- : Routine Details: Send daily news email to user
Rafael Fidelis(rafa_fidelis@yahoo.com.br)
I, [2016-07-15T17:13:40.092987 #2480] INFO -- : Routine started at: 2016-07-15 17:13:40 -0300
I, [2016-07-15T17:13:40.093143 #2480] INFO -- : Success sent daily news feed email to user
I, [2016-07-15T17:13:40.093242 #2480] INFO -- : Routine ended at: 2016-07-15 17:13:40 -0300
I, [2016-07-15T17:12:10.954792 #756] INFO -- : Routine Details: Send daily news email to user
Rafael Fidelis(rafa_fidelis@yahoo.com.br)
I, [2016-07-15T17:12:10.955025 #756] INFO -- : Routine started at: 2016-07-15 17:12:10 -0300
W, [2016-07-15T17:12:10.955186 #756] WARN -- : Something went wrong
E, [2016-07-15T17:12:11.019645 #756] ERROR -- : Error sending email to user. See details below :(
E, [2016-07-15T17:12:11.019838 #756] ERROR -- : ["User has already received daily news mail today"]
I, [2016-07-15T17:12:11.020073 #756] INFO -- : Routine ended at: 2016-07-15 17:12:11 -0300
The code above demonstrate a very basic example of how dead easy is to work with Services, let me clarify some things to your better understanding:
-
β All services classes must inherit from
NiftyServices::BaseService
-
β For convention(but not a rule) all services must expose only
execute
(and of course,initialize
) as public methods. -
β
execute_action(&block)
MUST be called to properly setup things in execution context. -
β
can_execute?
must be ALWAYS implemented in service classes, ALWAYS, this ensure that your code will safely runned. Note: ANotImplementedError
exception will be raised if service won't define your owncan_execute?
method. -
β There's a very simple helper functions for marking result as success/fail (eg:
unprocessable_entity_error!
orsuccess_response
). -
β Simple DSL for actions callbacks inside current execution context. (eg:
after_success
orbefore_error
) Note: You don't need to use the DSL if you don't want, you can simply define the methods(such as:private def after_success; do_something; end
This is the very basic concept of creating and executing a service object, now we need to know how to work with responses to get the most of our services, for this, let's digg in the mainly public API methods of NiftyService::BaseService
class:
Below, a list of most common public accessible methods for any instance of service: (Detailed usage and full API list is available below this section)
service.success? # boolean
service.fail? # boolean
service.errors # array
service.response_status # symbol (eg: :ok)
service.response_status_code # integer (eg: 200)
So, grabbing our DailyNewsMailSendService
service again, we could do:
service = DailyNewsMailSendService.new(User.new('test', 'test@test.com'))
service.execute
if service.success? # or unless service.fail?
SentEmails.create(user: service.user, type: 'welcome_email')
else
puts 'Error sending email, details below:'
puts 'Status: %s' % service.response_status
puts 'Status code: %s' % service.response_status_code
puts service.errors
end
# trying to re-execute the service will return `nil`
service.execute
This is really great and nifty, no? But we already started, there's some really cool stuff when dealing with Restful API's actions, before entering this subject let's see how to handle error and success response.
To mark a service running as successfully, you must call one of this methods (preferencially inside of execute_action
block):
success_response # [200, :ok]
success_created_response [201, :created]
The first value in comments above is the value which will be defined to service.response_status_code
and the last is the value set to service.response_status
.
By default, all services comes with following error methods:
(Hint: See all available error methods here
)
bad_request_error(message_key) # set response_status_code to 400
not_authorized_error(message_key) # set response_status_code to 401,
forbidden_error(message_key) # set response_status_code to 403,
not_found_error(message_key) # set response_status_code to 404,
unprocessable_entity_error(message_key) # set response_status_code to 422,
internal_server_error(message_key) # set response_status_code to 500,
not_implemented_error(message_key) # set response_status_code to 501
Beside this methods, you can always use low level API to generate errors, just call the error
method, ex:
# API
error(status, message_key, options = {})
# eg:
error(409, :conflict_error, reason: 'unkown')
error!(409, :conflict_error, reason: 'unkown')
# suppose you YML locale file have the configuration:
# nifty_services:
# errors:
# conflict_error: 'Conflict! The reason is %{reason}'
But you can always add new convenience errors methods via API, this way you will have more expressivity and sintax sugar:
## API
NiftyServices.add_response_error_method(status, status_code)
## eg:
NiftyServices.add_response_error_method(:conflict, 409)
## now you have the methods:
## conflict_error(:conflict_error)
## conflit_error!(:conflict_error)
So, until now we saw how to use NiftyServices::BaseService
to create generic services to couple specific domain logic for actions, this is very usefull, but things get a lot better when you're working with CRUD actions for your api.
Follow an example of Create, Update and Delete CRUD services for Post
resource:
class PostCreateService < NiftyServices::BaseCreateService
# record_type must be a object respond to :build and :save methods
# is possible to access this record outside of service using
# `service.record` or `service.post`
# if you want to create a custom alias name, use:
# record_type Post, alias_name: :user_post
# This way, you can access the record using
# `service.user_post`
record_type Post
WHITELIST_ATTRIBUTES = [:title, :content]
def record_attributes_whitelist
WHITELIST_ATTRIBUTES
end
# use custom scope to create the record
# scope returned below must respond_to :build method
def build_record_scope
@user.posts
end
# this key is used for I18n translations
def record_error_key
:posts
end
def user_can_create_record?
# (here you can do any kind of validation, eg:)
# check if user is trying to recreate a recent resource
# this will return false if user has already created a post with
# this title in the last 30 seconds (usefull to ban bots)
@user.posts.exists(title: record_allowed_attributes[:title], created_at: "NOW() - interval(30 seconds)")
end
end
service = PostCreateService.new(User.first, title: 'Teste', content: 'Post example content')
service.execute
service.success? # true
service.response_status_code # 200
service.response_status # :created
You must have the following keys setup up in your locales files:
nifty_services:
users:
not_found: "Invalid or not found user"
ip_temporarily_blocked: "This IP is temporarily blocked from creating records"
# note: posts is the key return in `record_error_key` service method
posts:
user_cant_create: "User cant create this record"
If you try to create a post for a invalid user, such as:
# PostCreateService.new(user, options)
service = PostCreateService.new(nil, options)
service.execute
service.success? # false
service.response_status # :not_found
service.response_status_code # 404
service.errors # ["Invalid or not found user"]
Or if user is trying to create a duplicate resource:
# PostCreateService.new(user, options)
service = PostCreateService.new(User.first, options)
service.execute
service.success? # false
service.errors # ["User cant create this record"]
service.response_status # :forbidden_error
service.response_status_code # 400
Eg: if any validation in Post model won't pass:
# PostCreateService.new(user, options)
# Post model as the validation:
# validates_presence_of :title, :content
service = PostCreateService.new(User.first, title: nil, content: nil)
service.execute
service.success? # false
service.errors # => [{ title: 'is empty', content: 'is empty' }]
service.response_status # :unprocessable_entity
service.response_status_code # 422
class PostUpdateService < NiftyServices::BaseUpdateService
# service.post or service.record
record_type Post
WHITELIST_ATTRIBUTES = [:title, :content]
def record_allowed_attributes
WHITELIST_ATTRIBUTES
end
# by default, internally @record must respond to
# user_can_update(user)
# so you can do specific validations per resource
def user_can_update_record?
# only system admins and owner can update this record
@user.admin? || @user.id == @record.id
end
def record_error_key
:posts
end
end
# :user_id will be ignored since it's not in whitelisted attributes
# this can safe yourself from parameter inject attacks, by default
update_service = PostUpdateService.new(Post.first, User.first, title: 'Changing title', content: 'Updating content', user_id: 2)
update_service.execute
update_service.success? # true
update_service.response_status # :ok
update_service.response_status_code # 200
update_service.changed_attributes # [:title, :content]
update_service.changed? # true
Your locale file must have the following keys:
posts:
not_found: "Invalid or not found post"
user_cant_update: "User can't update this record"
users:
not_found: "Invalid or not found user"
Response when owner user is not valid:
# PostUpdateService.new(post, user, params)
update_service = PostUpdateService.new(Post.first, nil, title: 'Changing title', content: 'Updating content')
update_service.execute
update_service.success? # false
update_service.response_status # :not_found_error
update_service.response_status_code # 404
update_service.errors # ["Invalid or not found user"]
Responses when trying to update to update a resource who don't belongs to owner:
# PostUpdateService.new(post, user, params)
update_service = PostUpdateService.new(Post.first, User.last, title: 'Changing title', content: 'Updating content')
update_service.execute
update_service.success? # false
update_service.response_status # :forbidden
update_service.response_status_code # 400
update_service.changed_attributes # []
update_service.changed? # false
update_service.errors # ["User can't update this record"]
Response when post don't exists:
# PostUpdateService.new(post, user, params)
update_service = PostUpdateService.new(nil, User.last, title: 'Changing title', content: 'Updating content')
update_service.execute
update_service.success? # false
update_service.response_status # :not_found_error
update_service.response_status_code # 404
update_service.errors # ["Invalid or not found post"]
class PostDeleteService < NiftyServices::BaseDeleteService
# record_type object must respond to :destroy or :delete method
record_type Post
def record_error_key
:posts
end
# below the code used internally, you can override to
# create custom delete, but remembers that this method
# must return a boolean value
def destroy_record
@record.try(:destroy) || @record.try(:delete)
end
# by default, internally @record must respond to
# @record.user_can_delete?(user)
# so you can do specific validations per resource
def user_can_delete_record?
# only system admins and owner can delete this record
@user.admin? || @user.id == @record.id
end
end
Your locale file must have the following keys:
posts:
not_found: "Invalid or not found post"
user_cant_delete: "User can't delete this record"
users:
not_found: "Invalid or not found user"
Response when owner user is not valid:
# PostDeleteService.new(post, user, params)
delete_service = PostDeleteService.new(Post.first, nil)
delete_service.execute
delete_service.success? # false
delete_service.response_status # :not_found_error
delete_service.response_status_code # 404
delete_service.errors # ["Invalid or not found user"]
Responses when trying to delete a resource who don't belongs to owner:
# PostDeleteService.new(post, user, params)
delete_service = PostDeleteService.new(Post.first, User.last)
delete_service.execute
delete_service.success? # false
delete_service.response_status # :forbidden
delete_service.response_status_code # 400
delete_service.errors # ["User can't delete this record"]
Response when post don't exists:
# PostDeleteService.new(post, user, params)
delete_service = PostDeleteService.new(nil, User.last)
delete_service.execute
delete_service.success? # false
delete_service.response_status # :not_found_error
delete_service.response_status_code # 404
delete_service.errors # ["Invalid or not found post"]
As you see in the above examples, with NiftyServices
you can respond in multiples languages for the same service error messages, by default your locales config file must be configured as:
# attention: dont use `resource_type`
# use the key setup up in `record_error_key` methods
resource_type:
not_found: "Invalid or not found post"
user_cant_create: "User can't delete this record"
user_cant_read: "User can't access this record"
user_cant_update: "User can't delete this record"
user_cant_delete: "User can't delete this record"
users:
not_found: "Invalid or not found user"
You can configure the default I18n namespace using configuration:
NiftyServies.configure do |config|
config.i18n_namespace = :my_app
end
Example config for Post
and Comment
resources using my_app
locale namespace:
# default is nifty_services
my_app:
errors:
default_crud: &default_crud
user_cant_create: "User can't delete this record"
user_cant_read: "User can't access this record"
user_cant_update: "User can't delete this record"
user_cant_delete: "User can't delete this record"
users:
not_found: "Invalid or not found user"
posts:
<<: *default_crud
not_found: "Invalid or not found post"
comments:
<<: *default_crud
not_found: "Invalid or not found comment"
Here the most common callbacks list you can use to hook actions in run-time:
(Hint: See all existent callbacks definitions in extensions/callbacks.rb
file)
- before_initialize
- after_initialize
- before_execute
- after_execute
- before_error
- after_error
- before_success
- after_success
Well, probably you will need to add custom callbacks to your services, in my case I need to save in database an object which tracks information about the environment used to create ALL RECORDS in my application, I was able to do it with just a few lines of code, see for yourself:
# Some monkey patch :)
NiftyServices::BaseCreateService.class_eval do
ORIGIN_WHITELIST_ATTRIBUTES = [:provider, :locale, :user_agent, :ip]
def origin_params(params = {})
filter_hash(params.fetch(:origin, {}).to_h, ORIGIN_WHITELIST_ATTRIBUTES)
end
def create_origin(originable, params = {})
return unless originable.respond_to?(:create_origin)
return unless create_origin?
originable.create_origin(origin_params(params))
end
# for records which we don't need to create origins, just
# overwrite this method inside service class turning it off with:
# return false
def create_origin?
Application::Config.create_origin_for_records
end
end
# This register a callback for ALL services who inherit from `NiftyServices::BaseCreateService`
# In other words: Every and all records created in my application will be tracked
# I can believe that is easy like this, I need a beer right now!
NiftyServices::BaseCreateService.register_callback(:after_success, :create_origin_for_record) do
create_origin(@record, @options)
end
Now, every record created in my application will have an associated origin
object, really simple and cool!
There are only a few things you must want and have to configure for your services work properly, below you can see all needed configuration:
NiftyServices.config do |config|
# [optional - but very recommend! Please, do it]
# class used to control ACL
config.user_class = User
# [optional]
# global logger for all services
# [Default: Logger.new('/dev/null')]
config.logger = Logger.new('log/services_logger.log')
# [optional]
# Namespace to lookup when using concerns with services
# [Default: 'NitfyServices::Concerns']
config.service_concerns_namespace = "Services::V1::Concerns"
end
You need a very minimal setup to integrate with your existing or new Rails application. I prefer to put my services files inside the lib/services
folder, cause this allow better namespacing configuration over app/services
, but this is up to you to decide.
First thing to do is add lib/
folder in autoload
path, place the following in your config/application.rb
# config/application.rb
config.paths.add(File.join(Rails.root, 'lib'), glob: File.join('**', '*.rb'))
config.autoload_paths << Rails.root.join('lib')
Second, create lib/services
directory:
$ mkdir -p lib/services/v1/users
Next, configure:
NiftyServices.configure do |config|
config.user_class = User
end
Note: See Configurations section to see all available configs
Create your first service:
$ touch lib/services/v1/users/create_service.rb
Use in your controller:
class UsersController < BaseController
def create
service = Services::V1::Users::CreateService.new(params).execute
default_response = { status: service.response_status, status_code: service.response_status_code }
if service.success?
response = { user: service.user, subscription: service.subscription }
else
response = { error: true, errors: service.errors }
end
render json: default_response.merge(response), status: service.response_status
end
end
This can be even better if you move response code to a helper:
# helpers/users_helper.rb
module UsersHelper
def response_for_user_create_service(service)
success_response = { user: service.user, subscription: service.subscription }
generic_response_for_service(service, success_response)
end
# THIS IS GREAT, you can use this method to standardize ALL of your
# endpoints responses, THIS IS SO FUCKING COOL!
def generic_response_for_service(service, success_response)
default_response = {
status: service.response_status,
status_code: service.response_status_code,
success: service.success?
}
if service.success?
response = success_response
else
response = {
error: true,
errors: service.errors
}
end
default_response.merge(response)
end
end
Changing controller again: (looks so readable now <3)
# controllers/users_controller.rb
class UsersController < BaseController
def create
service = Services::V1::Users::CreateService.new(params).execute
render json: response_for_user_create_service(service), status: service.response_status
end
end
Well done sir! Did you read the comments in generic_response_for_service
? Read it and think a little about this and prepare yourself for having orgasms when you realize how fuck awesome this will be for your API's. Need mode? Checkout Sample Standartized API with NiftyServices Repository
Well, the integration here don't variate too much from Rails, just follow the steps:
1 - Decide where you'll put your services 2 - Code that dam amazing services! 3 - Instantiate the service in your framework entry point 4 - Create helpers to handle service response 5 - Be happy and go party!
Need examples of integrations fully working? Check out one of the following repositories:
NiftyServices - Sinatra Sample NiftyServices - Rails Sample NiftyServices - Grape Sample
Here, for your convenience and sanity all basic service structures for reference when you start a brand new Service. Most of time, the best way is to copy all content from each service described below and change according to your needs.
class SomeCreateService < NiftyServices::BaseCreateService
# [Required]
# remember that inside the Service you always can use
# @record variable to access current record
# and from outside (service instance):
# service.record or service.record_type
# eg:
# record_type BlogPost
# service.record # BlogPost.new(...)
# service.blog_post # BlogPost.new(...)
# service.record == service.blog_post # true
# alias_name can be used to create a custom alias name
# eg:
# record_type BlogPost, alias_name: :post
# service.record # BlogPost.new(...)
# service.post # BlogPost.new(...)
# service.record == service.post # true
record_type RecordType, alias_name: :my_custom_alias_name
private
# [Required]
# Always validate if @user can create the current record_type
# If this method is not implemented a NotImplementedError exception will be raised
def user_can_create_record?
return forbidden_error!('errors.some_error') if (some_validation)
return bad_request_error!('errors.some_other_error') if (another_validation)
# remember to return true after all validations
# if you don't return true Service will not be able to create the record
return true
end
# [Optional]
# method called when save_error method call raises an exception
# this ocurr for example with ActiveRecord objects
# default: unprocessable_entity_error!(error)
def on_save_record_error(error)
logger.error(error)
if error.is_a?(ActiveRecord::RecordNotUnique)
return unprocessable_entity_error!(%s(posts.duplicate_record))
end
end
# [Optional]
# determine wheter user will be validate as valid object before
# record creation
# (default: true)
def validate_user?
return true
end
# [Optional]
# custom scope for record, eg: @user.posts
# default is nil
def build_record_scope
end
end
class SomeUpdateService < NiftyServices::BaseUpdateService
# [Required]
record_type RecordType, alias_name: :custom_alias_name
WHITELIST_ATTRIBUTES = [
:safe_attribute_1,
:safe_attribute_2,
]
private
# [Required]
# When a new instance of Service is created, the @options variables receive some
# values, eg: { user: { email: "...", name: "...."} }
# use record_attributes_hash to tell the Service from where to pull theses values
# eg: @options.fetch(:user, {})
# If this method is not implemented a NotImplementedError exception will be raised
def record_attributes_hash
@options.fetch(options_key, {})
end
# [Required]
# whitelisted attributes (must be an Array) which can be updated by this Service
# If this method is not implemented a NotImplementedError exception will be raised
def record_attributes_whitelist
WHITELIST_ATTRIBUTES
end
# [required]
# This is a VERY IMPORTANT point of attention
# always verify if @user has permissions to update the current @record object
# Hint: if @record respond_to `user_can_update?(user)` you can remove this
# method and do the validation inside `user_can_update(user)` method in @record
# If this method is not implemented a NotImplementedError exception will be raised
def user_can_update_record?
@record.user_id == @user.id
end
# [Optional]
# This is the default implementation of update record, you may overwrite it
# to to custom updates (MOST OF TIME YOU DONT NEED TO DO THIS)
# only change this if you know what you are really doing
def update_record
@record.class.send(:update, @record.id, record_allowed_attributes)
end
# [optional]
# Any callback is optional, this is just a example
def after_success
if changed?
logger.info 'Successfully update record ID %s' % @record.id
logger.info 'Changed attributes are %s' % changed_attributes
end
end
end
class SomeDeleteService < NiftyServices::BaseDeleteService
# [Required]
record_type RecordType, alias_name: :custom_alias_name
private
# [Required]
# This is a VERY IMPORTANT point of attention
# always verify if @user has permissions to delete the current @record object
# Hint: if @record respond_to `user_can_delete?(user)` you can remove this
# method and do the validation inside `user_can_delete(user)` method in @record
# If this method is not implemented a NotImplementedError exception will be raised
def user_can_delete_record?
@record.user_id == @user.id
end
# [optional]
# Any callback is optional, this is just a example
def after_success
logger.info('Successfully Deleted resource ID %s' % @record.id)
end
# [Optional]
# This is the default implementation of delete record, you may overwrite it
# to do custom delete (MOST OF TIME YOU DONT NEED TO DO THIS)
# only change this if you know what you are really doing
def destroy_record
@record.try(:destroy) || @record.try(:delete)
end
end
class SomeCustomActionService < NiftyServices::BaseActionService
# [required]
# this is the action identifier used internally
# and to generate error messages
# see: invalid_action_error_key method
action_name :custom_action_name
private
# [Required]
# Always validate if Service can execute the action
# This method MUST return a boolean value indicating if Service can or not
# run the method `execute_service_action`
# If this method is not implemented a NotImplementedError exception will be raised
def user_can_execute_action?
# do some specific validation here, you can return errors such:
# return not_found_error!(%(users.invalid_user)) # returns false and avoid execution
return true
end
# [Required]
# The core function of BaseActionServices
# This method is called when all validations passes, so here you can put
# all logic for Service (eg: send mails, clear logs, any kind of action you want)
# If this method is not implemented a NotImplementedError exception will be raised
def execute_service_action
# (do some complex stuff)
end
# You dont need to overwrite this method, just `record_error_key`
# But it's important you know how final message key will be created
# using the pattern below
def invalid_action_error_key
"#{record_error_key}.cant_execute_#{action_name}"
end
# [Required]
# Key used to created the error messages for this Service
# If this method is not implemented a NotImplementedError exception will be raised
def record_error_key
:users
end
end
You can use any of the methods above with your services instances
:
service.success? # boolean
service.fail? # boolean
service.errors # hash
service.add_error(error) # array
service.response_status # symbol (eg: :ok)
service.response_status_code # integer (eg: 200)
service.changed_attributes # array
service.changed? # boolean
service.callback_fired?(callback_name) # boolean
service.register_callback(name, method, &block) # nil
service.register_callback_action(&block) # nil
service.option_exists?(option_name) # boolean
service.option_enabled?(option_name) # boolean
service.option_disabled?(option_name) # boolean
Currently NiftyServices don't have CLI(command line interface) generators, but is in the roadmap, so keep your eyes here!
- β½ Create CLI Generators
- β½ Beter documentation for
BaseActionService
- β½ Write Sample Applications
- β½ Write better tests for all
Crud Services
- β½ Write better tests for
BaseActionServices
- β½ Write tests for Configuration
- β½ Write tests for Callbacks
After checking out the repo, run bin/setup
to install dependencies. Then, run rake rspec
to run the tests. You can also run bin/console
for an interactive prompt that will allow you to experiment.
To install this gem(: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.
Bug reports and pull requests are welcome on GitHub at http://github.com/fidelisrafael/nifty_services. 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.
The gem is available as open source under the terms of the MIT License.