From 66c1c0f172529963fa67cea89b668b43db9da782 Mon Sep 17 00:00:00 2001 From: Marian13 Date: Wed, 4 Sep 2024 03:18:34 +0300 Subject: [PATCH] feat(middleware): introduce new middleware backend --- BACKLOG.md | 47 ++ .../support/middleware/stack_builder.rb | 2 + .../middleware/stack_builder/constants.rb | 7 +- .../stack_builder/entities/builders.rb | 1 + .../entities/builders/stateful.rb | 248 ++++++++ .../entities/builders/stateful/exceptions.rb | 31 + .../stack_builder/constants_spec.rb | 8 +- .../entities/builders/rack_spec.rb | 35 ++ .../entities/builders/ruby_middleware_spec.rb | 35 ++ .../builders/stateful/exceptions_spec.rb | 11 + .../entities/builders/stateful_spec.rb | 538 ++++++++++++++++++ .../support/middleware/stack_builder_spec.rb | 6 + 12 files changed, 967 insertions(+), 2 deletions(-) create mode 100644 lib/convenient_service/support/middleware/stack_builder/entities/builders/stateful.rb create mode 100644 lib/convenient_service/support/middleware/stack_builder/entities/builders/stateful/exceptions.rb create mode 100644 spec/lib/convenient_service/support/middleware/stack_builder/entities/builders/stateful/exceptions_spec.rb create mode 100644 spec/lib/convenient_service/support/middleware/stack_builder/entities/builders/stateful_spec.rb diff --git a/BACKLOG.md b/BACKLOG.md index 7919edaeee0..cfec0e0249b 100644 --- a/BACKLOG.md +++ b/BACKLOG.md @@ -735,6 +735,18 @@ To hide overriden [eql?](https://github.com/marian13/convenient_service/blob/v0. | - | - | - | - | | Medium | Moderate | TODO | performance, included-modules | +--- + +### Loop middleware backends in performance benchmarks + +| Priority | Complexity | Status | Tags | +| - | - | - | - | +| Medium | Moderate | TODO | middleware-backend | + +Consider to extract common benchmark setup. + +--- + ## Memory ### Consider to drop references to already calculated steps @@ -755,6 +767,41 @@ To hide overriden [eql?](https://github.com/marian13/convenient_service/blob/v0. | - | - | - | - | | Medium | High | TODO | compile, middleware-stack | +```ruby +module ConvenientService + module Support + module Middleware + class StackBuilder + module Constants + module Backends + ## + # @return [Symbol] + # + COMPILED = :compiled + end + end + end + end + end +end +``` + +--- + +### Unify middleware backends interfaces + +| Priority | Complexity | Status | Tags | +| - | - | - | - | +| Medium | Moderate | TODO | middleware-backend, interface | + +```ruby +def run(env, original) + # ... +end +``` + +--- + ### Cache unmodified configs. | Priority | Complexity | Status | Tags | diff --git a/lib/convenient_service/support/middleware/stack_builder.rb b/lib/convenient_service/support/middleware/stack_builder.rb index a6a7dec62e7..f41f05e43f7 100644 --- a/lib/convenient_service/support/middleware/stack_builder.rb +++ b/lib/convenient_service/support/middleware/stack_builder.rb @@ -27,6 +27,8 @@ def by(backend) Entities::Builders::RubyMiddleware when Constants::Backends::RACK Entities::Builders::Rack + when Constants::Backends::STATEFUL + Entities::Builders::Stateful else ::ConvenientService.raise Exceptions::NotSupportedBackend.new(backend: backend) end diff --git a/lib/convenient_service/support/middleware/stack_builder/constants.rb b/lib/convenient_service/support/middleware/stack_builder/constants.rb index d91e199c203..d6c35429f6e 100644 --- a/lib/convenient_service/support/middleware/stack_builder/constants.rb +++ b/lib/convenient_service/support/middleware/stack_builder/constants.rb @@ -16,10 +16,15 @@ module Backends # RACK = :rack + ## + # @return [Symbol] + # + STATEFUL = :stateful + ## # @return [Array] # - ALL = [RUBY_MIDDLEWARE, RACK] + ALL = [RUBY_MIDDLEWARE, RACK, STATEFUL] ## # @return [Symbol] diff --git a/lib/convenient_service/support/middleware/stack_builder/entities/builders.rb b/lib/convenient_service/support/middleware/stack_builder/entities/builders.rb index fef7dd707a1..ed023efe696 100644 --- a/lib/convenient_service/support/middleware/stack_builder/entities/builders.rb +++ b/lib/convenient_service/support/middleware/stack_builder/entities/builders.rb @@ -2,3 +2,4 @@ require_relative "builders/ruby_middleware" require_relative "builders/rack" +require_relative "builders/stateful" diff --git a/lib/convenient_service/support/middleware/stack_builder/entities/builders/stateful.rb b/lib/convenient_service/support/middleware/stack_builder/entities/builders/stateful.rb new file mode 100644 index 00000000000..c22c2bc1d3c --- /dev/null +++ b/lib/convenient_service/support/middleware/stack_builder/entities/builders/stateful.rb @@ -0,0 +1,248 @@ +# frozen_string_literal: true + +require_relative "stateful/exceptions" + +module ConvenientService + module Support + module Middleware + class StackBuilder + module Entities + module Builders + class Stateful + ## + # @!attribute [r] stack + # @return [Array<#call>] + # + attr_reader :stack + + ## + # @!attribute [r] name + # @return [String] + # + attr_reader :name + + ## + # @param kwargs [Hash{Symbol => Object}] + # @return [void] + # + def initialize(**kwargs) + @name = kwargs.fetch(:name) { "Stack" } + @stack = kwargs.fetch(:stack) { [] } + @index = -1 + end + + ## + # @return [Boolean] + # + def empty? + stack.empty? + end + + ## + # @param some_middleware [#call] + # @return [Boolean] + # + def has?(some_middleware) + stack.any? { |middleware| middleware == some_middleware } + end + + ## + # @return [Boolean] + # + def clear + stack.clear + + self + end + + ## + # @param env [Hash{Symbol => Object}] + # @return [Object] Can be any type. + # + # @internal + # NOTE: When stack is empty - `env` is returned. Just like `ruby-middleware` does. + # NOTE: Once middleware backends are unified, consider to create new object to ensure thread-safety. + # NOTE: Once middleware backends are unified, move `stack.empty?` and `index == stack.size - 1` to entrypoint method. + # + # TODO: Direct specs. + # + def call(env) + return env if stack.empty? + + self.index += 1 + + if index == stack.size - 1 + stack[index].call(env) + else + stack[index].new(self).call(env) + end + ensure + self.index = -1 + end + + ## + # @param middleware [#call] + # @return [ConvenientService::Support::Middleware::StackBuilder::Entities::Builders::Rack] + # + def unshift(middleware) + stack.unshift(middleware) + + self + end + + ## + # @return [ConvenientService::Support::Middleware::StackBuilder::Entities::Builders::Rack] + # + alias_method :prepend, :unshift + + ## + # @param middleware [#call] + # @return [ConvenientService::Support::Middleware::StackBuilder::Entities::Builders::Rack] + # + def use(middleware) + stack << middleware + + self + end + + ## + # @param index_or_middleware [Integer, #call] + # @param other_middleware [#call] + # @return [ConvenientService::Support::Middleware::StackBuilder::Entities::Builders::Rack] + # @raise [ConvenientService::Support::Middleware::StackBuilder::Entities::Builders::Rack::Exceptions::MissingMiddleware] + # + def insert(index_or_middleware, other_middleware) + index = cast_index(index_or_middleware) + + stack.insert(index, other_middleware) + + self + end + + ## + # @return [ConvenientService::Support::Middleware::StackBuilder::Entities::Builders::Rack] + # + alias_method :insert_before, :insert + + ## + # @param index_or_middleware [Integer, #call] + # @param other_middleware [#call] + # @return [ConvenientService::Support::Middleware::StackBuilder::Entities::Builders::Rack] + # @raise [ConvenientService::Support::Middleware::StackBuilder::Entities::Builders::Rack::Exceptions::MissingMiddleware] + # + def insert_after(index_or_middleware, other_middleware) + index = cast_index(index_or_middleware) + + stack.insert(index + 1, other_middleware) + + self + end + + ## + # @param other_middleware [#call] + # @return [ConvenientService::Support::Middleware::StackBuilder::Entities::Builders::Rack] + # + def insert_before_each(other_middleware) + @stack = stack.reduce([]) { |stack, middleware| stack.push(other_middleware, middleware) } + + self + end + + ## + # @param other_middleware [#call] + # @return [ConvenientService::Support::Middleware::StackBuilder::Entities::Builders::Rack] + # + def insert_after_each(other_middleware) + @stack = stack.reduce([]) { |stack, middleware| stack.push(middleware, other_middleware) } + + self + end + + ## + # @param index_or_middleware [Integer, #call] + # @param other_middleware [#call] + # @return [ConvenientService::Support::Middleware::StackBuilder::Entities::Builders::Rack] + # @raise [ConvenientService::Support::Middleware::StackBuilder::Entities::Builders::Rack::Exceptions::MissingMiddleware] + # + def replace(index_or_middleware, other_middleware) + index = cast_index(index_or_middleware) + + stack[index] = other_middleware + + self + end + + ## + # @param index_or_middleware [Integer, #call] + # @return [ConvenientService::Support::Middleware::StackBuilder::Entities::Builders::Rack] + # @raise [ConvenientService::Support::Middleware::StackBuilder::Entities::Builders::Rack::Exceptions::MissingMiddleware] + # + def delete(index_or_middleware) + index = cast_index(index_or_middleware) + + stack.delete_at(index) + + self + end + + ## + # @return [ConvenientService::Support::Middleware::StackBuilder::Entities::Builders::Rack] + # + alias_method :remove, :delete + + ## + # @param other [Object] Can be any type. + # @return [Boolean, nil] + # + def ==(other) + return unless other.instance_of?(self.class) + + return false if name != other.name + return false if stack != other.stack + + true + end + + ## + # @return [Array] + # + def to_a + stack + end + + ## + # @return [ConvenientService::Support::Middleware::StackBuilder::Entities::Builders::Rack] + # + def dup + self.class.new(name: name.dup, stack: stack.dup) + end + + private + + ## + # @!attribute [r] index + # @return [Integer] + # + attr_accessor :index + + ## + # @param index_or_middleware [Integer, #call] + # @return [Integer] + # @raise [ConvenientService::Support::Middleware::StackBuilder::Entities::Builders::Rack::Exceptions::MissingMiddleware] + # + def cast_index(index_or_middleware) + return index_or_middleware if index_or_middleware.instance_of?(Integer) + + index = stack.find_index { |middleware| middleware == index_or_middleware } + + ::ConvenientService.raise Exceptions::MissingMiddleware.new(middleware: index_or_middleware) unless index + + index + end + end + end + end + end + end + end +end diff --git a/lib/convenient_service/support/middleware/stack_builder/entities/builders/stateful/exceptions.rb b/lib/convenient_service/support/middleware/stack_builder/entities/builders/stateful/exceptions.rb new file mode 100644 index 00000000000..468d6d310bd --- /dev/null +++ b/lib/convenient_service/support/middleware/stack_builder/entities/builders/stateful/exceptions.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module ConvenientService + module Support + module Middleware + class StackBuilder + module Entities + module Builders + class Stateful + module Exceptions + class MissingMiddleware < ::ConvenientService::Exception + ## + # @param middleware [#call] + # @return [void] + # + def initialize_with_kwargs(middleware:) + message = <<~TEXT + Middleware `#{middleware.inspect}` is NOT found in the stack. + TEXT + + initialize(message) + end + end + end + end + end + end + end + end + end +end diff --git a/spec/lib/convenient_service/support/middleware/stack_builder/constants_spec.rb b/spec/lib/convenient_service/support/middleware/stack_builder/constants_spec.rb index 7eadacd20cd..87bd7eecf9c 100644 --- a/spec/lib/convenient_service/support/middleware/stack_builder/constants_spec.rb +++ b/spec/lib/convenient_service/support/middleware/stack_builder/constants_spec.rb @@ -19,9 +19,15 @@ end end + describe "::Backends::STATEFUL" do + it "returns `:stateful`" do + expect(described_class::Backends::STATEFUL).to eq(:stateful) + end + end + describe "::Backends::ALL" do it "returns `[:ruby_middleware, :rack]`" do - expect(described_class::Backends::ALL).to eq([described_class::Backends::RUBY_MIDDLEWARE, described_class::Backends::RACK]) + expect(described_class::Backends::ALL).to eq([described_class::Backends::RUBY_MIDDLEWARE, described_class::Backends::RACK, described_class::Backends::STATEFUL]) end end diff --git a/spec/lib/convenient_service/support/middleware/stack_builder/entities/builders/rack_spec.rb b/spec/lib/convenient_service/support/middleware/stack_builder/entities/builders/rack_spec.rb index 5841182a647..cbe41821cce 100644 --- a/spec/lib/convenient_service/support/middleware/stack_builder/entities/builders/rack_spec.rb +++ b/spec/lib/convenient_service/support/middleware/stack_builder/entities/builders/rack_spec.rb @@ -88,6 +88,41 @@ end end + ## + # TODO: Comprehensive specs. + # + describe "#call" do + let(:service) do + Class.new do + include ConvenientService::Standard::Config + + step :foo + step :bar + step :baz + + def foo + success + end + + def bar + success + end + + def baz + success + end + end + end + + before do + stub_const("ConvenientService::Support::Middleware::StackBuilder::Constants::Backends::DEFAULT", ConvenientService::Support::Middleware::StackBuilder::Constants::Backends::RACK) + end + + it "runs middleware stack" do + expect(service.result.success?).to eq(true) + end + end + describe "#unshift" do specify do expect { stack_builder.unshift(middleware) } diff --git a/spec/lib/convenient_service/support/middleware/stack_builder/entities/builders/ruby_middleware_spec.rb b/spec/lib/convenient_service/support/middleware/stack_builder/entities/builders/ruby_middleware_spec.rb index 6e5fe4be230..6035d33a24d 100644 --- a/spec/lib/convenient_service/support/middleware/stack_builder/entities/builders/ruby_middleware_spec.rb +++ b/spec/lib/convenient_service/support/middleware/stack_builder/entities/builders/ruby_middleware_spec.rb @@ -89,6 +89,41 @@ end end + ## + # TODO: Comprehensive specs. + # + describe "#call" do + let(:service) do + Class.new do + include ConvenientService::Standard::Config + + step :foo + step :bar + step :baz + + def foo + success + end + + def bar + success + end + + def baz + success + end + end + end + + before do + stub_const("ConvenientService::Support::Middleware::StackBuilder::Constants::Backends::DEFAULT", ConvenientService::Support::Middleware::StackBuilder::Constants::Backends::RUBY_MIDDLEWARE) + end + + it "runs middleware stack" do + expect(service.result.success?).to eq(true) + end + end + describe "#unshift" do specify do expect { stack_builder.unshift(middleware, *args, &block) } diff --git a/spec/lib/convenient_service/support/middleware/stack_builder/entities/builders/stateful/exceptions_spec.rb b/spec/lib/convenient_service/support/middleware/stack_builder/entities/builders/stateful/exceptions_spec.rb new file mode 100644 index 00000000000..d79d7504750 --- /dev/null +++ b/spec/lib/convenient_service/support/middleware/stack_builder/entities/builders/stateful/exceptions_spec.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require "spec_helper" + +require "convenient_service" + +RSpec.describe ConvenientService::Support::Middleware::StackBuilder::Entities::Builders::Stateful::Exceptions, type: :standard do + include ConvenientService::RSpec::PrimitiveMatchers::BeDescendantOf + + specify { expect(described_class::MissingMiddleware).to be_descendant_of(ConvenientService::Exception) } +end diff --git a/spec/lib/convenient_service/support/middleware/stack_builder/entities/builders/stateful_spec.rb b/spec/lib/convenient_service/support/middleware/stack_builder/entities/builders/stateful_spec.rb new file mode 100644 index 00000000000..1f9d90d3353 --- /dev/null +++ b/spec/lib/convenient_service/support/middleware/stack_builder/entities/builders/stateful_spec.rb @@ -0,0 +1,538 @@ +# frozen_string_literal: true + +require "spec_helper" + +require "convenient_service" + +# rubocop:disable RSpec/NestedGroups, RSpec/MultipleMemoizedHelpers +RSpec.describe ConvenientService::Support::Middleware::StackBuilder::Entities::Builders::Stateful, type: :standard do + include ConvenientService::RSpec::Helpers::IgnoringException + + include ConvenientService::RSpec::Matchers::DelegateTo + + let(:stack_builder) { described_class.new(name: name, stack: stack) } + + let(:stack) { [] } + let(:name) { "Stack" } + + let(:args) { [:foo] } + let(:block) { proc { :foo } } + + example_group "class methods" do + describe ".new" do + context "when `name` is NOT passed" do + it "defaults `\"Stack\"`" do + expect(stack_builder.name).to eq("Stack") + end + end + + context "when `stack` is NOT passed" do + it "defaults to empty array" do + expect(stack_builder.stack).to eq([]) + end + end + end + end + + example_group "instance methods" do + let(:middleware) { proc { :foo } } + let(:other_middleware) { proc { :bar } } + let(:index) { 0 } + + example_group "attributes" do + include ConvenientService::RSpec::PrimitiveMatchers::HaveAttrReader + + subject { stack_builder } + + it { is_expected.to have_attr_reader(:name) } + it { is_expected.to have_attr_reader(:stack) } + end + + describe "#has?" do + context "when stack does NOT have middleware" do + before do + stack_builder.clear + end + + it "returns `false`" do + expect(stack_builder.has?(middleware)).to eq(false) + end + end + + context "when stack does has middleware" do + before do + stack_builder.use(middleware) + end + + it "returns `true`" do + expect(stack_builder.has?(middleware)).to eq(true) + end + end + end + + describe "#empty?" do + specify do + expect { stack_builder.empty? } + .to delegate_to(stack, :empty?) + .without_arguments + .and_return_its_value + end + end + + describe "#clear" do + specify do + expect { stack_builder.clear } + .to delegate_to(stack, :clear) + .without_arguments + .and_return { stack_builder } + end + end + + ## + # TODO: Comprehensive specs. + # + describe "#call" do + let(:service) do + Class.new do + include ConvenientService::Standard::Config + + step :foo + step :bar + step :baz + + def foo + success + end + + def bar + success + end + + def baz + success + end + end + end + + before do + stub_const("ConvenientService::Support::Middleware::StackBuilder::Constants::Backends::DEFAULT", ConvenientService::Support::Middleware::StackBuilder::Constants::Backends::STATEFUL) + end + + it "runs middleware stack" do + expect(service.result.success?).to eq(true) + end + end + + describe "#unshift" do + specify do + expect { stack_builder.unshift(middleware) } + .to delegate_to(stack, :unshift) + .with_arguments(middleware) + .and_return { stack_builder } + end + end + + describe "#prepend" do + specify do + expect { stack_builder.prepend(middleware) } + .to delegate_to(stack, :unshift) + .with_arguments(middleware) + .and_return { stack_builder } + end + end + + describe "#use" do + specify do + expect { stack_builder.use(middleware) } + .to delegate_to(stack, :<<) + .with_arguments(middleware) + .and_return { stack_builder } + end + end + + describe "#insert" do + context "when `index_or_middleware` is integer" do + specify do + expect { stack_builder.insert(index, other_middleware) } + .to delegate_to(stack, :insert) + .with_arguments(index, other_middleware) + .and_return { stack_builder } + end + end + + context "when `index_or_middleware` is middleware" do + context "when that middleware is NOT found in stack" do + let(:exception_message) do + <<~TEXT + Middleware `#{middleware.inspect}` is NOT found in the stack. + TEXT + end + + before do + stack_builder.clear + end + + it "raises `ConvenientService::Support::Middleware::StackBuilder::Entities::Builders::Stateful::Exceptions::MissingMiddleware`" do + expect { stack_builder.insert(middleware, other_middleware) } + .to raise_error(ConvenientService::Support::Middleware::StackBuilder::Entities::Builders::Stateful::Exceptions::MissingMiddleware) + .with_message(exception_message) + end + + specify do + expect { ignoring_exception(ConvenientService::Support::Middleware::StackBuilder::Entities::Builders::Stateful::Exceptions::MissingMiddleware) { stack_builder.insert(middleware, other_middleware) } } + .to delegate_to(ConvenientService, :raise) + end + end + + context "when that middleware is found in stack" do + before do + stack_builder.use(middleware) + end + + specify do + expect { stack_builder.insert(middleware, other_middleware) } + .to delegate_to(stack, :insert) + .with_arguments(index, other_middleware) + .and_return { stack_builder } + end + end + end + end + + describe "#insert_before" do + context "when `index_or_middleware` is integer" do + specify do + expect { stack_builder.insert_before(index, other_middleware) } + .to delegate_to(stack, :insert) + .with_arguments(index, other_middleware) + .and_return { stack_builder } + end + end + + context "when `index_or_middleware` is middleware" do + context "when that middleware is NOT found in stack" do + let(:exception_message) do + <<~TEXT + Middleware `#{middleware.inspect}` is NOT found in the stack. + TEXT + end + + before do + stack_builder.clear + end + + it "raises `ConvenientService::Support::Middleware::StackBuilder::Entities::Builders::Stateful::Exceptions::MissingMiddleware`" do + expect { stack_builder.insert_before(middleware, other_middleware) } + .to raise_error(ConvenientService::Support::Middleware::StackBuilder::Entities::Builders::Stateful::Exceptions::MissingMiddleware) + .with_message(exception_message) + end + + specify do + expect { ignoring_exception(ConvenientService::Support::Middleware::StackBuilder::Entities::Builders::Stateful::Exceptions::MissingMiddleware) { stack_builder.insert_before(middleware, other_middleware) } } + .to delegate_to(ConvenientService, :raise) + end + end + + context "when that middleware is found in stack" do + before do + stack_builder.use(middleware) + end + + specify do + expect { stack_builder.insert_before(middleware, other_middleware) } + .to delegate_to(stack, :insert) + .with_arguments(index, other_middleware) + .and_return { stack_builder } + end + end + end + end + + describe "#insert_after" do + context "when `index_or_middleware` is integer" do + specify do + expect { stack_builder.insert_after(index, other_middleware) } + .to delegate_to(stack, :insert) + .with_arguments(index + 1, other_middleware) + .and_return { stack_builder } + end + end + + context "when `index_or_middleware` is middleware" do + context "when that middleware is NOT found in stack" do + let(:exception_message) do + <<~TEXT + Middleware `#{middleware.inspect}` is NOT found in the stack. + TEXT + end + + before do + stack_builder.clear + end + + it "raises `ConvenientService::Support::Middleware::StackBuilder::Entities::Builders::Stateful::Exceptions::MissingMiddleware`" do + expect { stack_builder.insert_after(middleware, other_middleware) } + .to raise_error(ConvenientService::Support::Middleware::StackBuilder::Entities::Builders::Stateful::Exceptions::MissingMiddleware) + .with_message(exception_message) + end + + specify do + expect { ignoring_exception(ConvenientService::Support::Middleware::StackBuilder::Entities::Builders::Stateful::Exceptions::MissingMiddleware) { stack_builder.insert_after(middleware, other_middleware) } } + .to delegate_to(ConvenientService, :raise) + end + end + + context "when that middleware is found in stack" do + before do + stack_builder.use(middleware) + end + + specify do + expect { stack_builder.insert_after(middleware, other_middleware) } + .to delegate_to(stack, :insert) + .with_arguments(index + 1, other_middleware) + .and_return { stack_builder } + end + end + end + end + + describe "#insert_before_each" do + before do + stack_builder.use(middleware) + stack_builder.use(middleware) + end + + it "returns stack builder" do + expect(stack_builder.insert_before_each(other_middleware)).to eq(stack_builder) + end + + it "adds other middleware before each middleware" do + stack_builder.insert_before_each(other_middleware) + + expect(stack_builder.to_a).to eq([other_middleware, middleware, other_middleware, middleware]) + end + end + + describe "#insert_after_each" do + before do + stack_builder.use(middleware) + stack_builder.use(middleware) + end + + it "returns stack builder" do + expect(stack_builder.insert_after_each(other_middleware)).to eq(stack_builder) + end + + it "adds other middleware after each middleware" do + stack_builder.insert_after_each(other_middleware) + + expect(stack_builder.to_a).to eq([middleware, other_middleware, middleware, other_middleware]) + end + end + + describe "#replace" do + context "when `index_or_middleware` is integer" do + specify do + expect { stack_builder.replace(index, other_middleware) } + .to delegate_to(stack, :[]=) + .with_arguments(index, other_middleware) + .and_return { stack_builder } + end + end + + context "when `index_or_middleware` is middleware" do + context "when that middleware is NOT found in stack" do + let(:exception_message) do + <<~TEXT + Middleware `#{middleware.inspect}` is NOT found in the stack. + TEXT + end + + before do + stack_builder.clear + end + + it "raises `ConvenientService::Support::Middleware::StackBuilder::Entities::Builders::Stateful::Exceptions::MissingMiddleware`" do + expect { stack_builder.replace(middleware, other_middleware) } + .to raise_error(ConvenientService::Support::Middleware::StackBuilder::Entities::Builders::Stateful::Exceptions::MissingMiddleware) + .with_message(exception_message) + end + + specify do + expect { ignoring_exception(ConvenientService::Support::Middleware::StackBuilder::Entities::Builders::Stateful::Exceptions::MissingMiddleware) { stack_builder.replace(middleware, other_middleware) } } + .to delegate_to(ConvenientService, :raise) + end + end + + context "when that middleware is found in stack" do + before do + stack_builder.use(middleware) + end + + specify do + expect { stack_builder.replace(middleware, other_middleware) } + .to delegate_to(stack, :[]=) + .with_arguments(index, other_middleware) + .and_return { stack_builder } + end + end + end + end + + describe "#delete" do + context "when stack does NOT have middleware" do + let(:exception_message) do + <<~TEXT + Middleware `#{middleware.inspect}` is NOT found in the stack. + TEXT + end + + before do + stack_builder.clear + end + + it "raises `ConvenientService::Support::Middleware::StackBuilder::Entities::Builders::Stateful::Exceptions::MissingMiddleware`" do + expect { stack_builder.delete(middleware) } + .to raise_error(ConvenientService::Support::Middleware::StackBuilder::Entities::Builders::Stateful::Exceptions::MissingMiddleware) + .with_message(exception_message) + end + + specify do + expect { ignoring_exception(ConvenientService::Support::Middleware::StackBuilder::Entities::Builders::Stateful::Exceptions::MissingMiddleware) { stack_builder.delete(middleware) } } + .to delegate_to(ConvenientService, :raise) + end + end + + context "when stack does has middleware" do + before do + stack_builder.use(middleware) + end + + it "removes that middleware from stack" do + stack_builder.delete(middleware) + + expect(stack_builder.empty?).to eq(true) + end + + it "returns stack builder" do + expect(stack_builder.delete(middleware)).to eq(stack_builder) + end + end + end + + describe "#remove" do + context "when stack does NOT have middleware" do + before do + stack_builder.clear + end + + let(:exception_message) do + <<~TEXT + Middleware `#{middleware.inspect}` is NOT found in the stack. + TEXT + end + + it "raises `ConvenientService::Support::Middleware::StackBuilder::Entities::Builders::Stateful::Exceptions::MissingMiddleware`" do + expect { stack_builder.remove(middleware) } + .to raise_error(ConvenientService::Support::Middleware::StackBuilder::Entities::Builders::Stateful::Exceptions::MissingMiddleware) + .with_message(exception_message) + end + + specify do + expect { ignoring_exception(ConvenientService::Support::Middleware::StackBuilder::Entities::Builders::Stateful::Exceptions::MissingMiddleware) { stack_builder.remove(middleware) } } + .to delegate_to(ConvenientService, :raise) + end + end + + context "when stack does has middleware" do + before do + stack_builder.use(middleware) + end + + it "removes that middleware from stack" do + stack_builder.remove(middleware) + + expect(stack_builder.empty?).to eq(true) + end + + it "returns stack builder" do + expect(stack_builder.remove(middleware)).to eq(stack_builder) + end + end + end + + describe "#to_a" do + before do + stack_builder.use(middleware) + stack_builder.use(other_middleware) + end + + it "returns stack" do + expect(stack_builder.to_a).to eq(stack) + end + end + + describe "#dup" do + ## + # NOTE: Unfreezes string since it is NOT possible to set spy on frozen objects. + # + let(:name) { +"Stack" } + + before do + ## + # NOTE: Create stack, before setting spies on `stack_class.new`, `name.dup`. + # + stack_builder + end + + specify do + expect { stack_builder.dup } + .to delegate_to(described_class, :new) + .with_arguments(name: name, stack: stack) + .and_return_its_value + end + + specify { expect { stack_builder.dup }.to delegate_to(name, :dup) } + + specify { expect { stack_builder.dup }.to delegate_to(stack, :dup) } + end + + example_group "comparison" do + describe "#==" do + context "when `other` has different class" do + let(:other) { 42 } + + it "returns `false`" do + expect(stack_builder == other).to be_nil + end + end + + context "when `other` has different `name`" do + let(:other) { described_class.new(name: "OtherStack", stack: stack) } + + it "returns `false`" do + expect(stack_builder == other).to eq(false) + end + end + + context "when `other` has different `plain_stack`" do + let(:other) { described_class.new(name: name, stack: [middleware]) } + + it "returns `false`" do + expect(stack_builder == other).to eq(false) + end + end + + context "when `other` has same attributes" do + let(:other) { described_class.new(name: name, stack: stack) } + + it "returns `true`" do + expect(stack_builder == other).to eq(true) + end + end + end + end + end +end +# rubocop:enable RSpec/NestedGroups, RSpec/MultipleMemoizedHelpers diff --git a/spec/lib/convenient_service/support/middleware/stack_builder_spec.rb b/spec/lib/convenient_service/support/middleware/stack_builder_spec.rb index c6d23a81b3f..53d2fa3a80d 100644 --- a/spec/lib/convenient_service/support/middleware/stack_builder_spec.rb +++ b/spec/lib/convenient_service/support/middleware/stack_builder_spec.rb @@ -67,6 +67,12 @@ expect(described_class.by(ConvenientService::Support::Middleware::StackBuilder::Constants::Backends::RACK)).to eq(ConvenientService::Support::Middleware::StackBuilder::Entities::Builders::Rack) end end + + context "when `backend` is `ConvenientService::Support::Middleware::StackBuilder::Constants::Backends::STATEFUL`" do + it "returns `ConvenientService::Support::Middleware::StackBuilder::Entities::Builders::Stateful`" do + expect(described_class.by(ConvenientService::Support::Middleware::StackBuilder::Constants::Backends::STATEFUL)).to eq(ConvenientService::Support::Middleware::StackBuilder::Entities::Builders::Stateful) + end + end end end end