Toolx
This gem adds a set of tools to enhance your Ruby on Rails application, providing utilities for envs, uniq ids, and more.
Installation
Install the gem and add to the application's Gemfile by executing:
$ bundle add toolx
Use rake tasks if needed:
bin/rails toolx:aliases:generate
bin/rails toolx:annotate:generate
bin/rails toolx:stateman:generate[model]
Add your toolx mixins to the ApplicationRecord
or any other model:
class ApplicationRecord < ActiveRecord::Base
primary_abstract_class
include Toolx::Core::Concerns::Inquirer
include Toolx::Core::Concerns::Transformer
include Toolx::Core::Concerns::CustomIdentifier
include Toolx::Core::Concerns::DateTimeToBoolean
include Toolx::Core::Concerns::Keygen
include Toolx::Core::Concerns::Tokenizer
include Toolx::Core::Concerns::WithStateMachine
end
Features
Env
Access environment variables in a clean, expressive way.
ENV['ROLE'] = 'admin'
Env = Toolx::Core::Env
Env[:role]
Env.role
Env.role_.admin?
Env.role_.user?
Env.debug?
Env.timeout!(10)
Env.role_(:none).none?
CustomIdentifier
Generate a custom ID for the id
column
CustomIdentifier = Toolx::Core::Concerns::CustomIdentifier
class User < ApplicationRecord
include CustomIdentifier
cid 'usr'
end
user.id
CustomIdentifier = Toolx::Core::Concerns::CustomIdentifier
class Document < ApplicationRecord
include CustomIdentifier
belongs_to :workspace
cid 'doc', related: { workspace: 4 }
end
Inquirer
Adds ? predicate-style behavior for strings.
class User < ApplicationRecord
include Toolx::Core::Concerns::Inquirer
inquirer :status, :kind
end
user = User.new(status: "active", kind: "admin")
user.status.active?
user.status.inactive?
user.kind.admin?
user.kind.user?
Transformer
Transform input strings on assignment (e.g., strip, downcase, etc.)
class User < ApplicationRecord
include Toolx::Core::Concerns::Transformer
transform :email, :strip, :downcase
transform :name, :strip, :presence
end
user = User.new
user.email = " Foo@Example.COM "
user.name = " "
user.email
user.name
DateTimeToBoolean
Use datetime fields as boolean-style flags:
class User < ApplicationRecord
include Toolx::Core::Concerns::DateTimeToBoolean
date_time_to_boolean :deleted_at
end
user.deleted?
user.deleted!
user.deleted = true
user.deleted = false
Keygen
Generate unique keys for your models, ensuring they are unique across the database.
class User < ApplicationRecord
include Toolx::Core::Concerns::Keygen
keygen :dob, attr: :lic_key, secret: 'my$ecret', limit: 20, separator: '.', br: 3
end
user = User.new(dob: '1990-01-01')
user.save!
user.lic_key
Tokenizer
Generate unique tokens for your models before creation, useful for API keys, access tokens, invitations, etc.
class AccessToken < ApplicationRecord
include Toolx::Core::Concerns::Tokenizer
tokenize :token, size: 32, prefix: 'tok'
end
token = AccessToken.create!
token.token
Stateman via with_state_machine
It is working only with Statesman::Adapters::ActiveRecord
You can generate state machine classes via rake task
bin/rails "toolx:stateman:generate[Project]"
command will generate models for existing Project
model as
Project::StateMachine
and Project::Transition
.
also will add migraion file for those models.
Example usage:
class Project::StateMachine
include Statesman::Machine
state :active, initial: true
state :pending
state :skipped
state :cancelled
state :done
transition from: :active, to: [:pending, :pending, :skipped, :cancelled, :done]
transition from: :pending, to: [:skipped, :cancelled, :done]
transition from: :skipped, to: [:pending]
after_transition do |model, transition|
model.update!(status: transition.to_state)
end
end
class Project::Transition < ActiveRecord::Base
belongs_to :project
attribute :most_recent, :boolean, default: false
attribute :sort_key, :integer
attribute :to_state, :string
attribute :metadata, :json, default: {}
validates :to_state, inclusion: { in: Project::StateMachine.states }
end
class Project < ActiveRecord::Base
include Toolx::Core::Concerns::WithStateMachine
STATUSES = %w[active pending done skipped cancelled].freeze
with_state_machine
attribute :name, :string
attribute :status, :string, default: 'active'
validates :name, presence: true
validates :status, inclusion: { in: STATUSES }, allow_nil: true
end
Errors
A structured, extensible error-handling framework:
Wrap errors with nested backtrace info
class MyError < StandardError
include Toolx::Core::Errors::NestedError
end
begin
raise "Something failed"
rescue => e
raise MyError.new("Outer error", e)
end
Base error classes
Toolx::Core::Errors::NestedStandardError
Toolx::Core::Errors::AppError
Toolx::Core::Errors::ApiError
Toolx::Core::Errors::BadRequest
Toolx::Core::Errors::Unauthorized
Toolx::Core::Errors::NotFound
Toolx::Core::Errors::Conflict
Toolx::Core::Errors::UnprocessableEntity
Toolx::Core::Errors::FailedDependency
Toolx::Core::Errors::Forbidden
Toolx::Core::Errors::InternalServerError
Toolx::Core::Errors::BadGateway
Toolx::Core::Errors::ServiceUnavailable
Toolx::Core::Errors::GatewayTimeout
Form
A simple form builder for Rails applications:
class Space::InfoForm < Toolx::Core::Form
attribute :name, :string
attribute :description, :string
attribute :kind, :string
validates :name, presence: true
validates :kind, inclusion: { in: Space::KINDS }, allow_blank: true
end
Presenter
Elegant presenter base class wrapping any object, with I18n support and flexible invocation patterns.
class UserPresenter < Toolx::Core::Presenter
def display_name
name.upcase
end
end
UserPresenter.present(user).display_name
to_proc usage
users.map(&UserPresenter)
as_proc(use: :method) usage
users.map(&UserPresenter.as_proc(use: :display_name))
auto_present detection
class Article
class Presenter < ::Toolx::Core::Presenter
def short_title = title.truncate(10)
end
end
Presenter.auto_present(Article.new(title: "A very long title"))
SimpleCrypt
Simple AES encryption helper using ActiveSupport::MessageEncryptor.
Toolx::Core::SimpleCrypt.encrypt('hello')
Toolx::Core::SimpleCrypt.decrypt(encrypted_string)
- Uses ENV['CRYPT_SECRET'] or Rails credentials by default.
- You can pass a custom secret: if needed.
encrypted = Toolx::Core::SimpleCrypt.encrypt('secret info', secret: 'a' * 32)
Toolx::Core::SimpleCrypt.decrypt(encrypted, secret: 'a' * 32)
❗️ Errors
- MissingSecretError – when no secret is configured
- EncryptionError – on encryption failure
- DecryptionError – on verification or decoding failure
Operation
A base class for service operations, providing a structured way to define and execute business logic with typed parameters and responses.
class Country::Code < Toolx::Core::Operation
self.transactional = false
simplified_result :value
params { attribute :source, Types.Instance(ISO3166::Country) | Types::Coercible::String }
response { attribute :value, Types::Symbol }
def perform = { value: Country::Object[source:].alpha2.to_sym }
private
delegate :source, to: :params
end
# when simplified_result is used it is possible to use it like this:
source = 'Polska'
Country::Code[source:] # => :PL
Country::Code.call(source:) # => :PL
Country::Code.(source:) # => :PL
# And default one
Country::Code.new(source:).perform.value # => :PL
Operation provides accessories like params
, response
but it can be used as Param and Response classes as well.
class Country::Code < Toolx::Core::Operation
self.transactional = false
after -> { 'send notification' }
before :validate!
rescue_from ActiveRecord::RecordInvalid, with: :raise_something
class Params
attribute :source, Types.Instance(ISO3166::Country) | Types::Coercible::String }
end
class Response
attribute :value, Types::Symbol
end
def perform = { value: Country::Object[source:].alpha2.to_sym }
private
def validate! = true
def raise_something(exception)
raise Toolx::Core::Errors::BadRequest, "Invalid source: #{exception.message}"
end
delegate :source, to: :params
end
Operation::Flow
A streamlined interface for orchestrating and executing service operations with support for:
- typed parameters (via Dry::Types)
- typed responses (with translation helpers & inquiry methods)
- flow control (.on_success, .on_fail, .bind_with, .perform, etc.)
class SignupOperation < Toolx::Core::Operation
include Toolx::Core::Operation::Flow
params do
attribute :email, Types::String
attribute :age, Types::Integer
end
response do
include Toolx::Core::Concerns::Inquirer
attribute :status, Types::String
attribute :message, Types::String
inquirer :status
end
def perform
if age < 18
{ status: 'error', message: 'You must be 18+' }
else
{ status: 'success', message: "Welcome, #{email}" }
end
end
end
🚀 Execution Flow
Initialize, perform, and access result:
flow = SignupOperation.flow
executor = flow.perform(email: 'foo@bar.com', age: 20)
executor.response.status.success?
executor.response.message
Retry logic:
executor.perform!
executor.perform!(email: ..., age: ...)
🔁 Handler Bindings
Symbolic method bindings:
SignupOperation.flow
.bind_with(self)
.on(success: :redirect_to_dashboard, fail: :render_form)
.perform(email: 'foo@bar.com', age: 17)
Proc-style handler chaining:
SignupOperation.flow
.on_success { |resp, exec| notify_user(resp.message) }
.on_fail { |resp, exec| logger.warn("Failed: #{resp.message}") }
.perform!(email: 'foo@bar.com', age: 20)
License
The gem is available as open source under the terms of the MIT License.
Code of Conduct
Everyone interacting in the Toolx project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.