diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..6105638 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,25 @@ +name: Tests +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + ruby-version: ['2.6', '2.7', '3.0'] + steps: + - uses: actions/checkout@v2 + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby-version }} + bundler-cache: true # runs 'bundle install' and caches installed gems automatically + - name: Run tests + run: bundle exec rake + - name: Code Climate Coverage Action + uses: paambaati/codeclimate-action@v3.0.0 + env: + CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} diff --git a/README.md b/README.md index 143b397..f1d700f 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,48 @@ interactor needs to do its work. When an interactor does its single purpose, it affects its given context. +#### Declaring required keys in the context + +If you want to optionally declare what data an interactor requires to +function correctly, you can add the `ContextValidation` module to +your interactor and declare any required items. + +```ruby +include ContextValidation + +needs_context :user, :new_password +``` + +If any of the items from the `needs_context` declaration are missing, an +error is raised. + +```ruby +class UpdateUser + include Interactor + include ContextValidation + + needs_context :user, :new_password +end +``` + +```ruby +result = UpdateUser.call(user: user, new_password: 'newpasswordstring') +result.success? #=> true +``` + +```ruby +result = UpdateUser.call(user: user) +#> +``` + +Passing `nil` or `''` for the value of a required context will not raise +an error, to allow for setting items to those values. + +```ruby +result = UpdateUser.call(user: user, new_password: nil) +result.success? #=> true +``` + #### Adding to the Context As an interactor runs it can add information to the context. diff --git a/lib/interactor.rb b/lib/interactor.rb index 307234c..ed3f03a 100644 --- a/lib/interactor.rb +++ b/lib/interactor.rb @@ -1,4 +1,5 @@ require "interactor/context" +require "interactor/context_validation" require "interactor/error" require "interactor/hooks" require "interactor/organizer" diff --git a/lib/interactor/context_validation.rb b/lib/interactor/context_validation.rb new file mode 100644 index 0000000..9179f5b --- /dev/null +++ b/lib/interactor/context_validation.rb @@ -0,0 +1,30 @@ +module Interactor + module ContextValidation + def self.included(base) + base.extend(self) + end + + # Override Interactor before hooks to ensure + # that the needs_context before hook is + # executed last. This will allow us to + # set required context keys in an interactor + # before hook without raising a needs_context + # error. + def before(*hooks, &block) + before_hooks.unshift block if block + hooks.each { |h| before_hooks.unshift h } + end + + def needs_context(*args) + before_hooks.push -> { + missing_context = args - context.to_h.keys.map(&:to_sym) + missing_keys = missing_context.reduce([]) do |reduced, key| + reduced << key + reduced + end + + raise "Missing context: #{missing_keys.join(', ')} in #{self}" if missing_keys.any? + } + end + end +end diff --git a/spec/interactor/context_validation_spec.rb b/spec/interactor/context_validation_spec.rb new file mode 100644 index 0000000..54e823c --- /dev/null +++ b/spec/interactor/context_validation_spec.rb @@ -0,0 +1,52 @@ +module Interactor + describe ContextValidation do + describe "when context keys are missing" do + subject(:interactor) do + module Test + class SomeInteractor + include Interactor + include ContextValidation + + needs_context :a, :b + + def call + end + end + end + + Test::SomeInteractor + end + + it "raises an error" do + expect { interactor.call({}) }.to raise_error(/Missing context: a, b/) + end + end + + context "when missing context keys are set in a before hook" do + subject(:interactor) do + module Test + class InteractorWithContextInBeforeHook + include Interactor + include ContextValidation + + needs_context :a, :b + + before do + context.a = 'a' + context.b = 'b' + end + + def call + end + end + end + + Test::InteractorWithContextInBeforeHook + end + + it "does not raise an error" do + expect { interactor.call({}) }.not_to raise_error + end + end + end +end