Gaskit
Gaskit is a flexible, pluggable, and structured operations framework for Ruby applications. It provides a consistent way to implement application logic using service objects, query objects, flows, and contracts โ with robust support for early exits, structured logging, duration tracking, and failure handling.
โจ Features
- โ
Operation,Service, andQueryclasses - ๐ Customizable result and early exit contracts via explicit
input_contract/output_contract - ๐งฑ Composable multi-step flows using
FlowDSL - ๐งช Built-in error declarations and early exits via
exit(:key) - โฑ Integrated duration tracking and structured logging
- ๐ช Hook system for before/after/around instrumentation and auditing
๐ฆ Installation
Add this line to your application's Gemfile:
gem 'gaskit'And then execute:
$ bundle installOr install it yourself as:
$ gem install gaskit๐ง Configuration
You can configure Gaskit via an initializer:
Gaskit.configure do |config|
config.context_provider = -> { { request_id: RequestStore.store[:request_id] } }
config.setup_logger(Logger.new($stdout), formatter: Gaskit::Logger.formatter(:pretty))
end๐ Usage
Basic Operation (No Contracts)
class Ping < Gaskit::Operation
def call
"pong"
end
end
result = Ping.call
result.value # => "pong"Operation.call always returns a Gaskit::OperationResult wrapper.
Define an Operation
class MyOp < Gaskit::Operation
output_contract do
string :status
end
def call(user_id:)
user = User.find_by(id: user_id)
exit(:not_found, "User not found") unless user
logger.info("Found user id=#{user_id}")
{ status: "ok" }
end
endHandle Result
result = MyOp.call(user_id: 42)
if result.success?
puts "Found user: #{result.value}"
elsif result.early_exit?
puts "Early exit: #{result.to_h[:exit]}"
else
puts "Failure: #{result.to_h[:error]}"
endDefine Errors
class AuthOp < Gaskit::Operation
error :unauthorized, "Not allowed", code: "AUTH-001"
def call
exit(:unauthorized)
end
endComposing Flows
class CheckoutFlow < Gaskit::Flow
step AddToCart
step ApplyDiscount
step FinalizeOrder
end
result = CheckoutFlow.call(user_id: 123)๐ช Hooks
Use use_hooks to activate instrumentation:
class HookedOp < Gaskit::Operation
use_hooks :audit
before do |op|
op.logger.info("Starting operation")
end
after do |op, result:, error:|
op.logger.info("Finished with result=#{result.inspect} error=#{error.inspect}")
end
def call
"hello"
end
endRegister global hooks via:
Gaskit.hooks.register(:before, :audit) { |op| puts "Before: #{op.class}" }๐ Contracts
Gaskit uses Castkit for input/output validation at the operation boundary.
Input contracts run before any hooks. Output contracts run after a successful call
and before after hooks, so after hooks observe the final OperationResult.
Input contracts with DTOs
class UserInput < Castkit::DataObject
string :user_id
integer :limit, required: false
end
class FindUser < Gaskit::Operation
input_contract UserInput
def call(payload:)
payload.user_id
end
endOutput contracts with DTOs
class UserOutput < Castkit::DataObject
string :name
string :role
end
class LoadUser < Gaskit::Operation
output_contract UserOutput
def call(user_id:)
{ name: "user-#{user_id}", role: "admin" }
end
end
result = LoadUser.call(user_id: 42)
result.value # => #<UserOutput ...>Input/Output validation with Castkit
Optionally validate inputs and outputs using Castkit contracts or DTOs:
class ValidateOp < Gaskit::Operation
input_contract do
string :user_id
integer :limit, required: false
end
output_contract do
string :status
hash :meta, required: false
end
def call(user_id:, limit: 10)
{ status: "ok", meta: { limit: limit.to_i } }
end
endinput_contract and output_contract accept either a Castkit contract class, a Castkit data object, or a DSL block. Validation runs before executing #call (inputs) and after a successful call (outputs). Failures raise Castkit::ContractError (returned as a failure result for .call, raised for .call!). Input payloads are normalized (payload: hash, then kwargs, then a single Hash arg, else { args: [...] }). Outputs are validated directly when hashes, otherwise wrapped as { value: result } and unwrapped after casting.
Calling convention
- If you call with kwargs (including
payload:), the casted payload is passed as keyword args when it is a symbol-keyed Hash; otherwise it is passed aspayload:. - If you call with positional args, the casted payload is passed as a single positional argument.
Failure behavior
-
.callreturns a failureOperationResultfor contract errors or raised exceptions. -
.call!raises on contract errors and StandardError exceptions from#call. -
OperationExitraised viaexit(:key, ...)always returns an early-exitOperationResultfor both.calland.call!.
Castkit defaults
Gaskit configures Castkit with strict defaults (enforce_typing, enforce_attribute_access, enforce_array_options, strict_by_default) and registers :any/:symbol types (aliases :object and :sym).
Cache stores
Gaskit ships with Gaskit::Stores::MemoryStore and Gaskit::Stores::RedisStore. Configure globally via:
Gaskit.configure do |config|
config.cache_store :redis, connection: Redis.new
endUse Gaskit::Stores.register(:name, Klass) for custom stores. For cacheable classes, you can disable caching with cache_store :disabled, and config.enforce_cache_store controls whether missing stores raise.
๐งฑ Repositories
class UserRepository < Gaskit::Repository
model User
def find_by_name_or_slug(name, profile_slug)
where(name: name).or(where(profile_slug: profile_slug))
end
end
users = UserRepository.where(active: true)
user = UserRepository.find_by_name_or_slug("User", "user123")๐ Logging
Gaskit includes a flexible logger with support for structured JSON or pretty logs:
logger = Gaskit::Logger.new(self.class)
logger.info("Started process", context: { user_id: 1 })Planned Features
- Caching Flow operations to provide replaying and resume on failure.
๐ค Contributing
Bug reports and pull requests are welcome! Feel free to fork, extend, and share improvements.
๐ License
This gem is licensed under the MIT License.
Made with โค๏ธ by bnlucas