Last active
December 30, 2021 12:53
-
-
Save waiting-for-dev/caece1891a84125fb3415026f8d310b3 to your computer and use it in GitHub Desktop.
POC for a new version of dry-transaction
This file contains hidden or 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
# 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 |
This file contains hidden or 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
# 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