# 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 end def on_query_created(query) # NOOP end def on_command_created(command) # NOOP end def on_response_created(command) # NOOP end # After validation callbacks def on_failure(flow) # NOOP end def 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, string validates :title, :weight, :price, presence: true validates :weight, allow_blank: true, format: { with: /\A[0-9]+\s[g|kg]\z/ } end query do validate :product, :variant memoize def new_product Product.new(title: title) end memoize def new_variant new_product.build_variant(weight: weight, price: price) end private def product errors.add(:base, :unprocessable_entity, new_product.errors.full_messages.join("\n")) if new_product.invalid? end def variant errors.add(:base, :unprocessable_entity, new_variant.errors.full_messages.join("\n")) if new_variant.invalid? end end command 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 end def 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 def 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, string validates :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, :variant memoize def new_product Product.new(title: title) end memoize def new_variant new_product.build_variant(weight: weight, price: price) end private def product errors.add(:unprocessable_entity, new_product.errors.full_messages.join("\n")) if new_product.invalid? end def 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 end def 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 end def 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).