From 692f4f7e55566adcdc45930e08b4ce9cf6563d68 Mon Sep 17 00:00:00 2001 From: Anton Chuchkalov Date: Wed, 22 Mar 2017 12:33:11 +0300 Subject: [PATCH 01/27] add rubocop --- .rubocop.yml | 51 +++++++++++++++++++++++++++++++++++++++ Rakefile | 2 ++ interactor.gemspec | 6 ++--- lib/interactor.rb | 6 ++--- lib/interactor/context.rb | 2 +- lib/interactor/hooks.rb | 4 +-- spec/integration_spec.rb | 33 ++++++++++++++++--------- spec/support/lint.rb | 4 ++- 8 files changed, 85 insertions(+), 23 deletions(-) create mode 100644 .rubocop.yml diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..41c66a3 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,51 @@ +Style/StringLiterals: + EnforcedStyle: double_quotes + +Style/FrozenStringLiteralComment: + Enabled: false + +# Allow some style changes in specs +Metrics/ModuleLength: + Exclude: + - spec/**/* +Metrics/BlockLength: + Exclude: + - spec/**/* +Style/BlockDelimiters: + Exclude: + - spec/**/* +Style/RescueModifier: + Exclude: + - spec/**/* +Metrics/MethodLength: + Exclude: + - spec/interactor/hooks_spec.rb +Style/IndentArray: + Exclude: + - spec/integration_spec.rb + - spec/interactor/hooks_spec.rb + +# Allow nice tree-like comments in specs +Style/AsciiComments: + Exclude: + - spec/integration_spec.rb + +# Here inconsistent indentation helps to understand +# tree nature of callbacks. +Style/AlignArray: + Exclude: + - spec/integration_spec.rb + +# Rubocop suggests using $INPUT_RECORD_SEPARATOR +# variable from stdlib 'English' module over $/. +# This module appeared in Ruby 2.0, so we could use it +# only if we drop 1.9.3 support +Style/SpecialGlobalVars: + Exclude: + - interactor.gemspec + +# This could be removed if throws are used instead of +# raising Failure in #fail! +Lint/HandleExceptions: + Exclude: + - lib/interactor.rb diff --git a/Rakefile b/Rakefile index a5401c3..0db584f 100644 --- a/Rakefile +++ b/Rakefile @@ -1,7 +1,9 @@ require "bundler/gem_tasks" require "rspec/core/rake_task" +require "rubocop/rake_task" require "standard/rake" RSpec::Core::RakeTask.new(:spec) +RuboCop::RakeTask.new(:rubocop) task default: [:spec, :standard] diff --git a/interactor.gemspec b/interactor.gemspec index 1b7f830..24d2e0c 100644 --- a/interactor.gemspec +++ b/interactor.gemspec @@ -11,9 +11,9 @@ Gem::Specification.new do |spec| spec.homepage = "https://github.com/collectiveidea/interactor" spec.license = "MIT" - spec.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR) spec.test_files = spec.files.grep(/^spec/) - spec.add_development_dependency "bundler" - spec.add_development_dependency "rake" + spec.add_development_dependency "bundler", "~> 1.7" + spec.add_development_dependency "rake", "~> 10.3" + spec.add_development_dependency "rubocop", "~> 0.47.1" end diff --git a/lib/interactor.rb b/lib/interactor.rb index 2423630..d645ab3 100644 --- a/lib/interactor.rb +++ b/lib/interactor.rb @@ -153,14 +153,12 @@ def run! # each interactor class. # # Returns nothing. - def call - end + def call; end # Public: Reverse prior invocation of an Interactor instance. Any interactor # class that requires undoing upon downstream failure is expected to overwrite # the "rollback" instance method. # # Returns nothing. - def rollback - end + def rollback; end end diff --git a/lib/interactor/context.rb b/lib/interactor/context.rb index a477f1d..b3e893b 100644 --- a/lib/interactor/context.rb +++ b/lib/interactor/context.rb @@ -53,7 +53,7 @@ class Context < OpenStruct # # Returns the Interactor::Context. def self.build(context = {}) - self === context ? context : new(context) + context.is_a?(Context) ? context : new(context) end # Public: Whether the Interactor::Context is successful. By default, a new diff --git a/lib/interactor/hooks.rb b/lib/interactor/hooks.rb index d82e7a5..7ae4d00 100644 --- a/lib/interactor/hooks.rb +++ b/lib/interactor/hooks.rb @@ -219,9 +219,9 @@ def with_hooks # # Returns nothing. def run_around_hooks(&block) - self.class.around_hooks.reverse.inject(block) { |chain, hook| + self.class.around_hooks.reverse.inject(block) do |chain, hook| proc { run_hook(hook, chain) } - }.call + end.call end # Internal: Run before hooks. diff --git a/spec/integration_spec.rb b/spec/integration_spec.rb index ca083f3..161a2b3 100644 --- a/spec/integration_spec.rb +++ b/spec/integration_spec.rb @@ -25,7 +25,8 @@ def build_organizer(options = {}, &block) # └─ interactor5 let(:organizer) { - build_organizer(organize: [organizer2, interactor3, organizer4, interactor5]) do + interactors = [organizer2, interactor3, organizer4, interactor5] + build_organizer(organize: interactors) do around do |interactor| context.steps << :around_before interactor.call @@ -315,7 +316,8 @@ def rollback context "when an around hook fails early" do let(:organizer) { - build_organizer(organize: [organizer2, interactor3, organizer4, interactor5]) do + interactors = [organizer2, interactor3, organizer4, interactor5] + build_organizer(organize: interactors) do around do |interactor| context.fail! context.steps << :around_before @@ -345,8 +347,9 @@ def rollback context "when an around hook errors early" do let(:organizer) { - build_organizer(organize: [organizer2, interactor3, organizer4, interactor5]) do - around do |interactor| + interactors = [organizer2, interactor3, organizer4, interactor5] + build_organizer(organize: interactors) do + around do |_interactor| raise "foo" end @@ -382,7 +385,8 @@ def rollback context "when a before hook fails" do let(:organizer) { - build_organizer(organize: [organizer2, interactor3, organizer4, interactor5]) do + interactors = [organizer2, interactor3, organizer4, interactor5] + build_organizer(organize: interactors) do around do |interactor| context.steps << :around_before interactor.call @@ -413,7 +417,8 @@ def rollback context "when a before hook errors" do let(:organizer) { - build_organizer(organize: [organizer2, interactor3, organizer4, interactor5]) do + interactors = [organizer2, interactor3, organizer4, interactor5] + build_organizer(organize: interactors) do around do |interactor| context.steps << :around_before interactor.call @@ -453,7 +458,8 @@ def rollback context "when an after hook fails" do let(:organizer) { - build_organizer(organize: [organizer2, interactor3, organizer4, interactor5]) do + interactors = [organizer2, interactor3, organizer4, interactor5] + build_organizer(organize: interactors) do around do |interactor| context.steps << :around_before interactor.call @@ -504,7 +510,8 @@ def rollback context "when an after hook errors" do let(:organizer) { - build_organizer(organize: [organizer2, interactor3, organizer4, interactor5]) do + interactors = [organizer2, interactor3, organizer4, interactor5] + build_organizer(organize: interactors) do around do |interactor| context.steps << :around_before interactor.call @@ -564,7 +571,8 @@ def rollback context "when an around hook fails late" do let(:organizer) { - build_organizer(organize: [organizer2, interactor3, organizer4, interactor5]) do + interactors = [organizer2, interactor3, organizer4, interactor5] + build_organizer(organize: interactors) do around do |interactor| context.steps << :around_before interactor.call @@ -616,7 +624,8 @@ def rollback context "when an around hook errors late" do let(:organizer) { - build_organizer(organize: [organizer2, interactor3, organizer4, interactor5]) do + interactors = [organizer2, interactor3, organizer4, interactor5] + build_organizer(organize: interactors) do around do |interactor| context.steps << :around_before interactor.call @@ -725,7 +734,7 @@ def rollback context "when a nested around hook errors early" do let(:interactor3) { build_interactor do - around do |interactor| + around do |_interactor| raise "foo" end @@ -1255,7 +1264,7 @@ def rollback context "when a deeply nested around hook errors early" do let(:interactor4b) { build_interactor do - around do |interactor| + around do |_interactor| raise "foo" end diff --git a/spec/support/lint.rb b/spec/support/lint.rb index bfbc97c..b8fd9d3 100644 --- a/spec/support/lint.rb +++ b/spec/support/lint.rb @@ -43,7 +43,9 @@ let(:context) { double(:context) } it "initializes a context" do - expect(Interactor::Context).to receive(:build).once.with(foo: "bar") { context } + expect( + Interactor::Context + ).to receive(:build).once.with(foo: "bar") { context } instance = interactor.new(foo: "bar") From a18c417b176a6f818cb8eef48e9a84c7e2046749 Mon Sep 17 00:00:00 2001 From: Anton Chuchkalov Date: Wed, 22 Mar 2017 13:19:04 +0300 Subject: [PATCH 02/27] drop support for Rubies < 2.1.0 --- .rubocop.yml | 11 +++-------- interactor.gemspec | 2 ++ 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 41c66a3..de66a53 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -36,16 +36,11 @@ Style/AlignArray: Exclude: - spec/integration_spec.rb -# Rubocop suggests using $INPUT_RECORD_SEPARATOR -# variable from stdlib 'English' module over $/. -# This module appeared in Ruby 2.0, so we could use it -# only if we drop 1.9.3 support -Style/SpecialGlobalVars: - Exclude: - - interactor.gemspec - # This could be removed if throws are used instead of # raising Failure in #fail! Lint/HandleExceptions: Exclude: - lib/interactor.rb + +AllCops: + TargetRubyVersion: 2.1 diff --git a/interactor.gemspec b/interactor.gemspec index 24d2e0c..246513b 100644 --- a/interactor.gemspec +++ b/interactor.gemspec @@ -13,6 +13,8 @@ Gem::Specification.new do |spec| spec.test_files = spec.files.grep(/^spec/) + spec.required_ruby_version = ">= 2.1" + spec.add_development_dependency "bundler", "~> 1.7" spec.add_development_dependency "rake", "~> 10.3" spec.add_development_dependency "rubocop", "~> 0.47.1" From 6e3ea2a52bb6b3225866132d905704eca78ba299 Mon Sep 17 00:00:00 2001 From: Steve Richert Date: Tue, 21 Apr 2015 19:51:34 -0400 Subject: [PATCH 03/27] Build the "v4" branch on Travis --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index d4f2098..59c991d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,7 @@ before_install: branches: only: - master - - v3 + - v4 env: global: - secure: | # CODECLIMATE_REPO_TOKEN From 6c1c586b41cb515fe0b88846846a7a0ba3dda805 Mon Sep 17 00:00:00 2001 From: Steve Richert Date: Tue, 21 Apr 2015 19:53:18 -0400 Subject: [PATCH 04/27] Drop support for Ruby 1.9 and add support for Ruby 2.2 Welcome to the future! --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f8c44b0..af67534 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 4.0.0 / Unreleased + +* [ENHANCEMENT] Add support for Ruby 2.2 +* [ENHANCEMENT] Drop support for Ruby 1.9 + ## 3.1.2 / 2019-12-29 * [BUGFIX] Fix Context#fail! on Ruby 2.7 From 8dc9bb3fd66156853c3a6bb231ea6898a0820a17 Mon Sep 17 00:00:00 2001 From: Steve Richert Date: Tue, 21 Apr 2015 19:54:50 -0400 Subject: [PATCH 05/27] Reconfigure Travis to use container-based builds and to cache the bundle --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 59c991d..53b71f5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,6 +6,7 @@ branches: only: - master - v4 +cache: bundler env: global: - secure: | # CODECLIMATE_REPO_TOKEN @@ -35,3 +36,4 @@ rvm: - "2.7" - ruby-head script: bundle exec rake +sudo: false From e5d48f7c89694ef7d2fda5271f2807f702539148 Mon Sep 17 00:00:00 2001 From: Steve Richert Date: Tue, 21 Apr 2015 19:56:22 -0400 Subject: [PATCH 06/27] Update gem dependency version requirements --- interactor.gemspec | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/interactor.gemspec b/interactor.gemspec index 246513b..d288e8a 100644 --- a/interactor.gemspec +++ b/interactor.gemspec @@ -15,7 +15,7 @@ Gem::Specification.new do |spec| spec.required_ruby_version = ">= 2.1" - spec.add_development_dependency "bundler", "~> 1.7" - spec.add_development_dependency "rake", "~> 10.3" + spec.add_development_dependency "bundler", "~> 1.9" + spec.add_development_dependency "rake", "~> 10.4" spec.add_development_dependency "rubocop", "~> 0.47.1" end From 90c9575a85f38cd8034df93237ed8af1e9abb7e2 Mon Sep 17 00:00:00 2001 From: Steve Richert Date: Tue, 21 Apr 2015 19:58:15 -0400 Subject: [PATCH 07/27] Target version 4.0.0 --- README.md | 2 +- interactor.gemspec | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 6de00f2..3aefca1 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Add Interactor to your Gemfile and `bundle install`. ```ruby -gem "interactor", "~> 3.0" +gem "interactor", "~> 4.0" ``` ## What is an Interactor? diff --git a/interactor.gemspec b/interactor.gemspec index d288e8a..e0d7c38 100644 --- a/interactor.gemspec +++ b/interactor.gemspec @@ -1,8 +1,8 @@ require "English" Gem::Specification.new do |spec| - spec.name = "interactor" - spec.version = "3.1.2" + spec.name = "interactor" + spec.version = "4.0.0" spec.author = "Collective Idea" spec.email = "info@collectiveidea.com" From e6b19d932ff8c2c1aafa09e259ba6b11f69baa62 Mon Sep 17 00:00:00 2001 From: Steve Richert Date: Tue, 21 Apr 2015 20:01:14 -0400 Subject: [PATCH 08/27] Use RSpec's verbose "documentation" formatting by default [ci skip] --- .rspec | 1 + 1 file changed, 1 insertion(+) diff --git a/.rspec b/.rspec index b18b4cc..ce9d5ff 100644 --- a/.rspec +++ b/.rspec @@ -1,3 +1,4 @@ --color +--format documentation --order random --require spec_helper From 0ffa4e949b168e2ef11276421f84de643f15c2cb Mon Sep 17 00:00:00 2001 From: Anton Chuchkalov Date: Thu, 23 Mar 2017 11:12:52 +0300 Subject: [PATCH 09/27] implemented requested changes (#124) --- .rubocop.yml | 3 ++ Gemfile | 1 + interactor.gemspec | 3 +- lib/interactor.rb | 6 ++-- spec/integration_spec.rb | 64 +++++++++++++++++++++++++++++----------- spec/support/lint.rb | 5 ++-- 6 files changed, 58 insertions(+), 24 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index de66a53..717ebdf 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -42,5 +42,8 @@ Lint/HandleExceptions: Exclude: - lib/interactor.rb +Style/EmptyMethod: + Enabled: false + AllCops: TargetRubyVersion: 2.1 diff --git a/Gemfile b/Gemfile index 5e868c1..28a7a47 100644 --- a/Gemfile +++ b/Gemfile @@ -7,4 +7,5 @@ gem "standard" group :test do gem "codeclimate-test-reporter", require: false gem "rspec", "~> 3.7" + gem "rubocop", "~> 0.47.1" end diff --git a/interactor.gemspec b/interactor.gemspec index e0d7c38..65afccb 100644 --- a/interactor.gemspec +++ b/interactor.gemspec @@ -13,9 +13,8 @@ Gem::Specification.new do |spec| spec.test_files = spec.files.grep(/^spec/) - spec.required_ruby_version = ">= 2.1" + spec.required_ruby_version = ">= 2.0" spec.add_development_dependency "bundler", "~> 1.9" spec.add_development_dependency "rake", "~> 10.4" - spec.add_development_dependency "rubocop", "~> 0.47.1" end diff --git a/lib/interactor.rb b/lib/interactor.rb index d645ab3..2423630 100644 --- a/lib/interactor.rb +++ b/lib/interactor.rb @@ -153,12 +153,14 @@ def run! # each interactor class. # # Returns nothing. - def call; end + def call + end # Public: Reverse prior invocation of an Interactor instance. Any interactor # class that requires undoing upon downstream failure is expected to overwrite # the "rollback" instance method. # # Returns nothing. - def rollback; end + def rollback + end end diff --git a/spec/integration_spec.rb b/spec/integration_spec.rb index 161a2b3..1f16945 100644 --- a/spec/integration_spec.rb +++ b/spec/integration_spec.rb @@ -2,6 +2,11 @@ def build_interactor(&block) interactor = Class.new.send(:include, Interactor) interactor.class_eval(&block) if block + interactor.class_eval do + def unexpected_error! + raise "foo" + end + end interactor end @@ -9,6 +14,11 @@ def build_organizer(options = {}, &block) organizer = Class.new.send(:include, Interactor::Organizer) organizer.organize(options[:organize]) if options[:organize] organizer.class_eval(&block) if block + organizer.class_eval do + def unexpected_error! + raise "foo" + end + end organizer end @@ -349,8 +359,11 @@ def rollback let(:organizer) { interactors = [organizer2, interactor3, organizer4, interactor5] build_organizer(organize: interactors) do - around do |_interactor| - raise "foo" + around do |interactor| + unexpected_error! + context.steps << :around_before + interactor.call + context.steps << :around_after end before do @@ -426,7 +439,8 @@ def rollback end before do - raise "foo" + unexpected_error! + context.steps << :before end after do @@ -523,7 +537,8 @@ def rollback end after do - raise "foo" + unexpected_error! + context.steps << :after end end } @@ -629,7 +644,8 @@ def rollback around do |interactor| context.steps << :around_before interactor.call - raise "foo" + unexpected_error! + context.steps << :around_after end before do @@ -734,8 +750,11 @@ def rollback context "when a nested around hook errors early" do let(:interactor3) { build_interactor do - around do |_interactor| - raise "foo" + around do |interactor| + unexpected_error! + context.steps << :around_before3 + interactor.call + context.steps << :around_after3 end before do @@ -843,7 +862,8 @@ def rollback end before do - raise "foo" + unexpected_error! + context.steps << :before3 end after do @@ -956,7 +976,8 @@ def rollback end def call - raise "foo" + unexpected_error! + context.steps << :call3 end def rollback @@ -1058,7 +1079,8 @@ def rollback end after do - raise "foo" + unexpected_error! + context.steps << :after3 end def call @@ -1157,7 +1179,8 @@ def rollback around do |interactor| context.steps << :around_before3 interactor.call - raise "foo" + unexpected_error! + context.steps << :around_after3 end before do @@ -1264,8 +1287,11 @@ def rollback context "when a deeply nested around hook errors early" do let(:interactor4b) { build_interactor do - around do |_interactor| - raise "foo" + around do |interactor| + unexpected_error! + context.steps << :around_before4b + interactor.call + context.steps << :around_after4b end before do @@ -1383,7 +1409,8 @@ def rollback end before do - raise "foo" + unexpected_error! + context.steps << :before4b end after do @@ -1506,7 +1533,8 @@ def rollback end def call - raise "foo" + unexpected_error! + context.steps << :call4b end def rollback @@ -1618,7 +1646,8 @@ def rollback end after do - raise "foo" + unexpected_error! + context.steps << :after4b end def call @@ -1727,7 +1756,8 @@ def rollback around do |interactor| context.steps << :around_before4b interactor.call - raise "foo" + unexpected_error! + context.steps << :around_after4b end before do diff --git a/spec/support/lint.rb b/spec/support/lint.rb index b8fd9d3..73346e5 100644 --- a/spec/support/lint.rb +++ b/spec/support/lint.rb @@ -43,9 +43,8 @@ let(:context) { double(:context) } it "initializes a context" do - expect( - Interactor::Context - ).to receive(:build).once.with(foo: "bar") { context } + expect(Interactor::Context).to receive(:build) + .once.with(foo: "bar") { context } instance = interactor.new(foo: "bar") From 633555b95d269adeaeb4052ec79e50905242f1ca Mon Sep 17 00:00:00 2001 From: Anton Chuchkalov Date: Thu, 23 Mar 2017 16:14:02 +0300 Subject: [PATCH 10/27] configure rubocop to Ruby 2.0 syntax --- .rubocop.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.rubocop.yml b/.rubocop.yml index 717ebdf..57771ec 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -46,4 +46,4 @@ Style/EmptyMethod: Enabled: false AllCops: - TargetRubyVersion: 2.1 + TargetRubyVersion: 2.0 From 5a6f451261916c8c596408b85d76fd5aa0bacc8f Mon Sep 17 00:00:00 2001 From: Anton Chuchkalov Date: Thu, 23 Mar 2017 16:28:52 +0300 Subject: [PATCH 11/27] return Ruby 2.0 to Travis config --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 53b71f5..aaf4e05 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,6 +20,7 @@ matrix: - rvm: "2.1" - rvm: "2.2" - rvm: ruby-head + - rvm: "2.0" notifications: webhooks: on_start: always From cfd47208c4e9f71fae45971db7e084ffc84cf6c7 Mon Sep 17 00:00:00 2001 From: Steve Richert Date: Fri, 24 Mar 2017 14:18:20 -0400 Subject: [PATCH 12/27] Reconfigure the CodeClimate test reporter for version 1.0+ --- .travis.yml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index aaf4e05..8832f53 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,12 +7,6 @@ branches: - master - v4 cache: bundler -env: - global: - - secure: | # CODECLIMATE_REPO_TOKEN - BIemhM273wHZMpuULDMYGPsxYdfw+NMw7IQbOD6gy5r+dha07y9ssTYYE5Gn - t1ptAb09lhQ4gexXTr83i6angMrnHgQ1ZX2wfeoZ0FvWDHQht9YkXyiNH+R6 - odHUeDIYAlUiqLX9nAkklL89Rc22BrHMGGNyuA8Uc5sktW5P/FE= language: ruby matrix: allow_failures: @@ -20,7 +14,6 @@ matrix: - rvm: "2.1" - rvm: "2.2" - rvm: ruby-head - - rvm: "2.0" notifications: webhooks: on_start: always From c2715e7757a955ebe6d05bccbbcb27b594d3d2df Mon Sep 17 00:00:00 2001 From: Steve Richert Date: Fri, 31 Mar 2017 09:09:05 -0400 Subject: [PATCH 13/27] Update test dependency version requirements --- Gemfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index 28a7a47..aae1e1f 100644 --- a/Gemfile +++ b/Gemfile @@ -7,5 +7,5 @@ gem "standard" group :test do gem "codeclimate-test-reporter", require: false gem "rspec", "~> 3.7" - gem "rubocop", "~> 0.47.1" + gem "rubocop", "~> 0.48.0" end From 372d608a3a8c2933fc9ca22c524b4c28106e411f Mon Sep 17 00:00:00 2001 From: Steve Richert Date: Fri, 31 Mar 2017 10:55:50 -0400 Subject: [PATCH 14/27] Fix new Rubocop failures as of version 0.48.0 This also adds a little bit of organization to the .rubocop.yml configuration file. I also moved some specific rule exceptions out of .rubocop.yml into the code itself. --- .rubocop.yml | 46 ++++++++++++++++------------------- spec/integration_spec.rb | 4 +++ spec/interactor/hooks_spec.rb | 2 +- 3 files changed, 26 insertions(+), 26 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 57771ec..8338bf2 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,49 +1,45 @@ -Style/StringLiterals: - EnforcedStyle: double_quotes +# This should always correspond to the required Ruby version specified in the +# gemspec. +AllCops: + TargetRubyVersion: 2.0 +# TODO: What should we do here? Style/FrozenStringLiteralComment: Enabled: false -# Allow some style changes in specs -Metrics/ModuleLength: +# Allow some style changes in the specs. +AmbiguousBlockAssociation: Exclude: - spec/**/* Metrics/BlockLength: Exclude: - spec/**/* -Style/BlockDelimiters: +Metrics/ModuleLength: Exclude: - spec/**/* -Style/RescueModifier: +Style/BlockDelimiters: Exclude: - spec/**/* -Metrics/MethodLength: - Exclude: - - spec/interactor/hooks_spec.rb -Style/IndentArray: - Exclude: - - spec/integration_spec.rb - - spec/interactor/hooks_spec.rb -# Allow nice tree-like comments in specs -Style/AsciiComments: - Exclude: - - spec/integration_spec.rb - -# Here inconsistent indentation helps to understand -# tree nature of callbacks. +# Here, inconsistent indentation helps to understand tree nature of callbacks. Style/AlignArray: Exclude: - spec/integration_spec.rb -# This could be removed if throws are used instead of -# raising Failure in #fail! +# TODO: Remove when throw is used rather than raise in Interactor::Context.fail! Lint/HandleExceptions: Exclude: - lib/interactor.rb +Style/RescueModifier: + Exclude: + - spec/**/* +# These style conventions are personal preference. Style/EmptyMethod: Enabled: false - -AllCops: - TargetRubyVersion: 2.0 +Style/IndentArray: + EnforcedStyle: consistent +Style/StringLiterals: + EnforcedStyle: double_quotes +Style/SymbolArray: + Enabled: false diff --git a/spec/integration_spec.rb b/spec/integration_spec.rb index 1f16945..dfc196a 100644 --- a/spec/integration_spec.rb +++ b/spec/integration_spec.rb @@ -22,6 +22,8 @@ def unexpected_error! organizer end + # rubocop:disable Style/AsciiComments + # # organizer # ├─ organizer2 # │ ├─ interactor2a @@ -33,6 +35,8 @@ def unexpected_error! # │ ├─ interactor4b # │ └─ interactor4c # └─ interactor5 + # + # rubocop:enable Style/AsciiComments let(:organizer) { interactors = [organizer2, interactor3, organizer4, interactor5] diff --git a/spec/interactor/hooks_spec.rb b/spec/interactor/hooks_spec.rb index e0a188b..a15fcdd 100644 --- a/spec/interactor/hooks_spec.rb +++ b/spec/interactor/hooks_spec.rb @@ -1,7 +1,7 @@ module Interactor describe Hooks do describe "#with_hooks" do - def build_hooked(&block) + def build_hooked(&block) # rubocop:disable Metrics/MethodLength hooked = Class.new.send(:include, Interactor::Hooks) hooked.class_eval do From 13a97c89126155f5cfe7af596e43403645f8416a Mon Sep 17 00:00:00 2001 From: Anton Chuchkalov Date: Sun, 26 Mar 2017 11:03:13 +0300 Subject: [PATCH 15/27] allow multiple organize calls in an organizer (implements #127) --- lib/interactor/organizer.rb | 3 ++- spec/integration_spec.rb | 26 ++++++++++++++++++++++++++ spec/interactor/organizer_spec.rb | 10 ++++++++++ 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/lib/interactor/organizer.rb b/lib/interactor/organizer.rb index fcba5bc..1d27992 100644 --- a/lib/interactor/organizer.rb +++ b/lib/interactor/organizer.rb @@ -41,11 +41,12 @@ module ClassMethods # include Interactor::Organizer # # organize [InteractorThree, InteractorFour] + # organize InteractorFive # end # # Returns nothing. def organize(*interactors) - @organized = interactors.flatten + organized.concat(interactors.flatten) end # Internal: An Array of declared Interactors to be invoked. diff --git a/spec/integration_spec.rb b/spec/integration_spec.rb index dfc196a..f828fb8 100644 --- a/spec/integration_spec.rb +++ b/spec/integration_spec.rb @@ -1817,4 +1817,30 @@ def rollback }.to raise_error("foo") end end + + context "when there are multiple organize calls" do + it "runs all passed interactors in correct order" do + organizer = build_organizer(organize: [organizer2, interactor3]) + organizer.organize(organizer4, interactor5) + + expect { + organizer.call(context) + }.to change { + context.steps + }.from([]).to([ + :around_before2, :before2, + :around_before2a, :before2a, :call2a, :after2a, :around_after2a, + :around_before2b, :before2b, :call2b, :after2b, :around_after2b, + :around_before2c, :before2c, :call2c, :after2c, :around_after2c, + :after2, :around_after2, + :around_before3, :before3, :call3, :after3, :around_after3, + :around_before4, :before4, + :around_before4a, :before4a, :call4a, :after4a, :around_after4a, + :around_before4b, :before4b, :call4b, :after4b, :around_after4b, + :around_before4c, :before4c, :call4c, :after4c, :around_after4c, + :after4, :around_after4, + :around_before5, :before5, :call5, :after5, :around_after5 + ]) + end + end end diff --git a/spec/interactor/organizer_spec.rb b/spec/interactor/organizer_spec.rb index 5b02aaa..a664c7b 100644 --- a/spec/interactor/organizer_spec.rb +++ b/spec/interactor/organizer_spec.rb @@ -23,6 +23,16 @@ module Interactor organizer.organized }.from([]).to([interactor2, interactor3]) end + + it "allows multiple organize calls" do + interactor4 = double(:interactor4) + expect { + organizer.organize(interactor2, interactor3) + organizer.organize(interactor4) + }.to change { + organizer.organized + }.from([]).to([interactor2, interactor3, interactor4]) + end end describe ".organized" do From 780d202da5d1b652b7620f6d5ca0f9e6d0ea5e39 Mon Sep 17 00:00:00 2001 From: Taylor Thurlow Date: Sun, 12 Jul 2020 15:23:17 -0700 Subject: [PATCH 16/27] Remove bundler development dependency --- interactor.gemspec | 1 - 1 file changed, 1 deletion(-) diff --git a/interactor.gemspec b/interactor.gemspec index 65afccb..a499dad 100644 --- a/interactor.gemspec +++ b/interactor.gemspec @@ -15,6 +15,5 @@ Gem::Specification.new do |spec| spec.required_ruby_version = ">= 2.0" - spec.add_development_dependency "bundler", "~> 1.9" spec.add_development_dependency "rake", "~> 10.4" end From 71ffe1070acc2210ab9dd78a14484b8225772bc0 Mon Sep 17 00:00:00 2001 From: Taylor Thurlow Date: Sun, 12 Jul 2020 15:39:12 -0700 Subject: [PATCH 17/27] Update rake development dependency to 13.x --- interactor.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interactor.gemspec b/interactor.gemspec index a499dad..daf308c 100644 --- a/interactor.gemspec +++ b/interactor.gemspec @@ -15,5 +15,5 @@ Gem::Specification.new do |spec| spec.required_ruby_version = ">= 2.0" - spec.add_development_dependency "rake", "~> 10.4" + spec.add_development_dependency "rake", "~> 13.0" end From b07843033898c4834719084c59d32db14691704f Mon Sep 17 00:00:00 2001 From: Taylor Thurlow Date: Sun, 12 Jul 2020 15:23:42 -0700 Subject: [PATCH 18/27] Use Rubocop 0.85.x because it supports the latest standardrb version --- .rubocop.yml | 6 +++--- Gemfile | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 8338bf2..cbba9fe 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -22,12 +22,12 @@ Style/BlockDelimiters: - spec/**/* # Here, inconsistent indentation helps to understand tree nature of callbacks. -Style/AlignArray: +Layout/ArrayAlignment: Exclude: - spec/integration_spec.rb # TODO: Remove when throw is used rather than raise in Interactor::Context.fail! -Lint/HandleExceptions: +Lint/SuppressedException: Exclude: - lib/interactor.rb Style/RescueModifier: @@ -37,7 +37,7 @@ Style/RescueModifier: # These style conventions are personal preference. Style/EmptyMethod: Enabled: false -Style/IndentArray: +Layout/IndentArray: EnforcedStyle: consistent Style/StringLiterals: EnforcedStyle: double_quotes diff --git a/Gemfile b/Gemfile index aae1e1f..fcebd04 100644 --- a/Gemfile +++ b/Gemfile @@ -7,5 +7,5 @@ gem "standard" group :test do gem "codeclimate-test-reporter", require: false gem "rspec", "~> 3.7" - gem "rubocop", "~> 0.48.0" + gem "rubocop", "~> 0.85.0" end From ba6c79c0236cbc4187e29df6d0f75a796cdd8123 Mon Sep 17 00:00:00 2001 From: Taylor Thurlow Date: Sun, 12 Jul 2020 15:44:46 -0700 Subject: [PATCH 19/27] Drop support for all EOL Ruby versions (< 2.5) --- .rubocop.yml | 2 +- .standard.yml | 2 +- .travis.yml | 8 -------- CHANGELOG.md | 3 +-- interactor.gemspec | 2 +- 5 files changed, 4 insertions(+), 13 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index cbba9fe..0f9ec8a 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,7 +1,7 @@ # This should always correspond to the required Ruby version specified in the # gemspec. AllCops: - TargetRubyVersion: 2.0 + TargetRubyVersion: 2.5 # TODO: What should we do here? Style/FrozenStringLiteralComment: diff --git a/.standard.yml b/.standard.yml index 0d286f2..cc9dba4 100644 --- a/.standard.yml +++ b/.standard.yml @@ -1,4 +1,4 @@ -ruby_version: 2.3.0 +ruby_version: 2.5.0 ignore: - 'spec/**/*': - Lint/AmbiguousBlockAssociation diff --git a/.travis.yml b/.travis.yml index 8832f53..45ebe43 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,9 +10,6 @@ cache: bundler language: ruby matrix: allow_failures: - - rvm: "2.0" - - rvm: "2.1" - - rvm: "2.2" - rvm: ruby-head notifications: webhooks: @@ -20,11 +17,6 @@ notifications: urls: - http://buildlight.collectiveidea.com/ rvm: - - "2.0" - - "2.1" - - "2.2" - - "2.3" - - "2.4" - "2.5" - "2.6" - "2.7" diff --git a/CHANGELOG.md b/CHANGELOG.md index af67534..069da3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,6 @@ ## 4.0.0 / Unreleased -* [ENHANCEMENT] Add support for Ruby 2.2 -* [ENHANCEMENT] Drop support for Ruby 1.9 +* [ENHANCEMENT] Drop support for all EOL Ruby versions (< 2.5) ## 3.1.2 / 2019-12-29 * [BUGFIX] Fix Context#fail! on Ruby 2.7 diff --git a/interactor.gemspec b/interactor.gemspec index daf308c..0f0440c 100644 --- a/interactor.gemspec +++ b/interactor.gemspec @@ -13,7 +13,7 @@ Gem::Specification.new do |spec| spec.test_files = spec.files.grep(/^spec/) - spec.required_ruby_version = ">= 2.0" + spec.required_ruby_version = ">= 2.5" spec.add_development_dependency "rake", "~> 13.0" end From 3bbca930a805fe8b31920fbeca2d8571c71590bc Mon Sep 17 00:00:00 2001 From: Taylor Thurlow Date: Sun, 12 Jul 2020 15:47:48 -0700 Subject: [PATCH 20/27] Run "standardrb --fix" --- interactor.gemspec | 2 +- lib/interactor/hooks.rb | 4 +- spec/integration_spec.rb | 66 ++++++++++++++++----------------- spec/interactor/context_spec.rb | 8 ++-- spec/interactor/hooks_spec.rb | 26 ++++++------- 5 files changed, 52 insertions(+), 54 deletions(-) diff --git a/interactor.gemspec b/interactor.gemspec index 0f0440c..0972c7d 100644 --- a/interactor.gemspec +++ b/interactor.gemspec @@ -1,7 +1,7 @@ require "English" Gem::Specification.new do |spec| - spec.name = "interactor" + spec.name = "interactor" spec.version = "4.0.0" spec.author = "Collective Idea" diff --git a/lib/interactor/hooks.rb b/lib/interactor/hooks.rb index 7ae4d00..d82e7a5 100644 --- a/lib/interactor/hooks.rb +++ b/lib/interactor/hooks.rb @@ -219,9 +219,9 @@ def with_hooks # # Returns nothing. def run_around_hooks(&block) - self.class.around_hooks.reverse.inject(block) do |chain, hook| + self.class.around_hooks.reverse.inject(block) { |chain, hook| proc { run_hook(hook, chain) } - end.call + }.call end # Internal: Run before hooks. diff --git a/spec/integration_spec.rb b/spec/integration_spec.rb index f828fb8..82fb67a 100644 --- a/spec/integration_spec.rb +++ b/spec/integration_spec.rb @@ -323,7 +323,7 @@ def rollback :around_before4c, :before4c, :call4c, :after4c, :around_after4c, :after4, :around_after4, :around_before5, :before5, :call5, :after5, :around_after5, - :after, :around_after, + :after, :around_after ]) end end @@ -427,7 +427,7 @@ def rollback }.to change { context.steps }.from([]).to([ - :around_before, + :around_before ]) end end @@ -463,7 +463,7 @@ def rollback }.to change { context.steps }.from([]).to([ - :around_before, + :around_before ]) end @@ -521,7 +521,7 @@ def rollback :rollback3, :rollback2c, :rollback2b, - :rollback2a, + :rollback2a ]) end end @@ -577,7 +577,7 @@ def rollback :rollback3, :rollback2c, :rollback2b, - :rollback2a, + :rollback2a ]) end @@ -636,7 +636,7 @@ def rollback :rollback3, :rollback2c, :rollback2b, - :rollback2a, + :rollback2a ]) end end @@ -693,7 +693,7 @@ def rollback :rollback3, :rollback2c, :rollback2b, - :rollback2a, + :rollback2a ]) end @@ -746,7 +746,7 @@ def rollback :after2, :around_after2, :rollback2c, :rollback2b, - :rollback2a, + :rollback2a ]) end end @@ -797,7 +797,7 @@ def rollback :after2, :around_after2, :rollback2c, :rollback2b, - :rollback2a, + :rollback2a ]) end @@ -851,7 +851,7 @@ def rollback :around_before3, :rollback2c, :rollback2b, - :rollback2a, + :rollback2a ]) end end @@ -903,7 +903,7 @@ def rollback :around_before3, :rollback2c, :rollback2b, - :rollback2a, + :rollback2a ]) end @@ -957,7 +957,7 @@ def rollback :around_before3, :before3, :rollback2c, :rollback2b, - :rollback2a, + :rollback2a ]) end end @@ -1009,7 +1009,7 @@ def rollback :around_before3, :before3, :rollback2c, :rollback2b, - :rollback2a, + :rollback2a ]) end @@ -1064,7 +1064,7 @@ def rollback :rollback3, :rollback2c, :rollback2b, - :rollback2a, + :rollback2a ]) end end @@ -1117,7 +1117,7 @@ def rollback :rollback3, :rollback2c, :rollback2b, - :rollback2a, + :rollback2a ]) end @@ -1172,7 +1172,7 @@ def rollback :rollback3, :rollback2c, :rollback2b, - :rollback2a, + :rollback2a ]) end end @@ -1225,7 +1225,7 @@ def rollback :rollback3, :rollback2c, :rollback2b, - :rollback2a, + :rollback2a ]) end @@ -1283,7 +1283,7 @@ def rollback :rollback3, :rollback2c, :rollback2b, - :rollback2a, + :rollback2a ]) end end @@ -1339,7 +1339,7 @@ def rollback :rollback3, :rollback2c, :rollback2b, - :rollback2a, + :rollback2a ]) end @@ -1398,7 +1398,7 @@ def rollback :rollback3, :rollback2c, :rollback2b, - :rollback2a, + :rollback2a ]) end end @@ -1455,7 +1455,7 @@ def rollback :rollback3, :rollback2c, :rollback2b, - :rollback2a, + :rollback2a ]) end @@ -1514,7 +1514,7 @@ def rollback :rollback3, :rollback2c, :rollback2b, - :rollback2a, + :rollback2a ]) end end @@ -1571,7 +1571,7 @@ def rollback :rollback3, :rollback2c, :rollback2b, - :rollback2a, + :rollback2a ]) end @@ -1631,7 +1631,7 @@ def rollback :rollback3, :rollback2c, :rollback2b, - :rollback2a, + :rollback2a ]) end end @@ -1689,7 +1689,7 @@ def rollback :rollback3, :rollback2c, :rollback2b, - :rollback2a, + :rollback2a ]) end @@ -1749,7 +1749,7 @@ def rollback :rollback3, :rollback2c, :rollback2b, - :rollback2a, + :rollback2a ]) end end @@ -1807,7 +1807,7 @@ def rollback :rollback3, :rollback2c, :rollback2b, - :rollback2a, + :rollback2a ]) end @@ -1829,15 +1829,15 @@ def rollback context.steps }.from([]).to([ :around_before2, :before2, - :around_before2a, :before2a, :call2a, :after2a, :around_after2a, - :around_before2b, :before2b, :call2b, :after2b, :around_after2b, - :around_before2c, :before2c, :call2c, :after2c, :around_after2c, + :around_before2a, :before2a, :call2a, :after2a, :around_after2a, + :around_before2b, :before2b, :call2b, :after2b, :around_after2b, + :around_before2c, :before2c, :call2c, :after2c, :around_after2c, :after2, :around_after2, :around_before3, :before3, :call3, :after3, :around_after3, :around_before4, :before4, - :around_before4a, :before4a, :call4a, :after4a, :around_after4a, - :around_before4b, :before4b, :call4b, :after4b, :around_after4b, - :around_before4c, :before4c, :call4c, :after4c, :around_after4c, + :around_before4a, :before4a, :call4a, :after4a, :around_after4a, + :around_before4b, :before4b, :call4b, :after4b, :around_after4b, + :around_before4c, :before4c, :call4c, :after4c, :around_after4c, :after4, :around_after4, :around_before5, :before5, :call5, :after5, :around_after5 ]) diff --git a/spec/interactor/context_spec.rb b/spec/interactor/context_spec.rb index 1769172..94ad4b2 100644 --- a/spec/interactor/context_spec.rb +++ b/spec/interactor/context_spec.rb @@ -144,11 +144,9 @@ module Interactor end it "makes the context available from the failure" do - begin - context.fail! - rescue Failure => error - expect(error.context).to eq(context) - end + context.fail! + rescue Failure => error + expect(error.context).to eq(context) end end diff --git a/spec/interactor/hooks_spec.rb b/spec/interactor/hooks_spec.rb index a15fcdd..2e46e82 100644 --- a/spec/interactor/hooks_spec.rb +++ b/spec/interactor/hooks_spec.rb @@ -43,7 +43,7 @@ def add_around_before_and_around_after(hooked) expect(hooked.process).to eq([ :around_before, :process, - :around_after, + :around_after ]) end end @@ -63,7 +63,7 @@ def add_around_before_and_around_after(hooked) expect(hooked.process).to eq([ :around_before, :process, - :around_after, + :around_after ]) end end @@ -93,7 +93,7 @@ def add_around_before1_and_around_after1(hooked) :around_before2, :process, :around_after2, - :around_after1, + :around_after1 ]) end end @@ -125,7 +125,7 @@ def add_around_before2_and_around_after2(hooked) :around_before2, :process, :around_after2, - :around_after1, + :around_after1 ]) end end @@ -146,7 +146,7 @@ def add_before it "runs the before hook method" do expect(hooked.process).to eq([ :before, - :process, + :process ]) end end @@ -163,7 +163,7 @@ def add_before it "runs the before hook block" do expect(hooked.process).to eq([ :before, - :process, + :process ]) end end @@ -187,7 +187,7 @@ def add_before1 expect(hooked.process).to eq([ :before1, :before2, - :process, + :process ]) end end @@ -213,7 +213,7 @@ def add_before2 expect(hooked.process).to eq([ :before1, :before2, - :process, + :process ]) end end @@ -234,7 +234,7 @@ def add_after it "runs the after hook method" do expect(hooked.process).to eq([ :process, - :after, + :after ]) end end @@ -251,7 +251,7 @@ def add_after it "runs the after hook block" do expect(hooked.process).to eq([ :process, - :after, + :after ]) end end @@ -275,7 +275,7 @@ def add_after1 expect(hooked.process).to eq([ :process, :after2, - :after1, + :after1 ]) end end @@ -301,7 +301,7 @@ def add_after2 expect(hooked.process).to eq([ :process, :after2, - :after1, + :after1 ]) end end @@ -349,7 +349,7 @@ def add_after2 :after2, :after1, :around_after2, - :around_after1, + :around_after1 ]) end end From d667412fdbb95be5ad769c024bb8e6496c09a464 Mon Sep 17 00:00:00 2001 From: Taylor Thurlow Date: Sun, 12 Jul 2020 16:00:18 -0700 Subject: [PATCH 21/27] Fix standardrb warning --- spec/spec_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index ead3a1c..8a6db02 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -5,4 +5,4 @@ require "interactor" -Dir[File.expand_path("../support/*.rb", __FILE__)].each { |f| require f } +Dir[File.expand_path("../support/*.rb", __FILE__)].sort.each { |f| require f } From 8ab998a10d1c2df2995edff67793c521e39ffa9f Mon Sep 17 00:00:00 2001 From: Steve Richert Date: Fri, 31 Mar 2017 13:56:27 -0400 Subject: [PATCH 22/27] Allow developers to define #call with arguments for convenience If the "call" instance method accepts arguments, those arguments will be automatically assigned from the provided context, matching on name. This works for both positional and keyword arguments. If an argument is specified but no matching value is provided in the context, an ArgumentError is raised. --- lib/interactor.rb | 30 +++++- spec/interactor_spec.rb | 210 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 239 insertions(+), 1 deletion(-) diff --git a/lib/interactor.rb b/lib/interactor.rb index 2423630..69edce0 100644 --- a/lib/interactor.rb +++ b/lib/interactor.rb @@ -140,7 +140,7 @@ def run # Raises Interactor::Failure if the context is failed. def run! with_hooks do - call + call(*arguments_for_call) context.called!(self) end rescue @@ -163,4 +163,32 @@ def call # Returns nothing. def rollback end + + private + + # Internal: Determine what arguments (if any) should be passed to the "call" + # instance method when invoking an Interactor. The "call" instance method may + # accept any combination of positional and keyword arguments. This method + # will extract values from the context in order to populate those arguments + # based on their names. + # + # Returns an Array of arguments to be applied as an argument list. + def arguments_for_call + positional_arguments, keyword_arguments = [], {} + available_context_keys = context.to_h.keys + + method(:call).parameters.each do |(type, name)| + next unless available_context_keys.include?(name) + + case type + when :req, :opt + positional_arguments << context[name] + when :keyreq, :key + keyword_arguments[name] = context[name] + end + end + + positional_arguments << keyword_arguments if keyword_arguments.any? + positional_arguments + end end diff --git a/spec/interactor_spec.rb b/spec/interactor_spec.rb index 05eefdf..2153bfc 100644 --- a/spec/interactor_spec.rb +++ b/spec/interactor_spec.rb @@ -1,3 +1,213 @@ describe Interactor do include_examples :lint + + describe "#call" do + let(:interactor) { Class.new.send(:include, described_class) } + + context "positional arguments" do + it "accepts required positional arguments" do + interactor.class_eval do + def call(foo) + context.output = foo + end + end + + result = interactor.call(foo: "baz", hello: "world") + + expect(result.output).to eq("baz") + end + + it "accepts optional positional arguments" do + interactor.class_eval do + def call(foo = "bar") + context.output = foo + end + end + + result = interactor.call(foo: "baz", hello: "world") + + expect(result.output).to eq("baz") + end + + it "assigns absent positional arguments" do + interactor.class_eval do + def call(foo = "bar") + context.output = foo + end + end + + result = interactor.call(hello: "world") + + expect(result.output).to eq("bar") + end + + it "raises an error for missing positional arguments" do + interactor.class_eval do + def call(foo) + context.output = foo + end + end + + expect { interactor.call(hello: "world") }.to raise_error(ArgumentError) + end + end + + context "keyword arguments" do + it "accepts required keyword arguments" do + interactor.class_eval do + def call(foo:) + context.output = foo + end + end + + result = interactor.call(foo: "baz", hello: "world") + + expect(result.output).to eq("baz") + end + + it "accepts optional keyword arguments" do + interactor.class_eval do + def call(foo: "bar") + context.output = foo + end + end + + result = interactor.call(foo: "baz", hello: "world") + + expect(result.output).to eq("baz") + end + + it "assigns absent keyword arguments" do + interactor.class_eval do + def call(foo: "bar") + context.output = foo + end + end + + result = interactor.call(hello: "world") + + expect(result.output).to eq("bar") + end + + it "raises an error for missing keyword arguments" do + interactor.class_eval do + def call(foo:) + context.output = foo + end + end + + expect { interactor.call(hello: "world") }.to raise_error(ArgumentError) + end + end + + context "combination arguments" do + it "accepts required positional with required keyword arguments" do + interactor.class_eval do + def call(foo, hello:) + context.output = [foo, hello] + end + end + + result = interactor.call(foo: "baz", hello: "world") + + expect(result.output).to eq(["baz", "world"]) + end + + it "accepts required positional with optional keyword arguments" do + interactor.class_eval do + def call(foo, hello: "there") + context.output = [foo, hello] + end + end + + result = interactor.call(foo: "baz", hello: "world") + + expect(result.output).to eq(["baz", "world"]) + end + + it "accepts required positional and assigns absent keyword arguments" do + interactor.class_eval do + def call(foo, hello: "there") + context.output = [foo, hello] + end + end + + result = interactor.call(foo: "baz") + + expect(result.output).to eq(["baz", "there"]) + end + + it "accepts optional positional with required keyword arguments" do + interactor.class_eval do + def call(foo = "bar", hello:) + context.output = [foo, hello] + end + end + + result = interactor.call(foo: "baz", hello: "world") + + expect(result.output).to eq(["baz", "world"]) + end + + it "accepts optional positional with optional keyword arguments" do + interactor.class_eval do + def call(foo = "bar", hello: "there") + context.output = [foo, hello] + end + end + + result = interactor.call(foo: "baz", hello: "world") + + expect(result.output).to eq(["baz", "world"]) + end + + it "accepts optional positional and assigns absent keyword arguments" do + interactor.class_eval do + def call(foo = "bar", hello: "there") + context.output = [foo, hello] + end + end + + result = interactor.call(foo: "baz") + + expect(result.output).to eq(["baz", "there"]) + end + + it "assigns absent positional and accepts required keyword arguments" do + interactor.class_eval do + def call(foo = "bar", hello:) + context.output = [foo, hello] + end + end + + result = interactor.call(hello: "world") + + expect(result.output).to eq(["bar", "world"]) + end + + it "assigns absent positional and accepts optional keyword arguments" do + interactor.class_eval do + def call(foo = "bar", hello: "there") + context.output = [foo, hello] + end + end + + result = interactor.call(hello: "world") + + expect(result.output).to eq(["bar", "world"]) + end + + it "assigns absent positional and absent keyword arguments" do + interactor.class_eval do + def call(foo = "bar", hello: "there") + context.output = [foo, hello] + end + end + + result = interactor.call + + expect(result.output).to eq(["bar", "there"]) + end + end + end end From 8bc3e8ce9eeed85ef07a60599ad16cbfb3ebea77 Mon Sep 17 00:00:00 2001 From: Steve Richert Date: Fri, 31 Mar 2017 15:10:40 -0400 Subject: [PATCH 23/27] Address Rubocop concerns This also improves performance of the Interactor#arguments_for_call method by not duplicating the table of data held internally by the context. As a happy side effect, this adds the Interactor::Context#include? method which may be helpful for developers. --- .rubocop.yml | 2 ++ lib/interactor.rb | 14 ++++++-------- lib/interactor/context.rb | 9 +++++++++ spec/interactor/context_spec.rb | 20 ++++++++++++++++++++ 4 files changed, 37 insertions(+), 8 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 0f9ec8a..3f87993 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -43,3 +43,5 @@ Style/StringLiterals: EnforcedStyle: double_quotes Style/SymbolArray: Enabled: false +Style/WordArray: + Enabled: false diff --git a/lib/interactor.rb b/lib/interactor.rb index 69edce0..8cf246f 100644 --- a/lib/interactor.rb +++ b/lib/interactor.rb @@ -173,18 +173,16 @@ def rollback # based on their names. # # Returns an Array of arguments to be applied as an argument list. - def arguments_for_call - positional_arguments, keyword_arguments = [], {} - available_context_keys = context.to_h.keys + def arguments_for_call # rubocop:disable Metrics/MethodLength + positional_arguments = [] + keyword_arguments = {} method(:call).parameters.each do |(type, name)| - next unless available_context_keys.include?(name) + next unless context.include?(name) case type - when :req, :opt - positional_arguments << context[name] - when :keyreq, :key - keyword_arguments[name] = context[name] + when :req, :opt then positional_arguments << context[name] + when :keyreq, :key then keyword_arguments[name] = context[name] end end diff --git a/lib/interactor/context.rb b/lib/interactor/context.rb index b3e893b..399780d 100644 --- a/lib/interactor/context.rb +++ b/lib/interactor/context.rb @@ -158,6 +158,15 @@ def rollback! @rolled_back = true end + # Public: Check for the presence of a given key in the context. This does + # not check whether the value is truthy, just whether the key is set to any + # value at all. + # + # Returns true if the key is found or false otherwise. + def include?(key) + table.include?(key.to_sym) + end + # Internal: An Array of successfully called Interactor instances invoked # against this Interactor::Context instance. # diff --git a/spec/interactor/context_spec.rb b/spec/interactor/context_spec.rb index 94ad4b2..86f1fb1 100644 --- a/spec/interactor/context_spec.rb +++ b/spec/interactor/context_spec.rb @@ -190,6 +190,26 @@ module Interactor end end + describe "#include?" do + it "returns true if the key is found" do + context = Context.build(foo: "bar") + + expect(context.include?(:foo)).to eq(true) + end + + it "returns true if the symbolized key is found" do + context = Context.build(foo: "bar") + + expect(context.include?("foo")).to eq(true) + end + + it "returns false if the key is not found" do + context = Context.build(foo: "bar") + + expect(context.include?(:hello)).to eq(false) + end + end + describe "#_called" do let(:context) { Context.build } From 2d9d0ed145548791d1d9773a8ad9e28699025485 Mon Sep 17 00:00:00 2001 From: Steve Richert Date: Fri, 31 Mar 2017 15:12:26 -0400 Subject: [PATCH 24/27] Refactor Interactor::Context#fail! to be simpler MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …by not interacting directly with OpenStruct#modifiable. --- lib/interactor/context.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/interactor/context.rb b/lib/interactor/context.rb index 399780d..57f0087 100644 --- a/lib/interactor/context.rb +++ b/lib/interactor/context.rb @@ -121,7 +121,7 @@ def failure? # # Raises Interactor::Failure initialized with the Interactor::Context. def fail!(context = {}) - context.each { |key, value| self[key.to_sym] = value } + context.each { |key, value| self[key] = value } @failure = true raise Failure, self end From c36f52b79811b46fd2f267fc25b326b9ecc78dd4 Mon Sep 17 00:00:00 2001 From: Steve Richert Date: Fri, 28 Apr 2017 08:45:17 -0400 Subject: [PATCH 25/27] Back out the concept of magically assigning positional call arguments There were edge cases that weren't yet covered by the specs that would fail for combinations of positional and keyword arguments depending on what's available in the context. For example: class MyInteractor include Interactor def call(a, b: "bears") context.output = [a, b] end end MyInteractor.call(b: "beets").output # => [{ b: "beets" }, "bears"] Plus, this simplifies the interface by giving the developer one choice rather than multiple competing choices that achieve the same thing. --- lib/interactor.rb | 18 ++--- spec/interactor_spec.rb | 160 ++-------------------------------------- 2 files changed, 14 insertions(+), 164 deletions(-) diff --git a/lib/interactor.rb b/lib/interactor.rb index 8cf246f..0109494 100644 --- a/lib/interactor.rb +++ b/lib/interactor.rb @@ -166,24 +166,22 @@ def rollback private - # Internal: Determine what arguments (if any) should be passed to the "call" - # instance method when invoking an Interactor. The "call" instance method may - # accept any combination of positional and keyword arguments. This method - # will extract values from the context in order to populate those arguments - # based on their names. + # Internal: Determine what keyword arguments (if any) should be passed to the + # "call" instance method when invoking an Interactor. The "call" instance + # method may accept any number of keyword arguments. This method will extract + # values from the context in order to populate those arguments based on their + # names. # # Returns an Array of arguments to be applied as an argument list. - def arguments_for_call # rubocop:disable Metrics/MethodLength + def arguments_for_call positional_arguments = [] keyword_arguments = {} method(:call).parameters.each do |(type, name)| + next unless type == :keyreq || type == :key next unless context.include?(name) - case type - when :req, :opt then positional_arguments << context[name] - when :keyreq, :key then keyword_arguments[name] = context[name] - end + keyword_arguments[name] = context[name] end positional_arguments << keyword_arguments if keyword_arguments.any? diff --git a/spec/interactor_spec.rb b/spec/interactor_spec.rb index 2153bfc..d85b8db 100644 --- a/spec/interactor_spec.rb +++ b/spec/interactor_spec.rb @@ -4,54 +4,6 @@ describe "#call" do let(:interactor) { Class.new.send(:include, described_class) } - context "positional arguments" do - it "accepts required positional arguments" do - interactor.class_eval do - def call(foo) - context.output = foo - end - end - - result = interactor.call(foo: "baz", hello: "world") - - expect(result.output).to eq("baz") - end - - it "accepts optional positional arguments" do - interactor.class_eval do - def call(foo = "bar") - context.output = foo - end - end - - result = interactor.call(foo: "baz", hello: "world") - - expect(result.output).to eq("baz") - end - - it "assigns absent positional arguments" do - interactor.class_eval do - def call(foo = "bar") - context.output = foo - end - end - - result = interactor.call(hello: "world") - - expect(result.output).to eq("bar") - end - - it "raises an error for missing positional arguments" do - interactor.class_eval do - def call(foo) - context.output = foo - end - end - - expect { interactor.call(hello: "world") }.to raise_error(ArgumentError) - end - end - context "keyword arguments" do it "accepts required keyword arguments" do interactor.class_eval do @@ -60,9 +12,9 @@ def call(foo:) end end - result = interactor.call(foo: "baz", hello: "world") + result = interactor.call(foo: "bar", hello: "world") - expect(result.output).to eq("baz") + expect(result.output).to eq("bar") end it "accepts optional keyword arguments" do @@ -98,115 +50,15 @@ def call(foo:) expect { interactor.call(hello: "world") }.to raise_error(ArgumentError) end - end - - context "combination arguments" do - it "accepts required positional with required keyword arguments" do - interactor.class_eval do - def call(foo, hello:) - context.output = [foo, hello] - end - end - - result = interactor.call(foo: "baz", hello: "world") - - expect(result.output).to eq(["baz", "world"]) - end - - it "accepts required positional with optional keyword arguments" do - interactor.class_eval do - def call(foo, hello: "there") - context.output = [foo, hello] - end - end - - result = interactor.call(foo: "baz", hello: "world") - - expect(result.output).to eq(["baz", "world"]) - end - - it "accepts required positional and assigns absent keyword arguments" do - interactor.class_eval do - def call(foo, hello: "there") - context.output = [foo, hello] - end - end - - result = interactor.call(foo: "baz") - - expect(result.output).to eq(["baz", "there"]) - end - - it "accepts optional positional with required keyword arguments" do - interactor.class_eval do - def call(foo = "bar", hello:) - context.output = [foo, hello] - end - end - - result = interactor.call(foo: "baz", hello: "world") - - expect(result.output).to eq(["baz", "world"]) - end - it "accepts optional positional with optional keyword arguments" do + it "raises an error for call definitions with non-keyword arguments" do interactor.class_eval do - def call(foo = "bar", hello: "there") - context.output = [foo, hello] - end - end - - result = interactor.call(foo: "baz", hello: "world") - - expect(result.output).to eq(["baz", "world"]) - end - - it "accepts optional positional and assigns absent keyword arguments" do - interactor.class_eval do - def call(foo = "bar", hello: "there") - context.output = [foo, hello] - end - end - - result = interactor.call(foo: "baz") - - expect(result.output).to eq(["baz", "there"]) - end - - it "assigns absent positional and accepts required keyword arguments" do - interactor.class_eval do - def call(foo = "bar", hello:) - context.output = [foo, hello] - end - end - - result = interactor.call(hello: "world") - - expect(result.output).to eq(["bar", "world"]) - end - - it "assigns absent positional and accepts optional keyword arguments" do - interactor.class_eval do - def call(foo = "bar", hello: "there") - context.output = [foo, hello] - end - end - - result = interactor.call(hello: "world") - - expect(result.output).to eq(["bar", "world"]) - end - - it "assigns absent positional and absent keyword arguments" do - interactor.class_eval do - def call(foo = "bar", hello: "there") - context.output = [foo, hello] + def call(foo) + context.output = foo end end - result = interactor.call - - expect(result.output).to eq(["bar", "there"]) + expect { interactor.call(foo: "bar") }.to raise_error(ArgumentError) end end end From 5a683455286c7117a13e776220779f444fac5ef3 Mon Sep 17 00:00:00 2001 From: Anton Chuchkalov Date: Sat, 25 Mar 2017 12:28:58 +0300 Subject: [PATCH 26/27] replace raise with throw to handle context failure (#126) --- lib/interactor.rb | 22 +++++---- lib/interactor/context.rb | 2 +- lib/interactor/organizer.rb | 5 +- spec/interactor/context_spec.rb | 12 ++--- spec/interactor/organizer_spec.rb | 23 ++++++--- spec/support/lint.rb | 80 +++++++++++++++++++++++-------- 6 files changed, 96 insertions(+), 48 deletions(-) diff --git a/lib/interactor.rb b/lib/interactor.rb index 0109494..974bdb9 100644 --- a/lib/interactor.rb +++ b/lib/interactor.rb @@ -112,8 +112,17 @@ def initialize(context = {}) # # Returns nothing. def run - run! - rescue Failure + catch(:early_return) do + with_hooks do + call(*arguments_for_call) + context.called!(self) + end + end + + context.rollback! if context.failure? + rescue + context.rollback! + raise end # Internal: Invoke an Interactor instance along with all defined hooks. The @@ -139,13 +148,8 @@ def run # Returns nothing. # Raises Interactor::Failure if the context is failed. def run! - with_hooks do - call(*arguments_for_call) - context.called!(self) - end - rescue - context.rollback! - raise + run + raise(Failure, context) if context.failure? end # Public: Invoke an Interactor instance without any hooks, tracking, or diff --git a/lib/interactor/context.rb b/lib/interactor/context.rb index 57f0087..699b414 100644 --- a/lib/interactor/context.rb +++ b/lib/interactor/context.rb @@ -123,7 +123,7 @@ def failure? def fail!(context = {}) context.each { |key, value| self[key] = value } @failure = true - raise Failure, self + throw :early_return end # Internal: Track that an Interactor has been called. The "called!" method diff --git a/lib/interactor/organizer.rb b/lib/interactor/organizer.rb index 1d27992..6e22d64 100644 --- a/lib/interactor/organizer.rb +++ b/lib/interactor/organizer.rb @@ -8,7 +8,7 @@ module Interactor # class MyOrganizer # include Interactor::Organizer # - # organizer InteractorOne, InteractorTwo + # organize InteractorOne, InteractorTwo # end module Organizer # Internal: Install Interactor::Organizer's behavior in the given class. @@ -77,7 +77,8 @@ module InstanceMethods # Returns nothing. def call self.class.organized.each do |interactor| - interactor.call!(context) + throw(:early_return) if context.failure? + interactor.call(context) end end end diff --git a/spec/interactor/context_spec.rb b/spec/interactor/context_spec.rb index 86f1fb1..a479757 100644 --- a/spec/interactor/context_spec.rb +++ b/spec/interactor/context_spec.rb @@ -16,7 +16,7 @@ module Interactor end it "doesn't affect the original hash" do - hash = {foo: "bar"} + hash = { foo: "bar" } context = Context.build(hash) expect(context).to be_a(Context) @@ -137,16 +137,10 @@ module Interactor }.from("bar").to("baz") end - it "raises failure" do + it "throws :early_return" do expect { context.fail! - }.to raise_error(Failure) - end - - it "makes the context available from the failure" do - context.fail! - rescue Failure => error - expect(error.context).to eq(context) + }.to throw_symbol(:early_return) end end diff --git a/spec/interactor/organizer_spec.rb b/spec/interactor/organizer_spec.rb index a664c7b..825b693 100644 --- a/spec/interactor/organizer_spec.rb +++ b/spec/interactor/organizer_spec.rb @@ -43,25 +43,34 @@ module Interactor describe "#call" do let(:instance) { organizer.new } - let(:context) { double(:context) } + let(:context) { double(:context, failure?: false) } let(:interactor2) { double(:interactor2) } let(:interactor3) { double(:interactor3) } let(:interactor4) { double(:interactor4) } + let(:organized_interactors) { [interactor2, interactor3, interactor4] } before do allow(instance).to receive(:context) { context } - allow(organizer).to receive(:organized) { - [interactor2, interactor3, interactor4] - } + allow(organizer).to receive(:organized) { organized_interactors } + organized_interactors.each do |organized_interactor| + allow(organized_interactor).to receive(:call) + end end it "calls each interactor in order with the context" do - expect(interactor2).to receive(:call!).once.with(context).ordered - expect(interactor3).to receive(:call!).once.with(context).ordered - expect(interactor4).to receive(:call!).once.with(context).ordered + expect(interactor2).to receive(:call).once.with(context).ordered + expect(interactor3).to receive(:call).once.with(context).ordered + expect(interactor4).to receive(:call).once.with(context).ordered instance.call end + + it "throws :early_return on failure of one of organizers" do + allow(context).to receive(:failure?).and_return(false, true) + expect { + instance.call + }.to throw_symbol(:early_return) + end end end end diff --git a/spec/support/lint.rb b/spec/support/lint.rb index 73346e5..ffb18f2 100644 --- a/spec/support/lint.rb +++ b/spec/support/lint.rb @@ -1,6 +1,14 @@ shared_examples :lint do let(:interactor) { Class.new.send(:include, described_class) } + let(:context_double) do + double(:double, failure?: false, called!: nil, rollback!: nil) + end + + let(:failed_context_double) do + double(:failed_context_double, failure?: true, called!: nil, rollback!: nil) + end + describe ".call" do let(:context) { double(:context) } let(:instance) { double(:instance, context: context) } @@ -66,25 +74,45 @@ let(:instance) { interactor.new } it "runs the interactor" do - expect(instance).to receive(:run!).once.with(no_args) + expect(instance).to receive(:call).once.with(no_args) instance.run end - it "rescues failure" do - expect(instance).to receive(:run!).and_raise(Interactor::Failure) - + it "catches :early_return" do + allow(instance).to receive(:call).and_throw(:early_return) expect { instance.run - }.not_to raise_error + }.not_to throw_symbol end - it "raises other errors" do - expect(instance).to receive(:run!).and_raise("foo") + context "when error is raised inside #call" do + it "propagates it and rollbacks context" do + allow(instance).to receive(:context) { context_double } + allow(instance).to receive(:call).and_raise("foo") - expect { + expect(instance.context).to receive(:rollback!) + expect { + instance.run + }.to raise_error("foo") + end + end + + context "on call failure" do + before do + allow(instance).to receive(:context) { failed_context_double } + end + + it "doesn't raise Failure" do + expect { + instance.run + }.not_to raise_error + end + + it "rollbacks context on error" do + expect(instance.context).to receive(:rollback!) instance.run - }.to raise_error("foo") + end end end @@ -92,26 +120,38 @@ let(:instance) { interactor.new } it "calls the interactor" do - expect(instance).to receive(:call).once.with(no_args) + expect(instance).to receive(:run).once.with(no_args) instance.run! end - it "raises failure" do - expect(instance).to receive(:run!).and_raise(Interactor::Failure) - - expect { - instance.run! - }.to raise_error(Interactor::Failure) - end - - it "raises other errors" do - expect(instance).to receive(:run!).and_raise("foo") + it "propagates errors" do + expect(instance).to receive(:run).and_raise("foo") expect { instance.run }.to raise_error("foo") end + + context "on failure" do + before do + allow(instance).to receive(:context) { failed_context_double } + end + + it "raises Interactor::Failure" do + expect { + instance.run! + }.to raise_error(Interactor::Failure) + end + + it "makes context available from the error" do + begin + instance.run! + rescue Interactor::Failure => error + expect(error.context).to be(instance.context) + end + end + end end describe "#call" do From 6e34662c40d681f68fd1a4b3d1131e29e5356fdc Mon Sep 17 00:00:00 2001 From: Anton Chuchkalov Date: Sun, 26 Mar 2017 17:47:02 +0300 Subject: [PATCH 27/27] avoid context inner logic duplicating --- lib/interactor/context.rb | 4 ++++ lib/interactor/organizer.rb | 2 +- spec/interactor/organizer_spec.rb | 5 +++-- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/interactor/context.rb b/lib/interactor/context.rb index 699b414..47c5e2e 100644 --- a/lib/interactor/context.rb +++ b/lib/interactor/context.rb @@ -123,6 +123,10 @@ def failure? def fail!(context = {}) context.each { |key, value| self[key] = value } @failure = true + signal_early_return! + end + + def signal_early_return! throw :early_return end diff --git a/lib/interactor/organizer.rb b/lib/interactor/organizer.rb index 6e22d64..7a3799d 100644 --- a/lib/interactor/organizer.rb +++ b/lib/interactor/organizer.rb @@ -77,7 +77,7 @@ module InstanceMethods # Returns nothing. def call self.class.organized.each do |interactor| - throw(:early_return) if context.failure? + context.signal_early_return! if context.failure? interactor.call(context) end end diff --git a/spec/interactor/organizer_spec.rb b/spec/interactor/organizer_spec.rb index 825b693..b5404f5 100644 --- a/spec/interactor/organizer_spec.rb +++ b/spec/interactor/organizer_spec.rb @@ -65,11 +65,12 @@ module Interactor instance.call end - it "throws :early_return on failure of one of organizers" do + it "signals about early_return on failure of one of organizers" do allow(context).to receive(:failure?).and_return(false, true) + expect(context).to receive(:signal_early_return!).and_throw(:foo) expect { instance.call - }.to throw_symbol(:early_return) + }.to throw_symbol end end end