Created
April 5, 2022 19:22
-
-
Save rcmoret/a20099550bee0f0d64a1bb8af8eb6d08 to your computer and use it in GitHub Desktop.
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
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