Skip to content

bnlucas/gaskit

Repository files navigation

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, and Query classes
  • πŸ”€ Customizable result and early exit contracts via explicit input_contract / output_contract
  • 🧱 Composable multi-step flows using Flow DSL
  • πŸ§ͺ 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 install

Or 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
end

Handle 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]}"
end

Define Errors

class AuthOp < Gaskit::Operation
  error :unauthorized, "Not allowed", code: "AUTH-001"

  def call
    exit(:unauthorized)
  end
end

Composing 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
end

Register 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
end

Output 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
end

input_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 as payload:.
  • If you call with positional args, the casted payload is passed as a single positional argument.

Failure behavior

  • .call returns a failure OperationResult for contract errors or raised exceptions.
  • .call! raises on contract errors and StandardError exceptions from #call.
  • OperationExit raised via exit(:key, ...) always returns an early-exit OperationResult for both .call and .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
end

Use 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

About

Gaskit is a lightweight, extensible framework for building structured application operations.

Resources

License

Code of conduct

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages