From d37f7a5614238148cb55712c13c03ddb341fc39b Mon Sep 17 00:00:00 2001 From: Ivan Date: Thu, 1 Jul 2021 16:53:44 +0300 Subject: [PATCH] Use Kernel#throw as success interrupter (#7) * Remove Success Interruption, use Kernel#throw instead * Add spec for rollback operation * Add specs for failure * More checks * Remove value from success * Fix bug with catching thows of other classes * Fix bugs found in discus --- Gemfile.lock | 4 +-- lib/resol/service.rb | 39 ++++++++------------- lib/resol/version.rb | 2 +- spec/interruptions_spec.rb | 29 ++++++---------- spec/service_spec.rb | 70 +++++++++++++++++++++++++++++++++++++- 5 files changed, 96 insertions(+), 48 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 5c25a6d..eb8357e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - resol (0.5.1) + resol (0.6.0) smart_initializer (~> 0.7) GEM @@ -131,4 +131,4 @@ DEPENDENCIES simplecov-lcov BUNDLED WITH - 2.2.19 + 2.2.21 diff --git a/lib/resol/service.rb b/lib/resol/service.rb index 8052077..1fcf975 100644 --- a/lib/resol/service.rb +++ b/lib/resol/service.rb @@ -8,58 +8,47 @@ module Resol class Service class InvalidCommandImplementation < StandardError; end - class Interruption < StandardError - attr_accessor :data + class Failure < StandardError + attr_accessor :data, :code - def initialize(data) + def initialize(code, data) + self.code = code self.data = data - super + super(data) end def inspect "#{self.class.name}: #{message}" end - def message - data.inspect - end - end - - class Failure < Interruption - attr_accessor :code - - def initialize(code, data) - self.code = code - super(data) - end - def message data ? "#{code.inspect} => #{data.inspect}" : code.inspect end end - class Success < Interruption; end - include SmartCore::Initializer include Resol::Builder include Resol::Callbacks + Result = Struct.new(:data) + class << self def inherited(klass) klass.const_set(:Failure, Class.new(klass::Failure)) - klass.const_set(:Success, Class.new(klass::Success)) super end def call(*args, **kwargs, &block) command = build(*args, **kwargs) - __run_callbacks__(command) - command.call(&block) + result = catch(command) do + __run_callbacks__(command) + command.call(&block) + nil + end + return Resol::Success(result.data) unless result.nil? error_message = "No success! or fail! called in the #call method in #{command.class}" raise InvalidCommandImplementation, error_message - rescue self::Success => e - Resol::Success(e.data) rescue self::Failure => e Resol::Failure(e) end @@ -78,7 +67,7 @@ def fail!(code, data = nil) end def success!(data = nil) - raise self.class::Success.new(data) + throw(self, Result.new(data)) end end end diff --git a/lib/resol/version.rb b/lib/resol/version.rb index c87aa6e..258a4a8 100644 --- a/lib/resol/version.rb +++ b/lib/resol/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Resol - VERSION = "0.5.1" + VERSION = "0.6.0" end diff --git a/spec/interruptions_spec.rb b/spec/interruptions_spec.rb index 3bf3dc0..f8c5b12 100644 --- a/spec/interruptions_spec.rb +++ b/spec/interruptions_spec.rb @@ -1,28 +1,19 @@ # frozen_string_literal: true -RSpec.describe Resol::Service::Interruption do - describe Resol::Service::Success do - let(:object) { Resol::Service::Success.new(:some_data) } +RSpec.describe Resol::Service::Failure do + let(:object) { Resol::Service::Failure.new(:some_data, data) } - it { expect(object.inspect).to eq("Resol::Service::Success: :some_data") } + context "without data" do + let(:data) { nil } + + it { expect(object.inspect).to eq("Resol::Service::Failure: :some_data") } it { expect(object.message).to eq(":some_data") } end - describe Resol::Service::Failure do - let(:object) { Resol::Service::Failure.new(:some_data, data) } - - context "without data" do - let(:data) { nil } - - it { expect(object.inspect).to eq("Resol::Service::Failure: :some_data") } - it { expect(object.message).to eq(":some_data") } - end - - context "with data" do - let(:data) { :data } + context "with data" do + let(:data) { :data } - it { expect(object.inspect).to eq("Resol::Service::Failure: :some_data => :data") } - it { expect(object.message).to eq(":some_data => :data") } - end + it { expect(object.inspect).to eq("Resol::Service::Failure: :some_data => :data") } + it { expect(object.message).to eq(":some_data => :data") } end end diff --git a/spec/service_spec.rb b/spec/service_spec.rb index cf8c05b..8e07170 100644 --- a/spec/service_spec.rb +++ b/spec/service_spec.rb @@ -1,5 +1,23 @@ # frozen_string_literal: true +class DB + class << self + attr_accessor :rollbacked + + def transaction + self.rollbacked = false + + yield + "return_some_val" + # rubocop:disable Lint/RescueException + rescue Exception + # rubocop:enable Lint/RescueException + self.rollbacked = true + raise + end + end +end + class SuccessService < Resol::Service def call success!(:success_result) @@ -13,7 +31,9 @@ def call end class EmptyService < Resol::Service - def call; end + def call + "some_string" + end end class ServiceWithCallbacks < Resol::Service @@ -40,6 +60,27 @@ def set_other_value end end +class ServiceWithTransaction < Resol::Service + def call + DB.transaction { success! } + end +end + +class ServiceWithFailInTransaction < Resol::Service + def call + DB.transaction { fail!(:failed) } + end +end + +class HackyService < Resol::Service + param :count + + def call + success! unless count.zero? + HackyService.build(count + 1).call + end +end + RSpec.describe Resol::Service do it "returns a success result" do expect(SuccessService.call!).to eq(:success_result) @@ -64,4 +105,31 @@ def set_other_value expect(SubServiceWithCallbacks.call!).to eq("some_value_postfix") expect(ServiceWithCallbacks.call!).to eq("some_value") end + + it "doesn't rollback transaction" do + result = ServiceWithTransaction.call + expect(result.success?).to eq(true) + expect(result.value!).to eq(nil) + expect(DB.rollbacked).to eq(false) + end + + context "when service failed" do + it "rollbacks transaction" do + result = ServiceWithFailInTransaction.call + expect(result.failure?).to eq(true) + result.or do |error| + expect(error.code).to eq(:failed) + end + + expect(DB.rollbacked).to eq(true) + end + end + + context "when using instance #call inside other service" do + let(:expected_message) { /uncaught throw #