Class: Gaskit::Operation Abstract

Inherits:
Object
  • Object
show all
Includes:
Hookable
Defined in:
lib/gaskit/operation.rb

Overview

This class is abstract.

Subclass this and define #call or #call! to create a new operation.

The Gaskit::Operation class defines a structured and extensible pattern for building application operations. It enforces consistent behavior across operations while supporting customization via contracts.

Features

  • Pluggable contracts via use_contract, allowing you to define or reference a result class.
  • Integrated duration tracking, structured logging, and early exits.
  • Supports .call (non-raising) and .call! (raising) styles.

Examples:

Using a registered contract

class MyOperation < Gaskit::Operation
  use_contract :service

  def call
    # Do work
    "done"
  end
end

Overriding only part of the contract

class MyCustomOp < Gaskit::Operation
  use_contract :service, result: MyCustomResult

  def call
    exit(:unauthorized, "User not allowed") if unauthorized?
    "okay"
  end
end

Fully manual contract

class ManualOp < Gaskit::Operation
  use_contract result: MyResult, early_exit: MyExit

  def call
    do_work
  end
end

result = ManualOp.call(context: { request_id: "abc123" })

Direct Known Subclasses

Query, Service

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Hookable

#apply_after_hooks, #apply_around_hooks, #apply_before_hooks, #apply_hooks, included

Constructor Details

#initialize(raise_on_failure, context: {}) ⇒ void

Initializes a new Gaskit::Operation instance.

Raises:



265
266
267
268
269
270
271
272
273
# File 'lib/gaskit/operation.rb', line 265

def initialize(raise_on_failure, context: {})
  @raise_on_failure = raise_on_failure
  @context = apply_context(context)
  @logger = Gaskit::Logger.new(self, context: @context)

  return unless self.class.result_class.nil?

  raise Gaskit::Error, "No result_class defined for #{self.class.name} or its ancestors."
end

Instance Attribute Details

#contextObject (readonly)

Returns the value of attribute context.



258
259
260
# File 'lib/gaskit/operation.rb', line 258

def context
  @context
end

#loggerObject (readonly)

Returns the value of attribute logger.



258
259
260
# File 'lib/gaskit/operation.rb', line 258

def logger
  @logger
end

#raise_on_failureObject (readonly)

Returns the value of attribute raise_on_failure.



258
259
260
# File 'lib/gaskit/operation.rb', line 258

def raise_on_failure
  @raise_on_failure
end

Class Method Details

.call(*args, context: {}, **kwargs, &block) ⇒ Gaskit::OperationResult

Executes the operation with soft-failure handling



117
118
119
# File 'lib/gaskit/operation.rb', line 117

def call(*args, context: {}, **kwargs, &block)
  invoke(false, context, *args, **kwargs, &block)
end

.call!(*args, context: {}, **kwargs, &block) ⇒ Gaskit::OperationResult

Executes the operation with hard-failure handling (raises on unhandled errors)



127
128
129
# File 'lib/gaskit/operation.rb', line 127

def call!(*args, context: {}, **kwargs, &block)
  invoke(true, context, *args, **kwargs, &block)
end

.error(key, message, code: nil) ⇒ void

This method returns an undefined value.

Declares a symbolic error and message for use with exit(:key)

Examples:

error :unauthorized, "You must be signed in", code: "AUTH-001"

Raises:

  • (ArgumentError)


96
97
98
99
100
101
102
# File 'lib/gaskit/operation.rb', line 96

def error(key, message, code: nil)
  raise ArgumentError, "Error key must be a symbol or a string, received #{key}" unless key.is_a?(Symbol)
  raise ArgumentError, "Error message must be a string" unless message.is_a?(String)
  raise ArgumentError, "Error key :#{key} is already registered" if errors_registry.key?(key)

  errors_registry[key.to_sym] = { message: message, code: code }
end

.errors_registryvoid

This method returns an undefined value.

Returns the error registry for the operation class



107
108
109
# File 'lib/gaskit/operation.rb', line 107

def errors_registry
  @errors_registry ||= {}
end

.result_classObject



54
55
56
57
58
59
# File 'lib/gaskit/operation.rb', line 54

def result_class
  return @result_class if defined?(@result_class) && @result_class
  return superclass&.result_class if superclass.respond_to?(:result_class)

  nil
end

.use_contract(contract = nil, result: nil) ⇒ void

This method returns an undefined value.

Defines the result class for this operation. Can reference a named contract registered in Gaskit::Registry, or define one without using a registered contract.

Examples:

Use a registered contract

use_contract :service

Define a contract that has not been registered

use_contract result: CustomResult

Raises:

  • (ArgumentError)

    if contract is not a symbol or unexpected args are passed

  • (ResultTypeError)

    if result is not a subclass of Gaskit::BaseResult



76
77
78
79
80
81
82
83
84
85
# File 'lib/gaskit/operation.rb', line 76

def use_contract(contract = nil, result: nil)
  if contract
    raise ArgumentError, "use_contract must be called with a symbol or keyword args" unless contract.is_a?(Symbol)

    result = Gaskit.contracts.fetch(contract)
  end

  Gaskit::ContractRegistry.verify_result_class!(result)
  @result_class = result
end

Instance Method Details

#call(*args, **kwargs) ⇒ void

This method returns an undefined value.

Executes the operation logic.

Raises:

  • (NotImplementedError)

    Must be implemented by subclasses.



285
286
287
# File 'lib/gaskit/operation.rb', line 285

def call(*args, **kwargs)
  raise NotImplementedError, "#{self.class.name} must implement `#call`"
end

#exit(error_key, message = nil, code: nil) ⇒ Object Also known as: abort

Terminates the operation early with a symbolic key.

If the key was previously registered via self.error, it uses the declared message and code. Otherwise, it uses the key as the message.

Raises:

  • (OperationExit)

    always raises an instance with message and optional code



298
299
300
301
302
303
304
305
306
307
308
# File 'lib/gaskit/operation.rb', line 298

def exit(error_key, message = nil, code: nil)
  error_key = error_key.to_sym
  definition = self.class.errors_registry.fetch(error_key, nil)

  if definition
    message ||= definition[:message]
    code ||= definition[:code]
  end

  raise OperationExit.new(error_key, message, code: code)
end

#raise_on_failure?Boolean



275
276
277
# File 'lib/gaskit/operation.rb', line 275

def raise_on_failure?
  @raise_on_failure
end