https://github.com/Fretadao/f_service
Simpler, safer and more composable operations
https://github.com/Fretadao/f_service
chaining-services hacktoberfest hacktoberfest2023 interactor library monads operations ruby service-objects
Last synced: 7 months ago
JSON representation
Simpler, safer and more composable operations
- Host: GitHub
- URL: https://github.com/Fretadao/f_service
- Owner: Fretadao
- License: mit
- Created: 2020-04-14T13:47:09.000Z (almost 6 years ago)
- Default Branch: master
- Last Pushed: 2024-10-28T20:53:37.000Z (over 1 year ago)
- Last Synced: 2025-05-11T02:53:26.562Z (9 months ago)
- Topics: chaining-services, hacktoberfest, hacktoberfest2023, interactor, library, monads, operations, ruby, service-objects
- Language: Ruby
- Homepage:
- Size: 146 KB
- Stars: 10
- Watchers: 6
- Forks: 3
- Open Issues: 4
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: LICENSE
Awesome Lists containing this project
README

FService
Simpler, safer and more composable operations
FService is a small gem that provides a base class for your services (aka operations).
The goal is to make services simpler, safer, and more composable.
It uses the Result monad for handling operations.
## Installation
Add this line to your application's Gemfile:
```ruby
gem 'f_service'
```
And then execute:
$ bundle
Or install it yourself as:
$ gem install f_service
## Usage
### Creating your service
To start using it, you have to create your service class inheriting from FService::Base.
```ruby
class User::Create < FService::Base
end
```
Now, define your initializer to setup data.
```ruby
class User::Create < FService::Base
def initialize(name:)
@name = name
end
end
```
The next step is writing the `#run` method, which is where the work should be done.
Use the methods `#Success` and `#Failure` to handle your return values.
You can optionally specify a list of types which represents that result and a value for your result.
```ruby
class User::Create < FService::Base
# ...
def run
return Failure(:no_name, :invalid_attribute) if @name.nil?
user = UserRepository.create(name: @name)
if user.save
Success(:success, :created, data: user)
else
Failure(:creation_failed, data: user.errors)
end
end
end
```
> Remember, you **have** to return an `FService::Result` at the end of your services.
### Using your service
To run your service, use the method `#call` provided by `FService::Base`. We like to use the [implicit call](https://stackoverflow.com/a/19108981/8650655), but you can use it in the form you like most.
```ruby
User::Create.(name: name)
# or
User::Create.call(name: name)
```
> We do **not** recommend manually initializing and running your service because it **will not**
> type check your result (and you could lose nice features like [pattern
> matching](#pattern-matching) and [service chaining](#chaining-services))!
### Using the result
Use the methods `#successful?` and `#failed?` to check the status of your result. If it is successful, you can access the value with `#value`, and if your service fails, you can access the error with `#error`.
A hypothetical controller action using the example service could look like this:
```ruby
class UsersController < BaseController
def create
result = User::Create.(user_params)
if result.successful?
json_success(result.value)
else
json_error(result.error)
end
end
end
```
> Note that you're not limited to using services inside controllers. They're just PORO's (Play Old Ruby Objects), so you can use in controllers, models, etc. (even other services!).
### Pattern matching
The code above could be rewritten using the `#on_success` and `#on_failure` hooks. They work similar to pattern matching:
```ruby
class UsersController < BaseController
def create
User::Create.(user_params)
.on_success { |value| return json_success(value) }
.on_failure { |error| return json_error(error) }
end
end
```
Or else it is possible to specify an unhandled option to ensure that the callback will process that message anyway the
error.
```ruby
class UsersController < BaseController
def create
User::Create.(user_params)
.on_success(unhandled: true) { |value| return json_success(value) }
.on_failure(unhandled: true) { |error| return json_error(error) }
end
end
```
```ruby
class UsersController < BaseController
def create
User::Create.(user_params)
.on_success { |value| return json_success(value) }
.on_failure { |error| return json_error(error) }
end
end
```
> You can ignore any of the callbacks, if you want to.
Going further, you can match the Result type, in case you want to handle them differently:
```ruby
class UsersController < BaseController
def create
User::Create.(user_params)
.on_success(:user_created) { |value| return json_success(value) }
.on_success(:user_already_exists) { |value| return json_success(value) }
.on_failure(:invalid_data) { |error| return json_error(error) }
.on_failure(:critical_error) do |error|
MyLogger.report_failure(error)
return json_error(error)
end
end
end
```
It's possible to provide multiple types to the hooks too. If the result type matches any of the given types,
the hook will run.
```ruby
class UsersController < BaseController
def create
User::Create.(user_params)
.on_success(:user_created, :user_already_exists) { |value| return json_success(value) }
.on_failure(:invalid_data) { |error| return json_error(error) }
.on_failure(:critical_error) do |error|
MyLogger.report_failure(error)
return json_error(error)
end
end
end
```
### Type precedence
FService matches the service's types from left to right, from more specific to more generic.
For example, the following result `Failure(:unprocessable_entity, :client_error, :http_response)` will match in the following order:
1. `:unprocessable_entity`;
2. `:client_error`;
3. `:http_response`;
4. unmatched block;
### Chaining services
Since all services return Results, you can chain service calls making a data pipeline.
If some step fails, it will short circuit the call chain.
```ruby
class UsersController < BaseController
def create
result = User::Create.(user_params)
.and_then { |user| User::Login.(user) }
.and_then { |user| User::SendWelcomeEmail.(user) }
if result.successful?
json_success(result.value)
else
json_error(result.error)
end
end
end
```
You can use the `.to_proc` method on FService::Base to avoid explicit inputs when chaining services:
```ruby
class UsersController < BaseController
def create
result = User::Create.(user_params)
.and_then(&User::Login)
.and_then(&User::SendWelcomeEmail)
# ...
end
end
```
### `Check` and `Try`
You can use `Check` to converts a boolean to a Result, truthy values map to `Success`, and falsey values map to `Failures`:
```ruby
Check(:math_works) { 1 < 2 }
# => #
Check(:math_works) { 1 > 2 }
# => #
```
`Try` transforms an exception into a `Failure` if some exception is raised for the given block. You can specify which exception class to watch for
using the parameter `catch`.
```ruby
class IHateEvenNumbers < FService::Base
def run
Try(:rand_int) do
n = rand(1..10)
raise "Yuck! It's a #{n}" if n.even?
n
end
end
end
IHateEvenNumbers.call
# => #
IHateEvenNumbers.call
# => #, @types=[:rand_int]>
```
## Testing
We provide some helpers and matchers to make ease to test code envolving Fservice services.
To make available in the system, in the file 'spec/spec_helper.rb' or 'spec/rails_helper.rb'
add the folowing require:
```rb
require 'f_service/rspec'
```
### Mocking a result
```rb
mock_service(Uer::Create)
# => Mocks a successful result with all values nil
mock_service(Uer::Create, result: :success)
# => Mocks a successful result with all values nil
mock_service(Uer::Create, result: :success, types: [:created, :success])
# => Mocks a successful result with type created
mock_service(Uer::Create, result: :success, types: :created, value: instance_spy(User))
# => Mocks a successful result with type created and a value
mock_service(Uer::Create, result: :failure)
# => Mocs a failure with all nil values
mock_service(User::Create, result: :failure, types: [:unprocessable_entity, :client_error])
# => Mocs a failure with a failure type
mock_service(User::Create, result: :failure, types: [:unprocessable_entity, :client_error], value: { name: ["can't be blank"] })
# => Mocs a failure with a failure type and an error value
```
### Matching a result
```rb
expect(User::Create.(name: 'Joe')).to have_succeed_with(:created)
expect(User::Create.(name: 'Joe')).to have_succeed_with(:created).and_value(an_instance_of(User))
expect(User::Create.(name: nil)).to have_failed_with(:invalid_attributes)
expect(User::Create.(name: nil)).to have_failed_with(:invalid_attributes).and_error({ name: ["can't be blank"] })
expect(User::Create.(name: nil)).to have_failed_with(:invalid_attributes).and_error(a_hash_including(name: ["can't be blank"]))
```
## API Docs
You can access the API docs [here](https://www.rubydoc.info/gems/f_service/).
## 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 allows 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](https://rubygems.org).
## Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/Fretadao/f_service.
## License
The gem is available as open-source under the terms of the [MIT License](https://opensource.org/licenses/MIT).