From da088fa6b12ec67561d077bbfa828ecb62c3f425 Mon Sep 17 00:00:00 2001 From: Sufyan Adam Date: Sun, 23 Jan 2022 19:40:19 -0800 Subject: [PATCH] Add Optional Context Contract / Verification capability Add a `needs_context :key1, :key2, :key3...` method to support basic context contract verification / checking. By including the `ContextValidation` module in your interactor class, you get to declare what items in the context are actually required for the interactor to work correctly. If someone tries to use an interactor without passing it any one of the required items, an error is raised. --- .github/workflows/test.yml | 25 +++++++++++ README.md | 42 +++++++++++++++++ lib/interactor.rb | 1 + lib/interactor/context_validation.rb | 30 +++++++++++++ spec/interactor/context_validation_spec.rb | 52 ++++++++++++++++++++++ 5 files changed, 150 insertions(+) create mode 100644 .github/workflows/test.yml create mode 100644 lib/interactor/context_validation.rb create mode 100644 spec/interactor/context_validation_spec.rb 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 6de00f2..16835d4 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 2423630..308097d 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