Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/andriy-baran/steel_wheel
Steel Wheel - exactly what you need when you are on Rails
https://github.com/andriy-baran/steel_wheel
rails ruby
Last synced: 2 months ago
JSON representation
Steel Wheel - exactly what you need when you are on Rails
- Host: GitHub
- URL: https://github.com/andriy-baran/steel_wheel
- Owner: andriy-baran
- License: mit
- Created: 2019-03-25T10:24:19.000Z (almost 6 years ago)
- Default Branch: master
- Last Pushed: 2024-02-06T06:34:24.000Z (12 months ago)
- Last Synced: 2024-09-15T22:28:59.775Z (4 months ago)
- Topics: rails, ruby
- Language: Ruby
- Homepage:
- Size: 673 KB
- Stars: 9
- Watchers: 2
- Forks: 0
- Open Issues: 2
-
Metadata Files:
- Readme: README.md
- License: LICENSE.txt
- Code of conduct: CODE_OF_CONDUCT.md
Awesome Lists containing this project
README
# SteelWheel
[![Maintainability](https://api.codeclimate.com/v1/badges/a197758aa1cfde54f0e1/maintainability)](https://codeclimate.com/github/andriy-baran/steel_wheel/maintainability)
[![Test Coverage](https://api.codeclimate.com/v1/badges/a197758aa1cfde54f0e1/test_coverage)](https://codeclimate.com/github/andriy-baran/steel_wheel/test_coverage)
[![Gem Version](https://badge.fury.io/rb/steel_wheel.svg)](https://badge.fury.io/rb/steel_wheel)The library is a tool for building highly structured service objects.
## Concepts
### Stages
We may consider any controller action as a sequence of following stages:
1. **Input validations and preparations**
* Describe the structure of parameters
* Validate values, provide defaults
2. **Querying data and preparing context**
* Records lookups by IDs in parameters
* Validate permissions to perform an action
* Validate conditions (business logic requirements)
* Inject Dependencies
* Set up current user
3. **Performing Action (skipped on GET requests)**
* Updade database state
* Enqueue jobs
* Handle exceptions
* Validate intermediate states
4. **Exposing Results/Errors**
* Presenters
* Contextual information useful for the users### Implementation of stages
As you can see each step has specific tasks and can be implemented as a separate object.**SteelWheel::Params (gem https://github.com/andriy-baran/easy_params)**
* provides DSL for `params` structure definition
* provides type coercion and default values for individual attributes
* has ActionModel::Validation included
* implements `http_status` method that returs HTTP error code**SteelWheel::Query**
* has `Memery` module included
* has ActionModel::Validation included
* implements `http_status` method that returs HTTP error code**SteelWheel::Command**
* has ActionModel::Validation included
* implements `http_status` method that returs HTTP error code
* implements `call` method that should do the stuff**SteelWheel::Response**
* has ActionModel::Validation included
* implements `status` method that returs HTTP error code
* implements `success?` method that checks if there are any errors### Process
Let's image the process that connects stages described above
* Get an input and initialize object for params, trigger callbacks
* Initialize object for preparing context and give it an access to previous object, trigger callbacks
* Initialize object for performing action and give it an access to previous object, trigger callbacks
* Initialize resulting object and give it an access to previous object,
* Run validations, collect errros, trigger callbacks
* If everything is ok run action and handle errors that appear during execution time.
* If we have an error on any stage we stop validating following objects.### Callbacks
We have two types of callbacks explicit and implicit
### Implicit callbacks
We define them via handler instance methods
```ruby
def on_params_created(params)
# NOOP
enddef on_query_created(query)
# NOOP
enddef on_command_created(command)
# NOOP
enddef on_response_created(command)
# NOOP
end# After validation callbacks
def on_failure(flow)
# NOOP
enddef on_success(flow)
# NOOP
end
```### Explicit callbacks
We define them during instantiation of hanler by providing a block parameter
```ruby
handler = handler_class.new do |c|
c.params { |o| puts o }
c.query { |o| puts o }
c.command { |o| puts o }
c.response { |o| puts o }
end
result = handler.handle(input: { id: 1 })
```
In addition we can manipulate with objects directly via callback of `handle` mathod
```ruby
result = handler_class.handle(input: { id: 1 }) do |c|
c.params.id = 12
c.query.user = current_user
c.command.request_headers = request.headers
c.response.prepare_presenter
end
```## Installation
Add this line to your application's Gemfile:
```ruby
gem 'steel_wheel'
```And then execute:
$ bundle
Or install it yourself as:
$ gem install steel_wheel
## Usage
Add base handler
```bash
bin/rails g steel_wheel:application_handler
```Add specific handler
```bash
bin/rails g steel_wheel:handler products/create
```
This will generate `app/handlers/products/create_handler.rb`. And we can customize it```ruby
class Products::CreateHandler < ApplicationHandler
define do
params do
attribute :title, string
attribute :weight, string
attribute :price, stringvalidates :title, :weight, :price, presence: true
validates :weight, allow_blank: true, format: { with: /\A[0-9]+\s[g|kg]\z/ }
endquery do
validate :product, :variantmemoize def new_product
Product.new(title: title)
endmemoize def new_variant
new_product.build_variant(weight: weight, price: price)
endprivate
def product
errors.add(:base, :unprocessable_entity, new_product.errors.full_messages.join("\n")) if new_product.invalid?
enddef variant
errors.add(:base, :unprocessable_entity, new_variant.errors.full_messages.join("\n")) if new_variant.invalid?
end
endcommand do
def add_to_stock!
PointOfSale.find_each do |pos|
PosProductStock.create!(pos_id: pos.id, product_id: new_product.id, on_hand: 0.0)
end
enddef call(response)
::ApplicationRecord.transaction do
new_product.save!
new_variant.save!
add_to_stock!
rescue => e
response.errors.add(:unprocessable_entity, e.message)
raise ActiveRecord::Rollback
end
end
end
enddef on_success(flow)
flow.call
end
end
```
Looks too long. Lets move code into separate files.
```bash
bin/rails g steel_wheel:params products/create
```
Add relative code
```ruby
# Base class also can be refered via
# ApplicationHandler.main_builder.abstract_factory.params_factory.base_class
class Products::CreateHandler
class Params < SteelWheel::Params
attribute :title, string
attribute :weight, string
attribute :price, stringvalidates :title, :weight, :price, presence: true
validates :weight, allow_blank: true, format: { with: /\A[0-9]+\s[g|kg]\z/ }
end
end
```
Than do the same for query
```bash
bin/rails g steel_wheel:query products/create
```
Add code...
```ruby
# Base class also can be refered via
# ApplicationHandler.main_builder.abstract_factory.query_factory.base_class
class Products::CreateHandler
class Query < SteelWheel::Query
validate :product, :variantmemoize def new_product
Product.new(title: title)
endmemoize def new_variant
new_product.build_variant(weight: weight, price: price)
endprivate
def product
errors.add(:unprocessable_entity, new_product.errors.full_messages.join("\n")) if new_product.invalid?
enddef variant
errors.add(:unprocessable_entity, new_variant.errors.full_messages.join("\n")) if new_variant.invalid?
end
end
end
```
And finally command
```bash
bin/rails g steel_wheel:command products/create
```
Move code
```ruby
class Products::CreateHandler
class Command < SteelWheel::Command
def add_to_stock!
::PointOfSale.find_each do |pos|
::PosProductStock.create!(pos_id: pos.id, product_id: new_product.id, on_hand: 0.0)
end
enddef call(response)
::ApplicationRecord.transaction do
new_product.save!
new_variant.save!
add_to_stock!
rescue => e
response.errors.add(:unprocessable_entity, e.message)
raise ActiveRecord::Rollback
end
end
end
end
```
Than we can update handler
```ruby
# app/handlers/manage/products/create_handler.rb
class Manage::Products::CreateHandler < ApplicationHandler
define do
params Params
query Query
command Command
enddef on_success(flow)
flow.call(flow)
end
end
```### HTTP status codes and errors handling
It's important to provide a correct HTTP status when we faced some problem(s) during request handling. The library encourages developers to add the status codes when they add errors.
```ruby
errors.add(:unprocessable_entity, 'error')
```
As you know `full_messages` will produce `['Unprocessable Entity error']` to prevent this and get only error `SteelWheel::Response` has special method that makes some error keys to behave like `:base`
```ruby
# Default setup
generic_validation_keys(:not_found, :forbidden, :unprocessable_entity, :bad_request, :unauthorized)
# To override it in your app
class SomeHandler
define do
response do
generic_validation_keys(:not_found, :forbidden, :unprocessable_entity, :bad_request, :unauthorized, :payment_required)
end
end
end
```
In Rails 6.1 `ActiveModel::Error` was introdused and previous setup is not needed, second argument is used instead
```ruby
errors.add(:base, :unprocessable_entity, 'error')
```## 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](https://rubygems.org).
## Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/andriy-baran/steel_wheel. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
## License
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
## Code of Conduct
Everyone interacting in the SteelWheel project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/andriy-baran/steel_wheel/blob/master/CODE_OF_CONDUCT.md).