diff --git a/.github/workflows/matrix.yml b/.github/workflows/matrix.yml index 397dff7..ddc9f85 100644 --- a/.github/workflows/matrix.yml +++ b/.github/workflows/matrix.yml @@ -12,23 +12,21 @@ permissions: jobs: test: - runs-on: ${{ matrix.os }}-latest + runs-on: ubuntu-latest + continue-on-error: ${{ matrix.experimental }} strategy: fail-fast: false matrix: - os: [ubuntu] - ruby: - # - ruby-2.4 - # - ruby-2.5 - - ruby-2.6 - - ruby-2.7 - - jruby-9.3.9.0 - continue-on-error: ${{ endsWith(matrix.ruby, 'head') }} + experimental: [false] + ruby-version: ["2.7", "3.0", "3.1", "3.2", "jruby-9.3"] + include: + - ruby-version: jruby-9.4 + experimental: true steps: - uses: actions/checkout@v3 - uses: ruby/setup-ruby@v1 with: - ruby-version: ${{ matrix.ruby }} + ruby-version: ${{ matrix.ruby-version }} bundler-cache: true - - run: bundle exec rake test - # - run: bundle exec rubocop \ No newline at end of file + - run: bundle exec rspec + - run: bundle exec rubocop \ No newline at end of file diff --git a/.gitignore b/.gitignore index d5a4400..69b9968 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ /tmp/ .ruby-version +.rspec_status diff --git a/.rubocop.yml b/.rubocop.yml index c4f5438..1fb8d19 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,19 +1,15 @@ require: rt_rubocop_defaults AllCops: - TargetRubyVersion: 2.4 - SuggestExtensions: false + TargetRubyVersion: 2.6 + # vendor directory is used by github actions and causes issues if not excluded here Exclude: - - vendor/**/*.rb - -Layout/LineLength: - Max: 99 - Exclude: - - test/* + - vendor/bundle/**/* Metrics/BlockLength: Exclude: - '*.gemspec' + - 'spec/*' Metrics/AbcSize: Exclude: diff --git a/CHANGELOG.md b/CHANGELOG.md index c5a30d1..0dfda5f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,46 +1,47 @@ -Changelog -=== +# Changelog -master ---- +All notable changes to this project will be documented in this file. +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [2.0.0] - 2023-01-17 +### Changed * switch CI to github actions * remove danger * remove codecov +* Support for ruby 3 +* The wrapper interface has been redesigned (breaking change, see [upgrade notes](./upgrade_notes.md)) -1.0.0 ---- +## [1.0.0] +### Changed * repackage 0.3.1 as 1.0.0 * setup circleci * drop ruby < 2.4 -0.3.1 ---- - +## [0.3.1] +### Fixed * fix after wrapper ordering bug [PR#6](https://github.com/andreaseger/receptacle/pull/6) -0.3.0 ---- - +## [0.3.0} +### Added * add danger * also support higher arity methods (== method with more than one argument) -0.2.0 ---- - +## [0.2.0] +### Changed * update documentation * enable ruby 2.1+ -0.1.1 ---- - +## [0.1.1] +## Added * add changelog, update copyright * add test helper method `ensure_method_delegators` to make rspec stubs/mocks work as expected -0.1.0 ---- - +## [0.1.0] +## Added * initial release - diff --git a/Gemfile.lock b/Gemfile.lock index 573c715..9b5fc73 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,45 +1,19 @@ PATH remote: . specs: - receptacle (1.0.0) + receptacle (2.0.0) GEM remote: https://rubygems.org/ specs: ast (2.4.2) coderay (1.1.3) + diff-lcs (1.5.0) docile (1.4.0) - ffi (1.15.5) ffi (1.15.5-java) - formatador (1.1.0) - guard (2.18.0) - formatador (>= 0.2.4) - listen (>= 2.7, < 4.0) - lumberjack (>= 1.0.12, < 2.0) - nenv (~> 0.1) - notiffany (~> 0.0) - pry (>= 0.13.0) - shellany (~> 0.0) - thor (>= 0.18.1) - guard-compat (1.2.1) - guard-minitest (2.4.6) - guard-compat (~> 1.2) - minitest (>= 3.0) - guard-rubocop (1.5.0) - guard (~> 2.0) - rubocop (< 2.0) json (2.6.2) json (2.6.2-java) - listen (3.7.1) - rb-fsevent (~> 0.10, >= 0.10.3) - rb-inotify (~> 0.9, >= 0.9.10) - lumberjack (1.2.8) method_source (1.0.0) - minitest (5.16.3) - nenv (0.3.0) - notiffany (0.1.3) - nenv (~> 0.1) - shellany (~> 0.0) parallel (1.22.1) parser (3.1.2.1) ast (~> 2.4.1) @@ -51,12 +25,24 @@ GEM method_source (~> 1.0) spoon (~> 0.0) rainbow (3.1.1) - rake (13.0.6) - rb-fsevent (0.11.2) - rb-inotify (0.10.1) - ffi (~> 1.0) + rake (10.5.0) regexp_parser (2.6.0) rexml (3.2.5) + rspec (3.12.0) + rspec-core (~> 3.12.0) + rspec-expectations (~> 3.12.0) + rspec-mocks (~> 3.12.0) + rspec-core (3.12.0) + rspec-support (~> 3.12.0) + rspec-expectations (3.12.0) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.12.0) + rspec-mocks (3.12.0) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.12.0) + rspec-support (3.12.0) + rspec_junit_formatter (0.6.0) + rspec-core (>= 2, < 4, != 2.12.0) rt_rubocop_defaults (2.4.0) rubocop (~> 1.25) rubocop (1.38.0) @@ -71,9 +57,10 @@ GEM unicode-display_width (>= 1.4.0, < 3.0) rubocop-ast (1.23.0) parser (>= 3.1.1.0) + rubocop-rspec (2.14.2) + rubocop (~> 1.33) rubocop_runner (2.2.1) ruby-progressbar (1.11.0) - shellany (0.0.1) simplecov (0.21.2) docile (~> 1.1) simplecov-html (~> 0.11) @@ -82,7 +69,6 @@ GEM simplecov_json_formatter (0.1.4) spoon (0.0.6) ffi - thor (1.2.1) unicode-display_width (2.3.0) PLATFORMS @@ -91,16 +77,16 @@ PLATFORMS DEPENDENCIES bundler (>= 1.13, < 3) - guard - guard-minitest - guard-rubocop - minitest (~> 5.0) pry - rake (~> 13.0) + rake (~> 10.0) receptacle! + rspec (~> 3.11) + rspec_junit_formatter rt_rubocop_defaults (~> 2.4) - rubocop_runner (~> 2.0) + rubocop (~> 1.37) + rubocop-rspec (~> 2.14) + rubocop_runner (~> 2.2) simplecov (~> 0.13) BUNDLED WITH - 2.2.19 + 2.3.26 diff --git a/Guardfile b/Guardfile deleted file mode 100644 index 9c0d5f3..0000000 --- a/Guardfile +++ /dev/null @@ -1,32 +0,0 @@ -# frozen_string_literal: true - -# A sample Guardfile -# More info at https://github.com/guard/guard#readme - -## Uncomment and set this to only include directories you want to watch -# directories %w(app lib config test spec features) \ -# .select{|d| Dir.exists?(d) ? d : UI.warning("Directory #{d} does not exist")} - -## Note: if you are using the `directories` clause above and you are not -## watching the project directory ('.'), then you will want to move -## the Guardfile to a watched dir and symlink it back, e.g. -# -# $ mkdir config -# $ mv Guardfile config/ -# $ ln -s config/Guardfile . -# -# and, you'll have to watch "config/Guardfile" instead of "Guardfile" -group :red_green_refactor, halt_on_fail: true do - guard :minitest do - # with Minitest::Unit - watch(%r{^test/(.*)/?test_(.*)\.rb$}) - # watch(%r{^lib/(.*/)?([^/]+)\.rb$}) { |m| "test/#{m[1]}test_#{m[2]}.rb" } - watch(%r{^lib/(.*/)?([^/]+)\.rb$}) { "test" } - watch(%r{^test/test_helper\.rb$}) { "test" } - watch(%r{^test/fixture\.rb$}) { "test" } - end - guard :rubocop, all_on_start: false, cli: ["--auto-correct"] do - watch(/.+\.rb$/) - watch(%r{(?:.+/)?\.rubocop\.yml%}) { |m| File.dirname(m[0]) } - end -end diff --git a/README.md b/README.md index 1352686..2718188 100644 --- a/README.md +++ b/README.md @@ -81,14 +81,14 @@ Optionally wrapper classes can be defined ```ruby module Wrapper class Validator - def before_find(id:) + def find(id:) raise ArgumentError if id.nil? - {id: id} + yield(id: id) end end class ModelMapper - def after_find(return_value, **_kwargs) - Model::User.new(return_value) + def find(id:) + Model::User.new(yield(id: id)) end end end @@ -121,14 +121,14 @@ module Repository module Wrapper class Validator - def before_find(id:) + def find(id:) raise ArgumentError if id.nil? - {id: id} + yield(id: id) end end class ModelMapper - def after_find(return_value, **_kwargs) - Model::User.new(return_value) + def find(id:) + Model::User.new(yield(id: id)) end end end @@ -224,47 +224,28 @@ applying them in the business logic by using wrappers. One or multiple wrappers sit logically between the repository and the strategies. Based on the repository configuration it knows when and in which -order they should be applied. Right now there is support for 2 1/2 types of -actions. - -1. a _before_ method action: This action is called before the final strategy - method is executed. It has access to the method parameter and can even modify - them. -2. a _after_ method action: This action is called after the strategy method was - executed and has access to the method parameters passed to the strategy - method and the return value. The return value could be modified here too. - -The extra 1/2 action type is born by the fact that if a single wrapper class -implements both an before and after action for the same method the same wrapper -instance is used to execute both. Although this doesn't cover the all use cases -an _around_ method action would but many which need state before and after the -data source is accessed are covered. +order they should be applied. #### Implementation Wrapper actions are implemented as plain ruby classes which provide instance -methods named like `before_` or `after_` where -`` is the repository/strategy method this action should be applied -to. +methods named like the method that the repository/strategy method this action should be applied to. ```ruby module Wrapper class Validator - def before_find(id:) + def find(id:) raise ArgumentError if id.nil? - {id: id} + yield(id: id) end end end ``` -This wrapper class would provide a before action for the `find` method. The -return value of this wrapper will be used as parameters for the strategy method -(or the next wrapper in line). Keyword arguments can simply be returned as hash. - -If multiple wrapper classes are defined the before wrapper actions are executed -in the order the wrapper classes are defined while the after actions are applied -in reverse order. +This wrapper class would execute on any `find` call. You can use it to execute code +before or after the next wrapper/strategy is called. Calling `yield` executes the next +wrapper in line or the strategy, if this is the last wrapper that is called. The return +value is passed down to the previous wrapper and in the end to the repository caller. ### Memory Strategy @@ -314,8 +295,8 @@ Some alternative have some interesting features nevertheless: This gem on the other hand makes absolutely no assumptions about your data source or general structure of your code. It can be simply plugged in between -your business logic and data source to abstract the two. Of course like the other -repository pattern implementations strategy details should be hidden from the +your business logic and data source to abstract the two. Of course, like the other +repository pattern implementations, strategy details should be hidden from the interface. The data source can essentially be anything. A SQL database, a no-SQL database, a JSON API or even a gem. Placing a gem behind a repository can be useful if you're not yet sure this is the correct or best possible gem, @@ -326,15 +307,11 @@ this by giving all the different http libraries a common interface). A module called `TestSupport` can be found [here](https://github.com/andreaseger/receptacle/blob/master/lib/receptacle/test_support.rb). -Right now it provides 2 helper methods `with_strategy` to easily toggle -temporarily to another strategy and `ensure_method_delegators` to solve issues -caused by Rspec when attempting to stub a repository method. Both methods and -how to use them is described in more detail in the inline documentation. +Right now it provides a helper method `with_strategy` to easily toggle temporarily to another strategy. How to use it is described in more detail in the inline documentation. ## Goals of this implementation - small core codebase -- minimal processing overhead - fast method dispatching - flexible - all kind of methods should possible to be mediated - basic but powerful callbacks/hooks/observer possibilities diff --git a/Rakefile b/Rakefile index 13464d6..ab7f7eb 100644 --- a/Rakefile +++ b/Rakefile @@ -12,7 +12,4 @@ end require "rubocop/rake_task" RuboCop::RakeTask.new -require "rubocop_runner/rake_task" -RubocopRunner::RakeTask.new - task default: :test diff --git a/examples/simple_repo.rb b/examples/simple_repo.rb index 39dc933..68ed3ee 100755 --- a/examples/simple_repo.rb +++ b/examples/simple_repo.rb @@ -4,7 +4,7 @@ require "bundler/inline" gemfile true do source "https://rubygems.org" - gem "receptacle", "~>0.3" + gem "receptacle", "~> 2" gem "mongo" end require "irb" diff --git a/lib/receptacle.rb b/lib/receptacle.rb index 2b490cd..3132221 100644 --- a/lib/receptacle.rb +++ b/lib/receptacle.rb @@ -1,14 +1,7 @@ # frozen_string_literal: true require "receptacle/version" -require "receptacle/interface_methods" -require "receptacle/method_delegation" +require "receptacle/repo" module Receptacle - module Repo - def self.included(base) - base.extend(InterfaceMethods) - base.extend(MethodDelegation) - end - end end diff --git a/lib/receptacle/errors.rb b/lib/receptacle/errors.rb index 56fcc51..271b07a 100644 --- a/lib/receptacle/errors.rb +++ b/lib/receptacle/errors.rb @@ -10,7 +10,5 @@ def initialize(repo:) super("Missing Configuration for repository: <#{repo}>") end end - - class ReservedMethodName < StandardError; end end end diff --git a/lib/receptacle/interface_methods.rb b/lib/receptacle/interface_methods.rb deleted file mode 100644 index ee64fb7..0000000 --- a/lib/receptacle/interface_methods.rb +++ /dev/null @@ -1,51 +0,0 @@ -# frozen_string_literal: true - -require "receptacle/registration" -require "receptacle/errors" - -module Receptacle - module InterfaceMethods - RESERVED_METHOD_NAMES = Set.new(%i[wrappers mediate strategy delegate_to_strategy]) - private_constant :RESERVED_METHOD_NAMES - - # registers a method_name for the to be mediated or forwarded to the configured strategy - # - # @param method_name [String] name of method to register - def mediate(method_name) - raise Errors::ReservedMethodName if RESERVED_METHOD_NAMES.include?(method_name) - - Registration.repositories[self].methods << method_name - end - alias delegate_to_strategy mediate - - # get or sets the strategy - # - # @note will set the strategy for this receptacle if passed in; will only - # return the current strategy if nil or no parameter passed include - # @param value [Class,nil] - # @return [Class] current configured strategy class - def strategy(value = nil) - if value - Registration.repositories[self].strategy = value - Registration.clear_method_cache(self) - else - Registration.repositories[self].strategy - end - end - - # get or sets the wrappers - # - # @note will set the wrappers for this receptacle if passed in; will only - # return the current wrappers if nil or no parameter passed include - # @param value [Class,Array(Class),nil] wrappers - # @return [Array(Class)] current configured wrappers - def wrappers(value = nil) - if value - Registration.repositories[self].wrappers = Array(value) - Registration.clear_method_cache(self) - else - Registration.repositories[self].wrappers - end - end - end -end diff --git a/lib/receptacle/method_cache.rb b/lib/receptacle/method_cache.rb deleted file mode 100644 index f8958dd..0000000 --- a/lib/receptacle/method_cache.rb +++ /dev/null @@ -1,43 +0,0 @@ -# frozen_string_literal: true - -module Receptacle - # Cache describing which strategy and wrappers need to be applied for this method - # @api private - class MethodCache - # @return [Symbol] name of the method this cache belongs to - attr_reader :method_name - # @return [Class] strategy class currently setup - attr_reader :strategy - # @return [Array(Class)] Array of wrapper classes which implement a wrapper for this method - attr_reader :wrappers - # @return [Symbol] name of the before action method - attr_reader :before_method_name - # @return [Symbol] name of the after action method - attr_reader :after_method_name - # @return [Integer] arity of strategy method according to https://ruby-doc.org/core-2.3.3/Method.html#method-i-arity - attr_reader :arity - - def initialize(method_name:, strategy:, wrappers:) - @strategy = strategy - @before_method_name = :"before_#{method_name}" - @after_method_name = :"after_#{method_name}" - @method_name = method_name.to_sym - before_wrappers = wrappers.select { |w| w.method_defined?(@before_method_name) } - after_wrappers = wrappers.select { |w| w.method_defined?(@after_method_name) } - @wrappers = wrappers & (before_wrappers | after_wrappers) - @skip_before_wrappers = before_wrappers.empty? - @skip_after_wrappers = after_wrappers.empty? - @arity = strategy.new.method(method_name).arity - end - - # @return [Boolean] true if no before wrappers need to be applied for this method - def skip_before_wrappers? - @skip_before_wrappers - end - - # @return [Boolean] true if no after wrappers need to be applied for this method - def skip_after_wrappers? - @skip_after_wrappers - end - end -end diff --git a/lib/receptacle/method_delegation.rb b/lib/receptacle/method_delegation.rb deleted file mode 100644 index c6b5c2e..0000000 --- a/lib/receptacle/method_delegation.rb +++ /dev/null @@ -1,141 +0,0 @@ -# frozen_string_literal: true - -require "receptacle/method_cache" -require "receptacle/registration" -require "receptacle/errors" - -module Receptacle - # module which enables a repository to mediate methods dynamically to wrappers and strategy - # @api private - module MethodDelegation - # dynamically build mediation method on first invocation if the method is registered - def method_missing(method_name, *arguments, &block) - if Registration.repositories[self].methods.include?(method_name) - public_send(__build_method(method_name), *arguments, &block) - else - super - end - end - - def respond_to_missing?(method_name, include_private = false) - Registration.repositories[self].methods.include?(method_name) || super - end - - # @param method_name [#to_sym] - # @return [void] - def __build_method(method_name) - method_cache = __build_method_call_cache(method_name) - if method_cache.wrappers.nil? || method_cache.wrappers.empty? - __define_shortcut_method(method_cache) - elsif method_cache.arity.abs > 1 - __define_full_method_high_arity(method_cache) - else - __define_full_method(method_cache) - end - end - - # build method cache for given method name - # @param method_name [#to_sym] - # @return [MethodCache] - def __build_method_call_cache(method_name) - config = Registration.repositories[self] - - raise Errors::NotConfigured.new(repo: self) if config.strategy.nil? - - MethodCache.new( - strategy: config.strategy, - wrappers: config.wrappers, - method_name: method_name - ) - end - - # build lightweight method to mediate method calls to strategy without wrappers - # @param method_cache [MethodCache] method_cache of the method to be build - # @return [void] - def __define_shortcut_method(method_cache) - define_singleton_method(method_cache.method_name) do |*args, &inner_block| - method_cache.strategy.new.public_send(method_cache.method_name, *args, &inner_block) - end - end - - # build method to mediate method calls of arity 1 to strategy with full wrapper support - # @param method_cache [MethodCache] method_cache of the method to be build - # @return [void] - def __define_full_method(method_cache) - define_singleton_method(method_cache.method_name) do |*args, &inner_block| - __run_wrappers(method_cache, *args) do |*call_args| - method_cache.strategy.new.public_send(method_cache.method_name, *call_args, &inner_block) - end - end - end - - # build method to mediate method calls of higher arity to strategy with full wrapper support - # @param method_cache [MethodCache] method_cache of the method to be build - # @return [void] - def __define_full_method_high_arity(method_cache) - define_singleton_method(method_cache.method_name) do |*args, &inner_block| - __run_wrappers(method_cache, args, true) do |*call_args| - method_cache.strategy.new.public_send(method_cache.method_name, *call_args, &inner_block) - end - end - end - - # runtime method to call before and after wrapper in correct order - # @param method_cache [MethodCache] method_cache for the current method - # @param input_args input parameter of the repository method call - # @param high_arity [Boolean] if are intended for a higher arity method - # @return strategy method return value after all wrappers where applied - def __run_wrappers(method_cache, input_args, high_arity = false) - wrappers = method_cache.wrappers.map(&:new) - args = - if method_cache.skip_before_wrappers? - input_args - else - __run_before_wrappers(wrappers, method_cache.before_method_name, input_args, high_arity) - end - ret = high_arity ? yield(*args) : yield(args) - return ret if method_cache.skip_after_wrappers? - - __run_after_wrappers(wrappers, method_cache.after_method_name, args, ret, high_arity) - end - - # runtime method to execute all before wrappers - # @param wrappers [Array] all wrapper instances to be executed - # @param method_name [Symbol] name of method to be executed on wrappers - # @param args input args of the repository method - # @param high_arity [Boolean] if are intended for a higher arity method - # @return processed method args by before wrappers - def __run_before_wrappers(wrappers, method_name, args, high_arity = false) - wrappers.each do |wrapper| - next unless wrapper.respond_to?(method_name) - - args = if high_arity - wrapper.public_send(method_name, *args) - else - wrapper.public_send(method_name, args) - end - end - args - end - - # runtime method to execute all after wrappers - # @param wrappers [Array] all wrapper instances to be executed - # @param method_name [Symbol] name of method to be executed on wrappers - # @param args input args to the strategy method (after processing in before wrappers) - # @param return_value return value of strategy method - # @param high_arity [Boolean] if are intended for a higher arity method - # @return processed return value by all after wrappers - def __run_after_wrappers(wrappers, method_name, args, return_value, high_arity = false) - wrappers.reverse_each do |wrapper| - next unless wrapper.respond_to?(method_name) - - return_value = if high_arity - wrapper.public_send(method_name, return_value, *args) - else - wrapper.public_send(method_name, return_value, args) - end - end - return_value - end - end -end diff --git a/lib/receptacle/registration.rb b/lib/receptacle/registration.rb deleted file mode 100644 index 0d1e6ef..0000000 --- a/lib/receptacle/registration.rb +++ /dev/null @@ -1,35 +0,0 @@ -# frozen_string_literal: true - -require "singleton" -require "set" -module Receptacle - # keeps global state of repositories, the defined strategy, set wrappers and methods to mediate - class Registration - include Singleton - Tuple = Struct.new(:strategy, :wrappers, :methods) # rubocop:disable Lint/StructNewOverride - - attr_reader :repositories - - def initialize - @repositories = Hash.new do |h, k| - h[k] = Tuple.new(nil, [], Set.new) - end - end - - def self.repositories - instance.repositories - end - - # {clear_method_cache} removes dynamically defined methods - # this is needed to make strategy and wrappers changes inside the codebase possible - def self.clear_method_cache(receptacle) - instance.repositories[receptacle].methods.each do |method_name| - begin - receptacle.singleton_class.send(:remove_method, method_name) - rescue NameError - nil - end - end - end - end -end diff --git a/lib/receptacle/repo.rb b/lib/receptacle/repo.rb new file mode 100644 index 0000000..7f77139 --- /dev/null +++ b/lib/receptacle/repo.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +require "receptacle/errors" + +module Receptacle + module Repo + module ClassMethods + def mediate(method_name) + define_singleton_method(method_name) do |*args, **kwargs| + raise Errors::NotConfigured.new(repo: self) unless @strategy + + with_wrappers(@wrappers.dup, method_name, *args, **kwargs) do |*sub_args, **sub_kwargs| + strategy.new.public_send(method_name, *sub_args, **sub_kwargs) + end + end + end + + def wrappers(wrappers) + @wrappers = wrappers + end + + def strategy(value = nil) + if value + @strategy = value + else + @strategy + end + end + + private + + def with_wrappers(wrappers, method_name, *args, **kwargs, &block) + next_wrapper = wrappers.shift + if next_wrapper&.method_defined?(method_name) + next_wrapper.new.public_send(method_name, *args, **kwargs) do |*sub_args, **sub_kwargs| + with_wrappers(wrappers, method_name, *sub_args, **sub_kwargs, &block) + end + elsif next_wrapper + with_wrappers(wrappers, method_name, *args, **kwargs, &block) + else + yield(*args, **kwargs) + end + end + end + + def self.included(base) + base.instance_variable_set(:@wrappers, []) + base.extend(ClassMethods) + end + end +end diff --git a/lib/receptacle/test_support.rb b/lib/receptacle/test_support.rb index 597f83a..cb81c19 100644 --- a/lib/receptacle/test_support.rb +++ b/lib/receptacle/test_support.rb @@ -47,21 +47,5 @@ def with_strategy(strategy, repo = described_class) ensure repo.strategy original_strategy end - - # ensure_method_delegators - # - # When using something like `allow(SomeRepo).to receive(:find)` (where find - # is a mediated method) before the method was called the first time Rspec - # would stub the method with the wrong arity of 0 when also using jruby. - # This seems to be caused by the lazily defined methods. Simply calling the - # following method in a before hook when method stubbing is need solves this - # issue. As it's a problem which only affects mocking libraries it's - # currently not planned to change this behavior. - # - def ensure_method_delegators(repo) - Receptacle::Registration.repositories[repo].methods.each do |method_name| - repo.__build_method(method_name) - end - end end end diff --git a/lib/receptacle/version.rb b/lib/receptacle/version.rb index 7d8b6e0..c1683b0 100644 --- a/lib/receptacle/version.rb +++ b/lib/receptacle/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Receptacle - VERSION = "1.0.0" + VERSION = "2.0.0" end diff --git a/performance/benchmark.rb b/performance/benchmark.rb deleted file mode 100644 index 4ec3e2b..0000000 --- a/performance/benchmark.rb +++ /dev/null @@ -1,45 +0,0 @@ -# frozen_string_literal: true - -require "bundler/inline" - -gemfile false do - source "https://rubygems.org" - gem "benchmark-ips" - gem "receptacle", path: "./.." -end - -require_relative "speed_receptacle" - -Speed.strategy(Speed::Strategy::One) -Speed.wrappers [Speed::Wrappers::W1, - Speed::Wrappers::W2, - Speed::Wrappers::W3, - Speed::Wrappers::W4, - Speed::Wrappers::W5, - Speed::Wrappers::W6] - -print "w/ wrappers" -Benchmark.ips do |x| - x.warmup = 10 if RUBY_ENGINE == "jruby" - x.report("a: 2x around, 1x before, 1x after") { Speed.a(1) } - x.report("b: 1x around, 1x before, 1x after") { Speed.b(1) } - x.report("c: 1x before, 1x after") { Speed.c(1) } - x.report("d: 1x after") { Speed.d(1) } - x.report("e: 1x before") { Speed.e(1) } - x.report("f: 1x around") { Speed.f(1) } - x.report("g: no wrappers") { Speed.g(1) } -end - -Speed.wrappers [] -print "method dispatching w/ wrappers" -Benchmark.ips do |x| - x.warmup = 10 if RUBY_ENGINE == "jruby" - x.report("via receptacle") { Speed.a(:foo) } - x.report("direct via public_send") { Speed::Strategy::One.new.public_send(:a, :foo) } - x.report("direct via method-method") do - m = Speed::Strategy::One.new.method(:a) - m.call(:foo) - end - x.report("direct method-call") { Speed::Strategy::One.new.a(:foo) } - x.compare! -end diff --git a/performance/profile.rb b/performance/profile.rb deleted file mode 100755 index 7813142..0000000 --- a/performance/profile.rb +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env ruby -# frozen_string_literal: true - -# run with --profile.api in JRUBY_OPTS -require "bundler/inline" -require "jruby/profiler" -PROFILE_NAME = "receptacle" - -gemfile false do - source "https://rubygems.org" - gem "receptacle", path: "./.." -end -require_relative "speed_receptacle" - -Speed.strategy(Speed::Strategy::One) -Speed.wrappers [Speed::Wrappers::W1, - Speed::Wrappers::W2, - Speed::Wrappers::W3, - Speed::Wrappers::W4, - Speed::Wrappers::W5, - Speed::Wrappers::W6] -Speed.a(1) -Speed.b(1) -Speed.c(1) -Speed.d(1) -Speed.e(1) -Speed.f(1) -Speed.g(1) - -GC.disable -profile_data = JRuby::Profiler.profile do - 100_000.times { Speed.a(1) } -end - -profile_printer = JRuby::Profiler::GraphProfilePrinter.new(profile_data) -profile_printer.printProfile(File.open("#{PROFILE_NAME}.graph.profile", "w+")) -profile_printer.printProfile($stdout) - -profile_printer = JRuby::Profiler::FlatProfilePrinter.new(profile_data) -profile_printer.printProfile(File.open("#{PROFILE_NAME}.flat.profile", "w+")) diff --git a/performance/speed_receptacle.rb b/performance/speed_receptacle.rb deleted file mode 100644 index 500e4dd..0000000 --- a/performance/speed_receptacle.rb +++ /dev/null @@ -1,106 +0,0 @@ -# frozen_string_literal: true - -require "receptacle" -module Speed - include Receptacle::Repo - mediate :a - mediate :b - mediate :c - mediate :d - mediate :e - mediate :f - mediate :g - module Strategy - class One - def a(arg) - arg - end - alias b a - alias c a - alias d a - alias e a - alias f a - alias g a - end - end - - module Wrappers - class W1 - def before_a(args) - args - end - - def after_a(return_values, _args) - return_values - end - - def before_f(args) - args - end - - def after_f(return_values, _args) - return_values - end - end - - class W2 - # :a - def before_a(args) - args - end - - def after_a(return_values, _args) - return_values - end - - # :b - def before_b(args) - args - end - - def after_b(return_values, _args) - return_values - end - end - - class W3 - def before_a(args) - args - end - - def before_c(args) - args - end - end - - class W4 - def after_a(return_values, _args) - return_values - end - - def after_d(return_value, _args) - return_value - end - end - - class W5 - def before_b(args) - args - end - - def after_c(return_value, _args) - return_value - end - end - - class W6 - def after_b(return_value, _args) - return_value - end - - def before_e(args) - args - end - end - end -end diff --git a/receptacle.gemspec b/receptacle.gemspec index 1b084e2..b273d1b 100644 --- a/receptacle.gemspec +++ b/receptacle.gemspec @@ -5,34 +5,39 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require "receptacle/version" Gem::Specification.new do |spec| - spec.name = "receptacle" - spec.version = Receptacle::VERSION - spec.authors = ["Andreas Eger"] - spec.email = ["dev@eger-andreas.de"] + spec.name = "receptacle" + spec.version = Receptacle::VERSION + spec.authors = ["Andreas Eger"] + spec.email = ["dev@eger-andreas.de"] - spec.summary = "repository pattern" - spec.description = "provides functionality for the repository or strategy pattern" - spec.homepage = "https://github.com/andreaseger/receptacle" - spec.license = "MIT" + spec.summary = "repository pattern" + spec.description = "provides functionality for the repository or strategy pattern" + spec.homepage = "https://github.com/andreaseger/receptacle" + spec.license = "MIT" - spec.required_ruby_version = "~> 2.4" + spec.required_ruby_version = if RUBY_ENGINE == "jruby" + ">= 2.6" + else + ">= 2.7" + end spec.files = `git ls-files -z`.split("\x0").reject do |f| f.match(%r{^(test|spec|features)/}) end - spec.bindir = "exe" - spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } + spec.bindir = "exe" + spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } spec.require_paths = ["lib"] spec.add_development_dependency "bundler", ">= 1.13", "< 3" - spec.add_development_dependency "guard" - spec.add_development_dependency "guard-minitest" - spec.add_development_dependency "guard-rubocop" - spec.add_development_dependency "minitest", "~> 5.0" spec.add_development_dependency "pry" - spec.add_development_dependency "rake", "~> 13.0" + spec.add_development_dependency "rake", "~> 10.0" + spec.add_development_dependency "rspec", "~> 3.11" + spec.add_development_dependency "rspec_junit_formatter" spec.add_development_dependency "rt_rubocop_defaults", "~> 2.4" - spec.add_development_dependency "rubocop_runner", "~> 2.0" + spec.add_development_dependency "rubocop", "~> 1.37" + spec.add_development_dependency "rubocop-rspec", "~> 2.14" + spec.add_development_dependency "rubocop_runner", "~> 2.2" spec.add_development_dependency "simplecov", "~> 0.13" + spec.metadata["rubygems_mfa_required"] = "true" end diff --git a/spec/receptacle_spec.rb b/spec/receptacle_spec.rb new file mode 100644 index 0000000..8ef0571 --- /dev/null +++ b/spec/receptacle_spec.rb @@ -0,0 +1,221 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe Receptacle::Repo do + context "with a strategy" do + let(:repository) do + test_wrappers = wrappers + test_strategy = strategy + + Class.new do + include Receptacle::Repo + + mediate :find + mediate :delete + + wrappers(test_wrappers) + strategy(test_strategy) + end + end + + let(:strategy) do + Class.new do + def find(id, unscoped: true) # rubocop:disable Lint/UnusedMethodArgument + id + 50 + end + + def delete(id:) # rubocop:disable Lint/UnusedMethodArgument + "deleted" + end + end + end + + let(:strategy_instance) { strategy.new } + + before do + allow(strategy_instance).to receive(:find) + .and_call_original + allow(strategy_instance).to receive(:delete) + .and_call_original + allow(strategy).to receive(:new) + .and_return(strategy_instance) + end + + context "without wrappers being defined" do + let(:repository) do + test_strategy = strategy + + Class.new do + include Receptacle::Repo + + mediate :find + mediate :delete + + strategy(test_strategy) + end + end + + it "calls the method on the strategy" do + expect(repository.find(10, unscoped: false)).to eql(60) + + expect(strategy_instance).to have_received(:find) + .with(10, unscoped: false) + end + end + + context "with two wrappers that modify input and output of the find method" do + let(:wrappers) do + [ + add5_wrapper, + add10_flip_unscoped_and_multiply_result_wrapper + ] + end + let(:add5_wrapper) do + Class.new do + def find(id, unscoped: true) + yield(id + 5, unscoped: unscoped) + end + end + end + let(:add10_flip_unscoped_and_multiply_result_wrapper) do + Class.new do + def find(id, unscoped: true) + yield(id + 10, unscoped: !unscoped) * 100 + end + end + end + let(:add5_wrapper_instance) { add5_wrapper.new } + let(:add10_flip_unscoped_and_multiply_result_wrapper_instance) do + add10_flip_unscoped_and_multiply_result_wrapper.new + end + + before do + allow(add5_wrapper).to receive(:new) + .and_return(add5_wrapper_instance) + allow(add5_wrapper_instance).to receive(:find) + .and_call_original + + allow(add10_flip_unscoped_and_multiply_result_wrapper).to receive(:new) + .and_return(add10_flip_unscoped_and_multiply_result_wrapper_instance) + allow(add10_flip_unscoped_and_multiply_result_wrapper_instance).to receive(:find) + .and_call_original + end + + describe ".find" do + it "calls the first wrapper with the input arguments" do + repository.find(5, unscoped: true) + + expect(add5_wrapper_instance).to have_received(:find) + .with(5, unscoped: true) + end + + it "calls the second wrapper with the output of the first wrapper" do + repository.find(5) + + expect(add10_flip_unscoped_and_multiply_result_wrapper_instance).to have_received(:find) + .with(5 + 5, unscoped: true) + end + + it "calls the strategy with the output of the second wrapper" do + repository.find(5) + + expect(strategy_instance).to have_received(:find) + .with(5 + 5 + 10, unscoped: false) + end + + it "returns correct value for find" do + expect(repository.find(5, unscoped: true)).to eq((5 + 5 + 10 + 50) * 100) + end + end + + describe ".delete" do + context "when no wrapper implements the delete function" do + it "calls the strategy only" do + expect(repository.delete(id: 10)).to eql("deleted") + end + end + + context "when only the first wrapper implements the delete function" do + let(:add5_wrapper) do + Class.new do + def delete(id:) + yield(id: id + 5) + end + end + end + + before do + allow(add5_wrapper_instance).to receive(:delete) + .and_call_original + end + + it "calls the first wrapper with the input arguments" do + repository.delete(id: 5) + + expect(add5_wrapper_instance).to have_received(:delete) + .with(id: 5) + end + + it "calls the strategy with the output of the first wrapper" do + repository.delete(id: 5) + + expect(strategy_instance).to have_received(:delete) + .with(id: 5 + 5) + end + + it "returns correct value for delete" do + expect(repository.delete(id: 5)).to eq("deleted") + end + end + + context "when only the second wrapper implements the delete function" do + let(:add10_flip_unscoped_and_multiply_result_wrapper) do + Class.new do + def delete(id:) + yield(id: id + 10) + end + end + end + + before do + allow(add10_flip_unscoped_and_multiply_result_wrapper_instance).to receive(:delete) + .and_call_original + end + + it "calls the second wrapper with the input arguments" do + repository.delete(id: 5) + + expect(add10_flip_unscoped_and_multiply_result_wrapper_instance).to have_received(:delete) + .with(id: 5) + end + + it "calls the strategy with the output of the second wrapper" do + repository.delete(id: 5) + + expect(strategy_instance).to have_received(:delete) + .with(id: 5 + 10) + end + + it "returns correct value for delete" do + expect(repository.delete(id: 5)).to eq("deleted") + end + end + end + end + end + + context "with a repository that has no strategy" do + let(:repository) do + Class.new do + include Receptacle::Repo + + mediate :find + end + end + + it "throws an error when trying to use the repository" do + expect { repository.find }.to raise_error(Receptacle::Errors::NotConfigured) + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..7738eea --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require "bundler" + +$LOAD_PATH.unshift(Bundler.root) + +require "receptacle" +require "pry" + +[ + "spec/support/**/*.rb" +].each do |pattern| + Dir[File.join(pattern)].sort.each { |file| require file } +end + +RSpec.configure do |config| + config.example_status_persistence_file_path = ".rspec_status" + config.disable_monkey_patching! + config.expose_dsl_globally = true + config.filter_run focus: true + config.run_all_when_everything_filtered = true + + config.expect_with :rspec do |c| + c.syntax = :expect + end + + config.order = "random" + Kernel.srand config.seed +end diff --git a/test/count_down_latch.rb b/spec/support/count_down_latch.rb similarity index 100% rename from test/count_down_latch.rb rename to spec/support/count_down_latch.rb diff --git a/spec/thread_safety_spec.rb b/spec/thread_safety_spec.rb new file mode 100644 index 0000000..900c2a2 --- /dev/null +++ b/spec/thread_safety_spec.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe Receptacle::Repo do + let(:repository) do + Class.new do + include Receptacle::Repo + + mediate :foo + + wrapper_class = Class.new do + def foo(number) + @number = number + @number += 1 + yield(@number) + end + end + + strategy_class = Class.new do + def foo(number) + @number = number + @number *= 10 + end + end + + wrappers([wrapper_class]) + strategy(strategy_class) + end + end + + it "is threasafe" do + latch = CountDownLatch.new(1) + + t1 = Thread.new do + latch.wait + expect(repository.foo(5)).to eql(60) + end + t2 = Thread.new do + latch.wait + expect(repository.foo(6)).to eql(70) + end + t3 = Thread.new do + latch.wait + expect(repository.foo(9)).to eql(100) + end + + latch.count_down + t1.join + t2.join + t3.join + end +end diff --git a/test/fixture.rb b/test/fixture.rb deleted file mode 100644 index 2c892b1..0000000 --- a/test/fixture.rb +++ /dev/null @@ -1,183 +0,0 @@ -# frozen_string_literal: true - -require "receptacle" - -module Fixtures - def self.callstack - Thread.current[:receptacle_test_callstack] ||= [] - end - - module Test - include Receptacle::Repo - mediate :a - mediate :b - mediate :c - mediate :d - mediate :e - end - - module Strategy - class One - def a(number) - Fixtures.callstack.push([self.class, __method__, number]) - number - end - - def b(array) - Fixtures.callstack.push([self.class, __method__, array]) - array.reduce(:+) - end - - def c(string:) - Fixtures.callstack.push([self.class, __method__, string]) - string.upcase - end - - def d(context:) - Fixtures.callstack.push([self.class, __method__, context]) - yield(context) - end - - def e(first, second) - Fixtures.callstack.push([self.class, __method__, [first, second]]) - first + second - end - end - - class Two < One - def a(number) - Fixtures.callstack.push([self.class, __method__, number]) - number * 2 - end - end - end - - module Wrapper - class BeforeAfterA - def before_a(number) - Fixtures.callstack.push([self.class, __method__, number]) - number + 5 - end - - def after_a(return_value, number) - Fixtures.callstack.push([self.class, __method__, number, return_value]) - return_value + 5 - end - end - - class BeforeAfterAandB - # :a - def before_a(number) - Fixtures.callstack.push([self.class, __method__, number]) - number + 10 - end - - def after_a(return_value, number) - Fixtures.callstack.push([self.class, __method__, number, return_value]) - return_value + 10 - end - - # :b - def before_b(array) - Fixtures.callstack.push([self.class, __method__, array]) - array | [66] - end - - def after_b(return_value, array) - Fixtures.callstack.push([self.class, __method__, array, return_value]) - return_value + 10 - end - end - - class BeforeAfterWithStateC - # :c - def before_c(string:) - Fixtures.callstack.push([self.class, __method__, string]) - @state = string.length - { string: "#{string}_wat" } - end - - # :c - def after_c(return_value, string:) - Fixtures.callstack.push([self.class, __method__, string, return_value]) - return_value + @state.to_s - end - end - - class BeforeAandC - # :a - def before_a(number) - Fixtures.callstack.push([self.class, __method__, number]) - number + 10 - end - - # :c - def before_c(string:) - Fixtures.callstack.push([self.class, __method__, string]) - { string: "#{string}_foo" } - end - end - - class BeforeAll - # :a - def before_a(number) - Fixtures.callstack.push([self.class, __method__, number]) - number + 50 - end - - # :a - def before_b(array) - Fixtures.callstack.push([self.class, __method__, array]) - array | [33] - end - - # :c - def before_c(string:) - Fixtures.callstack.push([self.class, __method__, string]) - { string: "#{string}_bar" } - end - - # :d - def before_d(context:) - Fixtures.callstack.push([self.class, __method__, context]) - { context: "#{context}_bar" } - end - - def before_e(first, second) - Fixtures.callstack.push([self.class, __method__, [first, second]]) - [first + 5, second + 5] - end - end - - class AfterAll - # :a - def after_a(return_value, number) - Fixtures.callstack.push([self.class, __method__, number, return_value]) - return_value + 100 - end - - # :a - def after_b(return_value, array) - Fixtures.callstack.push([self.class, __method__, array, return_value]) - return_value + 100 - end - - # :c - def after_c(return_value, string:) - Fixtures.callstack.push([self.class, __method__, string, return_value]) - "#{return_value}_foobar" - end - - # :d - def after_d(return_value, context:) - Fixtures.callstack.push([self.class, __method__, context, return_value]) - "#{return_value}_foobar" - end - - def after_e(return_value, first, second) - Fixtures.callstack.push([self.class, __method__, [first, second], return_value]) - return_value + 300 - end - end - end -end diff --git a/test/test_helper.rb b/test/test_helper.rb deleted file mode 100644 index 860fc1b..0000000 --- a/test/test_helper.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -unless RUBY_PLATFORM == "java" - require "simplecov" - SimpleCov.start do - add_filter "/test/" - end -end - -$LOAD_PATH.unshift File.expand_path("../lib", __dir__) -require "receptacle" - -require "minitest/autorun" diff --git a/test/test_receptacle.rb b/test/test_receptacle.rb deleted file mode 100644 index 8b1d429..0000000 --- a/test/test_receptacle.rb +++ /dev/null @@ -1,315 +0,0 @@ -# frozen_string_literal: true - -require "test_helper" -require "count_down_latch" -require "fixture" - -class ReceptacleTest < Minitest::Test - def callstack - Thread.current[:receptacle_test_callstack] - end - - def receptacle - Fixtures::Test - end - - def clear_callstack - Thread.current[:receptacle_test_callstack] = [] - end - - def setup - Receptacle::Registration.repositories[receptacle].strategy = nil - Receptacle::Registration.repositories[receptacle].wrappers = [] - Receptacle::Registration.clear_method_cache(receptacle) - clear_callstack - end - - def test_provide_dsl - mod = Module.new - mod.include(Receptacle::Repo) - assert mod.respond_to?(:strategy) - assert mod.respond_to?(:mediate) - assert mod.respond_to?(:wrappers) - end - - def test_define_methods_via_mediate - mod = Module.new - mod.include(Receptacle::Repo) - refute mod.respond_to?(:some_method) - mod.mediate(:some_method) - assert mod.respond_to?(:some_method) - end - - def test_define_methods_via_delegate_to_strategy - mod = Module.new - mod.include(Receptacle::Repo) - refute mod.respond_to?(:some_method) - mod.delegate_to_strategy(:some_method) - assert mod.respond_to?(:some_method) - end - - def test_reserved_method_names - mod = Module.new - mod.include(Receptacle::Repo) - - # error for reserved method names - %i[wrappers strategy mediate delegate_to_strategy].each do |method_name| - assert_raises(Receptacle::Errors::ReservedMethodName) { mod.mediate(method_name) } - end - end - - def test_store_strategy_setup - strategy = Fixtures::Strategy::One - receptacle.strategy strategy - assert_equal strategy, Receptacle::Registration.repositories[receptacle].strategy - assert_equal strategy, receptacle.strategy - - strategy = Fixtures::Strategy::Two - receptacle.strategy strategy - assert_equal strategy, Receptacle::Registration.repositories[receptacle].strategy - end - - def test_store_wrappers_setup - assert_equal [], receptacle.wrappers - assert_equal [], Receptacle::Registration.repositories[receptacle].wrappers - - wrapper = Minitest::Mock.new - receptacle.wrappers wrapper - assert_equal [wrapper], Receptacle::Registration.repositories[receptacle].wrappers - assert_equal [wrapper], receptacle.wrappers - - receptacle.wrappers [wrapper] - assert_equal [wrapper], Receptacle::Registration.repositories[receptacle].wrappers - assert_equal [wrapper], receptacle.wrappers - - receptacle.wrappers [] - assert_equal [], receptacle.wrappers - assert_equal [], Receptacle::Registration.repositories[receptacle].wrappers - end - - def test_missing_strategy_setup - assert_raises(Receptacle::Errors::NotConfigured) do - receptacle.a(23) - end - end - - def test_method_mediation - receptacle.strategy Fixtures::Strategy::One - assert_equal 34, receptacle.a(34) - assert_raises(NoMethodError) { receptacle.foo } - end - - def test_argument_passing_no_wrapper - receptacle.strategy Fixtures::Strategy::One - receptacle.a(123) - assert_equal [[Fixtures::Strategy::One, :a, 123]], callstack - - clear_callstack - receptacle.wrappers [Fixtures::Wrapper::BeforeAandC] - assert_equal 244, receptacle.a(234) - assert_equal [ - [Fixtures::Wrapper::BeforeAandC, :before_a, 234], - [Fixtures::Strategy::One, :a, 244] - ], callstack - end - - def test_argument_passing_array - receptacle.strategy Fixtures::Strategy::One - assert_equal 17, receptacle.b([5, 12]) - assert_equal [[Fixtures::Strategy::One, :b, [5, 12]]], callstack - - clear_callstack - receptacle.wrappers [Fixtures::Wrapper::BeforeAll] - assert_equal 50, receptacle.b([3, 14]) - assert_equal [ - [Fixtures::Wrapper::BeforeAll, :before_b, [3, 14]], - [Fixtures::Strategy::One, :b, [3, 14, 33]] - ], callstack - end - - def test_argument_passing_kwargs - receptacle.strategy Fixtures::Strategy::One - assert_equal "ARGUMENT_PASSING", receptacle.c(string: "argument_passing") - assert_equal [ - [Fixtures::Strategy::One, :c, "argument_passing"] - ], callstack - clear_callstack - - receptacle.wrappers [Fixtures::Wrapper::BeforeAandC] - assert_equal "TEST_FOO", receptacle.c(string: "test") - assert_equal [ - [Fixtures::Wrapper::BeforeAandC, :before_c, "test"], - [Fixtures::Strategy::One, :c, "test_foo"] - ], callstack - end - - def test_argument_passing_block - receptacle.strategy Fixtures::Strategy::One - assert_equal "test_in_block", receptacle.d(context: "test") { |c| "#{c}_in_block" } - assert_equal [ - [Fixtures::Strategy::One, :d, "test"] - ], callstack - clear_callstack - - receptacle.wrappers [Fixtures::Wrapper::BeforeAll] - assert_equal "test_bar_in_block", receptacle.d(context: "test") { |c| "#{c}_in_block" } - assert_equal [ - [Fixtures::Wrapper::BeforeAll, :before_d, "test"], - [Fixtures::Strategy::One, :d, "test_bar"] - ], callstack - end - - def test_argument_passing_multiple_arguments - receptacle.strategy Fixtures::Strategy::One - assert_equal 46, receptacle.e(1, 45) - assert_equal [ - [Fixtures::Strategy::One, :e, [1, 45]] - ], callstack - clear_callstack - - receptacle.wrappers [Fixtures::Wrapper::BeforeAll, Fixtures::Wrapper::AfterAll] - assert_equal 348, receptacle.e(5, 33) - assert_equal [ - [Fixtures::Wrapper::BeforeAll, :before_e, [5, 33]], - [Fixtures::Strategy::One, :e, [10, 38]], - [Fixtures::Wrapper::AfterAll, :after_e, [10, 38], 48] - ], callstack - end - - def test_after_wrapper - receptacle.strategy Fixtures::Strategy::Two - receptacle.wrappers [Fixtures::Wrapper::AfterAll] - assert_equal 110, receptacle.a(5) - assert_equal [ - [Fixtures::Strategy::Two, :a, 5], - [Fixtures::Wrapper::AfterAll, :after_a, 5, 10] - ], callstack - clear_callstack - - assert_equal 112, receptacle.b([5, 7]) - assert_equal [ - [Fixtures::Strategy::Two, :b, [5, 7]], - [Fixtures::Wrapper::AfterAll, :after_b, [5, 7], 12] - ], callstack - clear_callstack - - assert_equal "WRAPPER_foobar", receptacle.c(string: "wrapper") - assert_equal [ - [Fixtures::Strategy::Two, :c, "wrapper"], - [Fixtures::Wrapper::AfterAll, :after_c, "wrapper", "WRAPPER"] - ], callstack - clear_callstack - - assert_equal "test_in_block_foobar", receptacle.d(context: "test") { |c| "#{c}_in_block" } - assert_equal [ - [Fixtures::Strategy::Two, :d, "test"], - [Fixtures::Wrapper::AfterAll, :after_d, "test", "test_in_block"] - ], callstack - end - - def test_before_and_after_wrapper - receptacle.strategy Fixtures::Strategy::Two - receptacle.wrappers [Fixtures::Wrapper::BeforeAfterA] - assert_equal 123, receptacle.a(54) - assert_equal [ - [Fixtures::Wrapper::BeforeAfterA, :before_a, 54], - [Fixtures::Strategy::Two, :a, 59], - [Fixtures::Wrapper::BeforeAfterA, :after_a, 59, 118] - ], callstack - end - - def test_multiple_wrapper - receptacle.strategy Fixtures::Strategy::Two - receptacle.wrappers [Fixtures::Wrapper::BeforeAfterA, Fixtures::Wrapper::BeforeAfterAandB] - assert_equal 53, receptacle.a(4) - assert_equal [ - [Fixtures::Wrapper::BeforeAfterA, :before_a, 4], - [Fixtures::Wrapper::BeforeAfterAandB, :before_a, 9], - [Fixtures::Strategy::Two, :a, 19], - [Fixtures::Wrapper::BeforeAfterAandB, :after_a, 19, 38], - [Fixtures::Wrapper::BeforeAfterA, :after_a, 19, 48] - ], callstack - clear_callstack - - assert_equal 92, receptacle.b([1, 6, 9]) - assert_equal [ - [Fixtures::Wrapper::BeforeAfterAandB, :before_b, [1, 6, 9]], - [Fixtures::Strategy::Two, :b, [1, 6, 9, 66]], - [Fixtures::Wrapper::BeforeAfterAandB, :after_b, [1, 6, 9, 66], 82] - ], callstack - end - - def test_call_order_after_setup_change - receptacle.strategy Fixtures::Strategy::One - receptacle.wrappers [Fixtures::Wrapper::BeforeAll, Fixtures::Wrapper::AfterAll] - assert_equal "BLA_BAR_foobar", receptacle.c(string: "bla") - assert_equal [ - [Fixtures::Wrapper::BeforeAll, :before_c, "bla"], - [Fixtures::Strategy::One, :c, "bla_bar"], - [Fixtures::Wrapper::AfterAll, :after_c, "bla_bar", "BLA_BAR"] - ], callstack - clear_callstack - - receptacle.strategy Fixtures::Strategy::Two - receptacle.wrappers [Fixtures::Wrapper::BeforeAll, Fixtures::Wrapper::BeforeAandC] - assert_equal "BLA_BAR_FOO", receptacle.c(string: "bla") - assert_equal [ - [Fixtures::Wrapper::BeforeAll, :before_c, "bla"], - [Fixtures::Wrapper::BeforeAandC, :before_c, "bla_bar"], - [Fixtures::Strategy::Two, :c, "bla_bar_foo"] - ], callstack - end - - def test_sharing_state_between_before_after_inside_wrapper - receptacle.strategy Fixtures::Strategy::One - receptacle.wrappers Fixtures::Wrapper::BeforeAfterWithStateC - - assert_equal "WOHOO_WAT5", receptacle.c(string: "wohoo") - assert_equal [ - [Fixtures::Wrapper::BeforeAfterWithStateC, :before_c, "wohoo"], - [Fixtures::Strategy::One, :c, "wohoo_wat"], - [Fixtures::Wrapper::BeforeAfterWithStateC, :after_c, "wohoo_wat", "WOHOO_WAT"] - ], callstack - - assert_equal "NEW_STATE_WAT9", receptacle.c(string: "new_state") - end - - def test_after_wrapper_order - receptacle.strategy Fixtures::Strategy::One - receptacle.wrappers [Fixtures::Wrapper::AfterAll, Fixtures::Wrapper::BeforeAfterA] - - assert_equal 187, receptacle.a(77) - assert_equal [ - [Fixtures::Wrapper::BeforeAfterA, :before_a, 77], - [Fixtures::Strategy::One, :a, 82], - [Fixtures::Wrapper::BeforeAfterA, :after_a, 82, 82], - [Fixtures::Wrapper::AfterAll, :after_a, 82, 87] - ], callstack - end - - def test_thread_safety_for_mediated_method_calls - # This config is global for the whole application - receptacle.strategy Fixtures::Strategy::One - receptacle.wrappers Fixtures::Wrapper::BeforeAfterWithStateC - - latch = CountDownLatch.new(1) - - t1 = Thread.new do - latch.wait - assert_equal "T1_WAT2", receptacle.c(string: "t1") - end - t2 = Thread.new do - latch.wait - assert_equal "T2_WAT2", receptacle.c(string: "t2") - end - t3 = Thread.new do - latch.wait - assert_equal "T3_WAT2", receptacle.c(string: "t3") - end - - latch.count_down - t1.join - t2.join - t3.join - end -end diff --git a/upgrade_notes.md b/upgrade_notes.md new file mode 100644 index 0000000..01578b9 --- /dev/null +++ b/upgrade_notes.md @@ -0,0 +1,45 @@ +# Upgrade notes + +## Upgrade from 1.0.0 to 2.0.0 +The test helper `ensure_method_delegators` has been removed. If you used it, just remove its usage. + +If you do not use wrappers, there is nothing further you need to do to upgrade. + +The wrapper interface changed. With version 1.0 you had to implement methods prefixed with `before_` and `after_`. The return value of before-hooks where used as arguments to the next wrapper, while the return value of after-hooks was passed down to the previous wrapper. E.g: + +```ruby +module Wrapper + class Validator + def before_find(id:) + raise ArgumentError if id.nil? + {id: id} + end + end + + class ModelMapper + def after_find(return_value, **_kwargs) + Model::User.new(return_value) + end + end +end +``` + +In version 2.0 there are no before- and after-methods anymore. Instead you provide one method that wraps around the next wrapper or the strategy. You need to name the wrapper methods exactly as they are named in your strategy and you call `yield` to call the next wrapper. E.g: + +```ruby +module Wrapper + class Validator + def find(id:) + raise ArgumentError if id.nil? + yield(id: id) + end + end + + class ModelMapper + def find(id:) + return_value = yield(id: id) + Model::User.new(return_value) + end + end +end +``` \ No newline at end of file