Created
November 1, 2014 19:21
-
-
Save andremedeiros/d0449d1499aee56d9fd5 to your computer and use it in GitHub Desktop.
An experiment with services
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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