Skip to content

Instantly share code, notes, and snippets.

@amoslanka
Created December 4, 2014 05:20
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save amoslanka/981104b9c7deb8e75343 to your computer and use it in GitHub Desktop.
Save amoslanka/981104b9c7deb8e75343 to your computer and use it in GitHub Desktop.
A stripped down version of interactors.
require "ostruct"
# A base Service Object module.
#
# Example usage:
#
# class DoSomething
# include Service
# def call
# puts context.foo
# end
# end
#
# Alternatively, execute the service object and allow failures to raise errors.
#
# DoSomething.call! foo: :bar
#
# To declare a failure within a service's call method:
#
# class DoSomething
# include Service
# def call
# fail!
# end
# end
#
# Invoke a service object:
#
# DoSomething.call foo: :bar
#
# The key `:foo` with the value `:bar` will be set on the context of the service
# object, available to the call method when it runs and to the caller of the
# service when it is complete.
#
# Any values assigned to the context will be returned by the call method in the
# context object after the call method is complete.
#
# class DoSomething
# include Service
# def call
# context.foo = :bar
# end
# end
#
# result = DoSomething.call
# puts result.foo
# => :bar
#
#
module Service
# Internal: Install Interactor's behavior in the given class.
def self.included(base)
base.class_eval do
extend ClassMethods
# Public: Gets the Interactor::Context of the Interactor instance.
attr_reader :context
end
end
# Internal: Interactor class methods.
module ClassMethods
# Public: Invoke the service object. Wraps up instantiation and calling the
# #call method on the instance. Returns the context.
#
# context - A hash of object for service object to act upon.
def call(context={})
new(context).tap(&:run).context
end
# Public: Invoke the service object, the same as `.call`, but allow failures
# to raise errors.
#
# context - A hash of object for service object to act upon.
def call!(context={})
new(context).tap(&:run!).context
end
end
# Public: Initialize a service object.
def initialize(context={})
@context = Context.build(context)
end
# Public: The primary process method. Override this method in your service object.
def call
call!
rescue Failure
end
# Internal: Used by the class to invoke the call method with rescue protection.
def run
run!
rescue Failure
end
# Internal: Used by the class to invoke the call method without rescue protection.
def run!; call; end
# Public: The object for tracking state of a service object's interaction with
# the subjects it acts upon. The context contains all arguments initially passed
# into the invokation, and is the object returned after the process is complete.
# The service object manipulates the context to produce the result of invocation.
#
# A context is little more than an OpenStruct and can be manipulated using hash
# or dot syntax.
#
# Examples
#
# context = Service::Context.new
# # => #<Service::Context>
# context.foo = "bar"
# # => "bar"
# context
# # => #<Service::Context foo="bar">
# context.hello = "world"
# # => "world"
# context
# # => #<Service::Context foo="bar" hello="world">
# context.foo = "baz"
# # => "baz"
# context
# # => #<Service::Context foo="baz" hello="world">
class Context < OpenStruct
# Internal: Initialize a context or preserve an existing one. If the argument
# given is a context, the argument is returned. Otherwise, a new context is
# initialized from the provided hash.
#
# The "build" method is used during service object initialization.
def self.build(context={})
self === context ? context : new(context)
end
# Public: Whether the context, which represents the state of a service
# invokation, is successful. By default, a new context is successful and only
# changes when explicitly failed.
#
# Returns true by default or false if failed.
def success?
!failure?
end
# Public: Whether the context, which represents the state of a service
# invokation, has failed. By default, a new context is successful and only
# changes when explicitly failed.
#
# Returns false by default or true if failed.
def failure?
@failure || false
end
# Public: Fail the context, which represents the state of a service invokation.
# Failing a context raises an error that may be rescued by the calling service.
# The context is also flagged as having failed.
#
# Optionally the caller may provide a hash of key/value pairs to be merged
# into the context before failure.
#
# context - A Hash whose key/value pairs are merged into the existing
# context. (default: {})
#
# Examples
#
# context = Service::Context.new
# # => #<Service::Context>
# context.fail!
# # => Service::Failure: #<Service::Context>
# context.fail! rescue false
# # => false
# context.fail!(foo: "baz")
# # => Service::Failure: #<Service::Context foo="baz">
#
# Raises Service::Failure initialized with the Service::Context.
def fail!(context = {})
modifiable.update(context)
@failure = true
raise Failure, self
end
end
# Internal: An error class representing the failure of a service to complete
# execution as it expects to do. A Failure error holds a reference to the
# context used in the service.
class Failure < StandardError
attr_reader :context
def initialize(context=nil)
@context = context
super
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment