Skip to content

Instantly share code, notes, and snippets.

@andremedeiros
Created November 1, 2014 19:21
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 andremedeiros/d0449d1499aee56d9fd5 to your computer and use it in GitHub Desktop.
Save andremedeiros/d0449d1499aee56d9fd5 to your computer and use it in GitHub Desktop.
An experiment with services
require 'ostruct'
class Context < OpenStruct
def fail(details)
@_failure = true
@_error = details
end
def fail!(details)
fail(details)
throw :halt
end
def error
@_error
end
def success?
!@_failure
end
end
module Kernel
def Context(attributes)
case attributes.class.name
when 'Context' then attributes
when 'Hash' then Context.new(attributes)
else raise TypeError, "can't convert #{ attributes.class } into Context"
end
end
end
class Organizer
class << self
attr_accessor :organized
def organizes(*services)
@organized = services.flatten
end
end
attr_accessor :context
def organized
self.class.organized
end
def initialize(attributes = {})
@context = Context(attributes)
end
def call
services_called = []
organized.each do |service|
instance = service.new(context)
instance.call
if context.success?
services_called << instance
else
services_called.reverse.each(&:rollback)
break
end
end
context.success?
end
end
require 'forwardable'
class Service
ServiceError = Class.new(StandardError)
RequirementNotMetError = Class.new(ServiceError)
ProvisionNotMetError = Class.new(ServiceError)
ImplementationMissingError = Class.new(ServiceError)
extend Forwardable
class << self
# Service requirements setup
attr_accessor :requirements
def requires(*requirements)
requirements = requirements.flatten
requirements.each do |requirement|
def_delegator :context, requirement
def_delegator :context, "#{ requirement }="
end
@requirements ||= []
@requirements += requirements
end
# Service provisions
attr_accessor :provisions
def provides(*provisions)
provisions = provisions.flatten
provisions.each do |provision|
def_delegator :context, "#{ provision }="
end
@provisions ||= []
@provisions += provisions
end
end
attr_accessor :context
def_delegators :context, :fail, :fail!
def initialize(attributes = {})
@context = Context(attributes)
end
def call
# Check for requirements
raise RequirementNotMetError unless requirements.all? { |req| context[req] }
catch(:halt) { execute }
# Check for provisions
raise ProvisionNotMetError unless provisions.all? { |prov| context[prov] }
rescue RequirementNotMetError
unmet_requirements = requirements.select { |req| !context[req] }
fail(message: "Requirements not met", requirements: unmet_requirements)
rescue ProvisionNotMetError
unmet_provisions = provisions.select { |prov| !context[prov] }
fail(message: 'Provisions not met', provisions: unmet_provisions)
ensure
context.success?
end
def execute
raise ImplementationMissingError, <<-STR.strip
Implement #execute on #{ self.class.name }
STR
end
# Might be a no-op most times.
def rollback; end
protected
def requirements
self.class.requirements || []
end
def provisions
self.class.provisions || []
end
end
require 'active_support/all'
def service(*namespace)
context = Context.new
yield context if block_given?
name = namespace.map(&:to_s).map(&:camelize).join('::')
klass = name.constantize
klass.new(context)
end
puts '=== Failing service (calling fail!)'
class FailingService1 < Service
def execute
fail!(message: 'YOU SHALL NOT PASS')
puts "Never gonna get here"
end
end
service = service(:failing_service1)
service.call
puts service.context.error
puts '=== Failing service (unmet requirements)'
class FailingService2 < Service
requires :foo
def execute; end
end
service = service(:failing_service2)
service.call
puts service.context.error
puts '=== Failing service (unmet provisions)'
class FailingService3 < Service
provides :foo
def execute; end
end
service = service(:failing_service3)
service.call
puts service.context.error
puts '=== Organizers '
class Service1 < Service
def execute
puts 'Service1 executing'
end
def rollback
puts 'Service1 rolling back'
end
end
class Service2 < Service
provides :foo
def execute
self.foo = 'abc'
puts 'Service2 executing'
end
def rollback
puts 'Service2 rolling back'
end
end
class Service3 < Service
requires :foo
provides :bar
def execute
puts 'Service3 executing'
self.bar = 1
end
def rollback
puts 'Service3 rolling back'
end
end
class Organizer1 < Organizer
organizes Service1, Service2, Service3
end
organizer ||= service(:organizer1) do |ctx|
ctx.params = 'omg'
end
puts organizer.call
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment