μ-service (Micro::Service)
Create simple and powerful service objects.
The main goals of this project are:
- The smallest possible learning curve.
- Referential transparency and data integrity.
- No callbacks, compose a pipeline of service objects to represents complex business logic. (input >> process/transform >> output)
- μ-service (Micro::Service)
- Required Ruby version
- Installation
- Usage
- How to create a Service Object?
- How to use the result hooks?
- How to create a pipeline of Service Objects?
- What is a strict Service Object?
- How to validate Service Object attributes?
- It's possible to compose pipelines with other pipelines?
- Comparisons
- Benchmarks
- Development
- Contributing
- License
- Code of Conduct
Required Ruby version
>= 2.2.0
Installation
Add this line to your application's Gemfile:
gem 'u-service'
And then execute:
$ bundle
Or install it yourself as:
$ gem install u-service
Usage
How to create a Service Object?
class Multiply < Micro::Service::Base
attributes :a, :b
def call!
if a.is_a?(Numeric) && b.is_a?(Numeric)
Success(a * b)
else
Failure(:invalid_data)
end
end
end
#====================#
# Calling a service #
#====================#
result = Multiply.call(a: 2, b: 2)
p result.success? # true
p result.value # 4
# Note:
# The result of a Micro::Service#call
# is an instance of Micro::Service::Result
#----------------------------#
# Calling a service instance #
#----------------------------#
result = Multiply.new(a: 2, b: 3).call
p result.success? # true
p result.value # 6
#===========================#
# Verify the result failure #
#===========================#
result = Multiply.call(a: '2', b: 2)
p result.success? # false
p result.failure? # true
p result.value # :invalid_data
How to use the result hooks?
class Double < Micro::Service::Base
attributes :number
def call!
return Failure(:invalid) { 'the number must be a numeric value' } unless number.is_a?(Numeric)
return Failure(:lte_zero) { 'the number must be greater than 0' } if number <= 0
Success(number * number)
end
end
#================================#
# Printing the output if success #
#================================#
Double
.call(number: 3)
.on_success { |number| p number }
.on_failure(:invalid) { |msg| raise TypeError, msg }
.on_failure(:lte_zero) { |msg| raise ArgumentError, msg }
# The output when is a success:
# 9
#=============================#
# Raising an error if failure #
#=============================#
Double
.call(number: -1)
.on_success { |number| p number }
.on_failure(:invalid) { |msg| raise TypeError, msg }
.on_failure(:lte_zero) { |msg| raise ArgumentError, msg }
# The output (raised an error) when is a failure:
# ArgumentError (the number must be greater than 0)
How to create a pipeline of Service Objects?
module Steps
class ConvertToNumbers < Micro::Service::Base
attribute :numbers
def call!
if numbers.all? { |value| String(value) =~ /\d+/ }
Success(numbers: numbers.map(&:to_i))
else
Failure('numbers must contain only numeric types')
end
end
end
class Add2 < Micro::Service::Strict
attribute :numbers
def call!
Success(numbers: numbers.map { |number| number + 2 })
end
end
class Double < Micro::Service::Strict
attribute :numbers
def call!
Success(numbers: numbers.map { |number| number * 2 })
end
end
class Square < Micro::Service::Strict
attribute :numbers
def call!
Success(numbers: numbers.map { |number| number * number })
end
end
end
#=================================================#
# Creating a pipeline using the collection syntax #
#=================================================#
Add2ToAllNumbers = Micro::Service::Pipeline[
Steps::ConvertToNumbers,
Steps::Add2
]
result = Add2ToAllNumbers.call(numbers: %w[1 1 2 2 3 4])
p result.success? # true
p result.value # {:numbers => [3, 3, 4, 4, 5, 6]}
#=======================================================#
# An alternative way to create a pipeline using classes #
#=======================================================#
class DoubleAllNumbers
include Micro::Service::Pipeline
pipeline Steps::ConvertToNumbers, Steps::Double
end
DoubleAllNumbers
.call(numbers: %w[1 1 b 2 3 4])
.on_failure { || p } # "numbers must contain only numeric types"
#=================================================================#
# Another way to create a pipeline using the composition operator #
#=================================================================#
SquareAllNumbers =
Steps::ConvertToNumbers >> Steps::Square
SquareAllNumbers
.call(numbers: %w[1 1 2 2 3 4])
.on_success { |value| p value[:numbers] } # [1, 1, 4, 4, 9, 16]
What is a strict Service Object?
A: Is a service object which will require all keywords (attributes) on its initialization.
class Double < Micro::Service::Strict
attribute :numbers
def call!
Success(numbers.map { |number| number * 2 })
end
end
Double.call({})
# The output (raised an error):
# ArgumentError (missing keyword: :numbers)
How to validate Service Object attributes?
Note: To do this your application must have the activemodel >= 3.2 as a dependency.
#
# By default, if your project has the activemodel
# any kind of service attribute can be validated.
#
class Multiply < Micro::Service::Base
attribute :a
attribute :b
validates :a, :b, presence: true, numericality: true
def call!
return Failure(errors: self.errors) unless valid?
Success(number: a * b)
end
end
#
# But if do you want an automatic way to fail
# your services if there is some invalid data.
# You can use:
require 'micro/service/with_validation'
# Using this approach, you can rewrite the previous sample with fewer lines of code.
class Multiply < Micro::Service::WithValidation
attribute :a
attribute :b
validates :a, :b, presence: true, numericality: true
def call!
Success(number: a * b)
end
end
# Note:
# There is a strict variation for Micro::Service::WithValidation
# Use Micro::Service::Strict::Validation if do you want this behavior.
It's possible to compose pipelines with other pipelines?
Answer: Yes
module Steps
class ConvertToNumbers < Micro::Service::Base
attribute :numbers
def call!
if numbers.all? { |value| String(value) =~ /\d+/ }
Success(numbers: numbers.map(&:to_i))
else
Failure('numbers must contain only numeric types')
end
end
end
class Add2 < Micro::Service::Strict
attribute :numbers
def call!
Success(numbers: numbers.map { |number| number + 2 })
end
end
class Double < Micro::Service::Strict
attribute :numbers
def call!
Success(numbers: numbers.map { |number| number * 2 })
end
end
class Square < Micro::Service::Strict
attribute :numbers
def call!
Success(numbers: numbers.map { |number| number * number })
end
end
end
Add2ToAllNumbers = Steps::ConvertToNumbers >> Steps::Add2
DoubleAllNumbers = Steps::ConvertToNumbers >> Steps::Double
SquareAllNumbers = Steps::ConvertToNumbers >> Steps::Square
DoubleAllNumbersAndAdd2 = DoubleAllNumbers >> Steps::Add2
SquareAllNumbersAndAdd2 = SquareAllNumbers >> Steps::Add2
DoubleAllNumbersAndSquareThem = DoubleAllNumbers >> SquareAllNumbersAndAdd2
SquareAllNumbersAndDoubleThem = SquareAllNumbersAndAdd2 >> DoubleAllNumbers
DoubleAllNumbersAndSquareThem
.call(numbers: %w[1 1 2 2 3 4])
.on_success { |value| p value[:numbers] } # [6, 6, 18, 18, 38, 66]
SquareAllNumbersAndDoubleThem
.call(numbers: %w[1 1 2 2 3 4])
.on_success { |value| p value[:numbers] } # [6, 6, 12, 12, 22, 36]
Note: You can blend any of the syntaxes/approaches to create the pipelines) - examples.
Comparisons
Check it out implementations of the same use case with different libs (abstractions).
Benchmarks
https://github.com/serradura/u-service/tree/master/benchmarks/interactor
Development
After checking out the repo, run bin/setup
to install dependencies. Then, run ./test.sh
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/serradura/u-service. 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 Micro::Service project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.