CtrlK
BlogDocsLog inGet started
Tessl Logo

igmarin/hanakai-yaku

Curated library of atomic AI agent skills for Hanami, dry-rb, and ROM Ruby development. Covers actions, slices, repositories, relations, changesets, providers, DI, operations, TDD, CLI, views, routing, and validation. Shared Ruby process skills have moved to ruby-core-skills. Uses Markdown + Front-matter architecture.

92

1.33x
Quality

94%

Does it follow best practices?

Impact

92%

1.33x

Average score across 35 eval scenarios

SecuritybySnyk

Passed

No known issues

Overview
Quality
Evals
Security
Files

SKILL.mdskills/dry-monads/handle-result-pattern/

name:
handle-result-pattern
license:
MIT
description:
Use when using dry-monads Result pattern in Hanami 2.x. Covers Success/Failure wrapping, bind/fmap chaining, and use in service objects registered in the DI container.
metadata:
{"ecosystem_sources":["dry-rb/dry-monads"],"tags":["dry-monads","result","monads","service-objects"],"version":"1.0.0"}

handle-result-pattern

Use this skill when using the dry-monads Result pattern in Hanami 2.x.

Core principle: Explicitly model success and failure. Return Success or Failure from operations that can fail, then chain them with bind and fmap.


Quick Reference

ScenarioApproach
Wrap a successSuccess(value)
Wrap a failureFailure(error_message) or Failure(error_object)
Chain on successresult.bind { |value| next_operation(value) }
Transform success valueresult.fmap { |value| value.upcase }
Handle failureresult.or { |error| fallback_operation(error) }
Extract valueresult.value! (raises on Failure)
Check statusresult.success? / result.failure?
Pattern matchcase result; in Success(v); ...; in Failure(e); ...; end

Core Rules

  1. Use dry-monads in service objects registered in the DI container:

    # app/operations/create_user.rb
    # frozen_string_literal: true
    
    require "dry/monads"
    require "dry/monads/do"
    
    module MyApp
      module Operations
        class CreateUser
          include Dry::Monads[:result]
          include Dry::Monads::Do.for(:call)
    
          include Deps["repos.user_repo"]
    
          def call(attrs)
            validated = yield validate(attrs)
            user = yield create_user(validated)
    
            Success(user)
          end
    
          private
    
          def validate(attrs)
            return Failure("Email is required") if attrs[:email].nil? || attrs[:email].empty?
            return Failure("Name is required") if attrs[:first_name].nil? || attrs[:first_name].empty?
    
            Success(attrs)
          end
    
          def create_user(attrs)
            user = user_repo.create(attrs)
            Success(user)
          rescue StandardError => e
            Failure("Database error: #{e.message}")
          end
        end
      end
    end
  2. Register the service object in the container:

    # config/providers/operations.rb
    Hanami.app.register_provider(:operations) do
      start do
        register("operations.create_user", MyApp::Operations::CreateUser.new)
      end
    end
  3. Inject and call the service object in an Action:

    # app/actions/users/create.rb
    module MyApp
      module Actions
        module Users
          class Create < MyApp::Action
            include Deps["operations.create_user"]
    
            def handle(request, response)
              result = create_user.call(request.params[:user])
    
              case result
              in Dry::Monads::Success(user)
                response.status = 201
                response.body = user.to_json
              in Dry::Monads::Failure(error)
                response.status = 422
                response.body = { error: error }.to_json
              end
            end
          end
        end
      end
    end
  4. Chain operations with bind:

    result = validate_params(params)

result.bind { |valid| find_user(valid[:id]).bind { |user| update_user(user, valid[:attrs]) } }.fmap { |user| format_user(user) }

5. **Use `Do notation`** for sequential operations:

```ruby
def call(attrs)
  validated = yield validate(attrs)
  user = yield create_user(validated)
  notify = yield send_welcome_email(user)

  Success(user)
end
  1. Model failures as data, not exceptions:

    # GOOD: Return Failure with structured error
    Failure({ code: :email_taken, message: "Email already registered" })
    
    # BAD: Raise exception for expected failures
    raise EmailTakenError, "Email already registered"
  2. Keep service objects focused. One operation = one class. Do not create monolithic operation classes.


Common Mistakes & Red Flags

Reviewers and developers should check for these patterns:

  • Raising exceptions for expected domain errors (validation, resource missing). Use Failure instead.
  • Unsafely unwrapping monadic Results with value! without verifying status. Use pattern matching (in Success(v)) or flow-based chains (bind/fmap).
  • Putting business logic in HTTP Actions instead of service objects.
  • Forgetting to register service objects/operations in the DI container.
  • Returning ambiguous nil or false values instead of explicit Success or Failure.
  • Writing deeply nested bind blocks. Use Do notation (yield) to flatten sequential operations.
  • Creating monolithic service objects doing multiple unrelated tasks.

Integration

Related SkillWhen to chain
inject-dependenciesService objects are injected into Actions via Deps[].
create-actionActions call service objects and handle Success/Failure.
handle-errorsFailure results map to HTTP error responses (422, 404, etc.).
validation-contractValidation results are often Success/Failure from dry-validation.
refactor-codeExtract business logic from Actions into Success/Failure service objects.

Rails → Hanami Reference

For developers transitioning from Rails/ActiveRecord exception patterns, see the side-by-side RAILS_COMPARISON.md guide.

skills

dry-monads

handle-result-pattern

README.md

tile.json