Skip to content

Instantly share code, notes, and snippets.

@waiting-for-dev
Last active December 30, 2021 12:53
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 waiting-for-dev/caece1891a84125fb3415026f8d310b3 to your computer and use it in GitHub Desktop.
Save waiting-for-dev/caece1891a84125fb3415026f8d310b3 to your computer and use it in GitHub Desktop.
POC for a new version of dry-transaction
# frozen_string_literal: true
require "dry/monads"
require "rspec"
require "ostruct"
module Dry
module TransactionResurrection
def self.[](monad)
Module.new do
@monad = monad
def self.included(klass)
klass.extend(DSL.new(@monad))
klass.include(InstanceMethods)
end
end
end
def self.included(klass)
klass.include(self[:result])
end
class DSL < Module
attr_reader :adapter
def initialize(monad)
@adapter = Kernel.const_get("Dry::TransactionResurrection::Adapters::#{monad.capitalize}").new
self.class.include(Dry::Monads[monad])
define_step
define___steps__
define_adapter
end
def define_step
define_method :step do |name, take: [:_previous]|
__steps__ << [name, take]
end
end
def define___steps__
define_method :__steps__ do
@__steps__ ||= []
end
end
def define_adapter
module_exec(@adapter) do |adapter|
define_method(:adapter) { adapter }
end
end
end
module InstanceMethods
attr_reader :outputs
def call(input)
adapter = self.class.adapter
outputs_0 = { _initial: input, _previous: input }
result_0 = adapter.pure(input)
result, outputs = self.class.__steps__.reduce([result_0, outputs_0]) do |(result, outputs), (method_name, take)|
new_result = adapter.bind(result) do |value|
adapter.coerce(
method(method_name).call(*outputs.values_at(*take))
)
end
adapter.case(
new_result,
->(value) { [new_result, outputs.merge(_previous: value, method_name => value)] },
-> { [new_result, outputs] }
)
end
@outputs = outputs
result
end
end
module Adapters
class Result
include Dry::Monads[:result]
def pure(value)
Success(value)
end
def coerce(value)
value.to_result
end
def bind(value, &block)
value.bind(&block)
end
def case(value, success, failure)
case value
in Success[x]
success.(x)
in Failure
failure.()
end
end
end
class Maybe
include Dry::Monads[:maybe]
def pure(value)
Some(value)
end
def coerce(value)
value.to_maybe
end
def bind(value, &block)
value.bind(&block)
end
def case(value, success, failure)
case value
in Some[x]
success.(x)
in None
failure.()
end
end
end
end
end
end
RSpec.describe Dry::TransactionResurrection do
include Dry::Monads[:result, :maybe]
it "chains using Result monad by default" do
t = Class.new do
include Dry::TransactionResurrection
include Dry::Monads[:result]
step :create_user
step :log
private
def create_user(username)
Success(OpenStruct.new(username: username))
end
def log(user)
Success("Logged user #{user.username}")
end
end
result = t.new.call("Alice")
expect(result).to eq(Success("Logged user Alice"))
end
it "stops chaining when a failure is found" do
t = Class.new do
include Dry::TransactionResurrection
include Dry::Monads[:result]
step :create_user
step :log
private
def create_user(username)
Failure(:no_more_users_are_allowed)
end
def log(user)
Success("Logged user #{user.username}")
end
end
result = t.new.call("Alice")
expect(result).to eq(Failure(:no_more_users_are_allowed))
end
it "can use a different type of monad" do
t = Class.new do
include Dry::TransactionResurrection[:maybe]
include Dry::Monads[:maybe]
step :create_user
step :log
private
def create_user(username)
Some(OpenStruct.new(username: username))
end
def log(user)
Some("Logged user #{user.username}")
end
end
result = t.new.call("Alice")
expect(result).to eq(Some("Logged user Alice"))
end
it "can coerce following the adapter rules" do
t = Class.new do
include Dry::TransactionResurrection[:maybe]
include Dry::Monads[:maybe, :result]
step :create_user
step :log
private
def create_user(username)
Some(OpenStruct.new(username: username))
end
def log(user)
Failure("Logged user #{user.username}")
end
end
result = t.new.call("Alice")
expect(result).to eq(None())
end
it "can use other steps inputs" do
t = Class.new do
include Dry::TransactionResurrection
include Dry::Monads[:result]
step :create_user
step :create_user_greeting
step :log, take: %i[create_user create_user_greeting]
private
def create_user(username)
Success(OpenStruct.new(username: username))
end
def create_user_greeting(user)
Success("Hi, #{user.username}")
end
def log(user, greeting)
Success("Logged user #{user.username} and said '#{greeting}'")
end
end
result = t.new.call("Alice")
expect(result).to eq(Success("Logged user Alice and said 'Hi, Alice'"))
end
it "can reuse initial input" do
t = Class.new do
include Dry::TransactionResurrection
include Dry::Monads[:result]
step :create_user
step :log, take: %i[_initial]
private
def create_user(username)
Success(OpenStruct.new(username: username))
end
def log(username)
Success("Logged user #{username}")
end
end
result = t.new.call("Alice")
expect(result).to eq(Success("Logged user Alice"))
end
it "can inspect individual steps once the transaction is done" do
t = Class.new do
include Dry::TransactionResurrection
include Dry::Monads[:result]
step :create_user
step :log
private
def create_user(username)
Success(OpenStruct.new(username: username))
end
def log(user)
Success("Logged user #{user.username}")
end
end
t_instance = t.new
t_instance.call("Alice")
expect(t_instance.outputs[:create_user]).to eq(OpenStruct.new(username: 'Alice'))
end
end
# frozen_string_literal: true
require "dry/monads"
require "dry/core/constants"
require "rspec"
require "ostruct"
module Dry
module TransactionResurrection
class Step
class Halt < StandardError
attr_reader :failure
def initialize(failure)
@failure = failure
end
end
class Execution
attr_reader :result, :trace
def initialize(result: Dry::Core::Constants::Undefined, trace:)
@result = result
@trace = trace
end
def with(result: nil, trace: nil)
self.class.new(
result: result || self.result,
trace: trace || self.trace
)
end
end
class DefaultTrace
attr_reader :results
def initialize(results: [])
@results = results
end
def call(result)
self.class.new(results: results + [result])
end
end
attr_reader :execution, :trace
def initialize(trace:)
@trace = trace
@execution = Execution.new(trace: trace)
end
def call(result)
@execution = execution.with(
result: result,
trace: trace.(result)
)
result.value_or { raise Halt.new(result) }
end
def result
execution.result
end
def trace
execution.trace
end
end
def self.included(klass)
klass.define_singleton_method(:transaction) do |trace: Step::DefaultTrace.new, &block|
define_method(:call) do |input|
transaction(input, trace: trace) do |input, t|
instance_exec(input, t, &block)
end
end
end
end
def transaction(input, trace: Step::DefaultTrace.new, &block)
Step.new(trace: trace).tap do |t|
block.call(input, t)
rescue Step::Halt => e
e.failure
end
end
end
end
RSpec.describe Dry::TransactionResurrection do
include Dry::Monads[:result, :maybe]
it "using DSL" do
t = Class.new do
include Dry::TransactionResurrection
include Dry::Monads[:result]
transaction do |input, t|
user = t.(create_user(input))
t.(log(user))
end
private
def create_user(username)
Success(OpenStruct.new(username: username))
end
def log(user)
Success("Logged user #{user.username}")
end
end
result = t.new.call("Alice").result
expect(result).to eq(Success("Logged user Alice"))
end
it "stops chaining when a failure is found" do
t = Class.new do
include Dry::TransactionResurrection
include Dry::Monads[:result]
transaction do |input, t|
user = t.(create_user(input))
t.(log(user))
end
private
def create_user(username)
Failure(:no_more_users_are_allowed)
end
def log(user)
Success("Logged user #{user.username}")
end
end
result = t.new.call("Alice").result
expect(result).to eq(Failure(:no_more_users_are_allowed))
end
it "can use other steps inputs" do
t = Class.new do
include Dry::TransactionResurrection
include Dry::Monads[:result]
transaction do |input, t|
user = t.(create_user(input))
greeting = t.(create_user_greeting(user))
t.(log(user, greeting))
end
private
def create_user(username)
Success(OpenStruct.new(username: username))
end
def create_user_greeting(user)
Success("Hi, #{user.username}")
end
def log(user, greeting)
Success("Logged user #{user.username} and said '#{greeting}'")
end
end
result = t.new.call("Alice").result
expect(result).to eq(Success("Logged user Alice and said 'Hi, Alice'"))
end
it "can reuse initial input" do
t = Class.new do
include Dry::TransactionResurrection
include Dry::Monads[:result]
transaction do |input, t|
user = t.(create_user(input))
t.(log(input))
end
private
def create_user(username)
Success(OpenStruct.new(username: username))
end
def log(username)
Success("Logged user #{username}")
end
end
result = t.new.call("Alice").result
expect(result).to eq(Success("Logged user Alice"))
end
it "can inspect trace once the transaction is done" do
t = Class.new do
include Dry::TransactionResurrection
include Dry::Monads[:result]
transaction do |input, t|
user = t.(create_user(input))
t.(log(user))
end
private
def create_user(username)
Success(OpenStruct.new(username: username))
end
def log(user)
Success("Logged user #{user.username}")
end
end
t_instance = t.new
trace = t_instance.call("Alice").trace
expect(trace.results[0]).to eq(Success(OpenStruct.new(username: 'Alice')))
end
it "can configure its own trace handling" do
t = Class.new do
include Dry::TransactionResurrection
include Dry::Monads[:result]
class Trace
attr_reader :number_of_steps
def initialize
@number_of_steps = 0
end
def call(_result)
self.tap { @number_of_steps += 1 }
end
end
transaction(trace: Trace.new) do |input, t|
user = t.(create_user(input))
t.(log(user))
end
private
def create_user(username)
Success(OpenStruct.new(username: username))
end
def log(user)
Success("Logged user #{user.username}")
end
end
t_instance = t.new
trace = t_instance.call("Alice").trace
expect(trace.number_of_steps).to be(2)
end
it "composing is not rocket science" do
t1 = Class.new do
include Dry::TransactionResurrection
include Dry::Monads[:result]
transaction do |input, t|
t.(create_user(input))
end
private
def create_user(username)
Success(OpenStruct.new(username: username))
end
end
t2 = Class.new do
include Dry::TransactionResurrection
include Dry::Monads[:result]
transaction do |input, t|
user = t.(t1.new.(input).result)
t.(log(user))
end
private
def log(user)
Success("Logged user #{user.username}")
end
end
result = t2.new.call("Alice").result
expect(result).to eq(Success("Logged user Alice"))
end
it "using instance method" do
t = Class.new do
include Dry::TransactionResurrection
include Dry::Monads[:result]
def call(input)
transaction(input) do |input, t|
user = t.(create_user(input))
t.(log(user))
end
end
private
def create_user(username)
Success(OpenStruct.new(username: username))
end
def log(user)
Success("Logged user #{user.username}")
end
end
execution = t.new.call("Alice")
expect(execution.result).to eq(Success("Logged user Alice"))
expect(execution.trace.results[0]).to eq(Success(OpenStruct.new(username: "Alice")))
end
it "using instance method and custom trace" do
t = Class.new do
include Dry::TransactionResurrection
include Dry::Monads[:result]
class Trace
attr_reader :number_of_steps
def initialize
@number_of_steps = 0
end
def call(_result)
self.tap { @number_of_steps += 1 }
end
end
def call(input)
transaction(input, trace: Trace.new) do |input, t|
user = t.(create_user(input))
t.(log(user))
end
end
private
def create_user(username)
Success(OpenStruct.new(username: username))
end
def log(user)
Success("Logged user #{user.username}")
end
end
execution = t.new.call("Alice")
expect(execution.trace.number_of_steps).to be(2)
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment