Skip to content

Instantly share code, notes, and snippets.

@rcmoret
Created April 5, 2022 19:22
Show Gist options
  • Save rcmoret/a20099550bee0f0d64a1bb8af8eb6d08 to your computer and use it in GitHub Desktop.
Save rcmoret/a20099550bee0f0d64a1bb8af8eb6d08 to your computer and use it in GitHub Desktop.
module CallChain
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
private
def define_function(name, function)
define_method(name) { function }
end
def function_chain(name, functions:)
define_method(name) { FunctionChain.new(functions, context: self) }
end
end
class FunctionChain
def initialize(functions, context:)
@function_chain = functions.map { |function| chainable_method(context, function) }
end
def call(**initial_args)
result = function_chain.reduce(Payload.new(:ok, initial_args)) { |memo, function| function.call(memo) }.as_result
block_given? ? yield(result) : result
end
private
def chainable_method(context, function)
case function
in Symbol => method_name
ChainableFunction.new(context, context.public_send(method_name))
in Proc => callable
ChainableFunction.new(context, callable)
end
end
attr_reader :function_chain
end
private_constant :FunctionChain
class ChainableFunction
def initialize(context, function)
@context = context
@function = function
end
attr_reader :function, :context
def call(payload)
case payload.tuple
in [:ok, messages, errors]
handle_call(function, payload, messages, errors).then do |args|
Payload.new(*args)
end
in [:error, messages, errors]
payload
end
end
private
def handle_call(function, payload, messages, errors)
case context.instance_exec(payload, &function)
in :ok | nil
payload.tuple
in [:ok, messages]
[:ok, payload.add(messages)]
in [:ok, messages, errors]
[:ok, payload.add(messages), payload.add_errors(errors)]
in [:error, messages, errors]
[:error, payload.add(messages), payload.add_errors(errors)]
in [:error, errors]
[:error, payload.messages, payload.add_errors(errors)]
end
end
end
private_constant :ChainableFunction
class Payload
def initialize(status, messages = {}, errors = {})
@status = status
@messages = MessageHash.new(messages, initial_keys: :warnings)
@errors = hash_with_sets.merge(errors)
end
def fetch(*args)
messages.fetch(*args)
end
def tuple
[status, messages, errors]
end
def as_result
[status, { messages: messages, errors: errors }]
end
def add(new_messages)
messages.merge(new_messages)
end
def add_errors(error_messages)
errors.merge(error_messages)
end
def warnings
messages.fetch(:warnings)
end
def delete(key)
raise KeyError unless messages.key?(key)
messages.delete(key)
end
attr_reader :messages
private
def hash_with_sets
Hash.new { |hash, key| hash[key] = Set.new }
end
attr_reader :status, :errors
end
private_constant :Payload
class MessageHash < Hash
def initialize(messages, initial_keys: [])
super() { |hash, key| hash[key] = Set.new }
.merge!(messages)
.tap { |hash| Array(initial_keys).each { |key| hash[key] } }
end
def merge(other_hash)
super(other_hash, &default_merge_block)
end
private
def default_merge_block
lambda { |_key, val1, val2|
case [val1, val2]
in [Set => set1, Array => collection]
set1 + collection
in [Set => set1, val]
set1 << val
else
val2
end
}
end
end
private_constant :MessageHash
end
require "faker"
class User
def self.find_by(id:)
case id
when 42
new(id: 42)
else
nil
end
end
def initialize(id:)
@id = id
end
attr_reader :id
def attributes
@attributes ||= {
"id" => id,
"business_id" => business_id,
"last_name" => Faker::Name.last_name,
}
end
def business_id
@business_id ||= rand(100)
end
end
class Business
def self.find_by(id:)
new(id: id)
end
def initialize(id:)
@id = id
end
attr_reader :id
def attributes
@attributes ||= {
"id" => id,
"name" => Faker::Company.name,
"industry" => Faker::Company.industry,
}
end
end
class FunnyBusiness
include CallChain
define_function :lookup_user, lambda { |payload|
user_id = payload.delete(:user_id)
User.find_by(id: user_id).then do |potential_user|
if potential_user.nil?
[:error, { user_lookup: "Could not find user w/ id: #{user_id}" }]
elsif !nice?
[:error, { user_lookup: "user w/ id: #{user_id} was not nice" }]
else
[:ok, { user: potential_user }]
end
end
}
define_function :format_user, lambda { |payload|
payload.fetch(:user).then do |user|
[
:ok,
{
warnings: "dropped attributes",
user: user.attributes.slice("id", "last_name"),
business_id: user.business_id,
},
]
end
}
define_function :lookup_business, lambda { |payload|
business_id = payload.delete(:business_id)
Business.find_by(id: business_id).then do |potential_business|
if potential_business.nil?
[:error, { business_lookup: "Could not find business w/ id: #{business_id}" }]
else
[:ok, { business: potential_business, warnings: "nothing to see here" }]
end
end
}
define_function :format_business, lambda { |payload|
if payload.warnings.none?
:ok
else
payload.fetch(:business).then do |business|
[
:ok,
{
warnings: "dropped attributes",
business: business.attributes.slice("id", "name", "industry"),
},
]
end
end
}
def payload_inspect
->(payload) { puts payload.inspect }
end
function_chain :user_and_biz_lookup_chain, functions: [
:lookup_user,
:format_user,
:lookup_business,
:format_business,
->(*) { :ok },
lambda { |_payload|
[:ok, *error_tuples].sample
},
:payload_inspect,
]
def call(user_id)
user_and_biz_lookup_chain.call(user_id: user_id)
end
private
def nice?
true
# [true, false].sample
end
def error_tuples
[
[:error, {}, { sorry: "about your luck" }],
[:error, { greetings: "try again" }],
]
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment