Skip to content

Commit

Permalink
contracts: implement a couple of example integrations
Browse files Browse the repository at this point in the history
  • Loading branch information
alessandro-fazzi committed Aug 11, 2024
1 parent 72585a0 commit 9c69e75
Show file tree
Hide file tree
Showing 7 changed files with 227 additions and 0 deletions.
4 changes: 4 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,7 @@ gem "rake", "~> 13.0"
gem "minitest", "~> 5.16"

gem "rubocop", "~> 1.21"

gem "activemodel", "~> 7.2"

gem "dry-validation", "~> 1.10"
60 changes: 60 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,63 @@ PATH
GEM
remote: https://rubygems.org/
specs:
activemodel (7.2.0)
activesupport (= 7.2.0)
activesupport (7.2.0)
base64
bigdecimal
concurrent-ruby (~> 1.0, >= 1.3.1)
connection_pool (>= 2.2.5)
drb
i18n (>= 1.6, < 2)
logger (>= 1.4.2)
minitest (>= 5.1)
securerandom (>= 0.3)
tzinfo (~> 2.0, >= 2.0.5)
ast (2.4.2)
base64 (0.2.0)
bigdecimal (3.1.8)
concurrent-ruby (1.3.4)
connection_pool (2.4.1)
drb (2.2.1)
dry-configurable (1.2.0)
dry-core (~> 1.0, < 2)
zeitwerk (~> 2.6)
dry-core (1.0.1)
concurrent-ruby (~> 1.0)
zeitwerk (~> 2.6)
dry-inflector (1.1.0)
dry-initializer (3.1.1)
dry-logic (1.5.0)
concurrent-ruby (~> 1.0)
dry-core (~> 1.0, < 2)
zeitwerk (~> 2.6)
dry-schema (1.13.4)
concurrent-ruby (~> 1.0)
dry-configurable (~> 1.0, >= 1.0.1)
dry-core (~> 1.0, < 2)
dry-initializer (~> 3.0)
dry-logic (>= 1.4, < 2)
dry-types (>= 1.7, < 2)
zeitwerk (~> 2.6)
dry-types (1.7.2)
bigdecimal (~> 3.0)
concurrent-ruby (~> 1.0)
dry-core (~> 1.0)
dry-inflector (~> 1.0)
dry-logic (~> 1.4)
zeitwerk (~> 2.6)
dry-validation (1.10.0)
concurrent-ruby (~> 1.0)
dry-core (~> 1.0, < 2)
dry-initializer (~> 3.0)
dry-schema (>= 1.12, < 2)
zeitwerk (~> 2.6)
i18n (1.14.5)
concurrent-ruby (~> 1.0)
json (2.7.2)
language_server-protocol (3.17.0.3)
logger (1.6.0)
minitest (5.24.1)
parallel (1.26.1)
parser (3.3.4.2)
Expand All @@ -34,14 +88,20 @@ GEM
rubocop-ast (1.32.0)
parser (>= 3.3.1.0)
ruby-progressbar (1.13.0)
securerandom (0.3.1)
strscan (3.1.0)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
unicode-display_width (2.5.0)
zeitwerk (2.6.17)

PLATFORMS
arm64-darwin-23
ruby

DEPENDENCIES
activemodel (~> 7.2)
dry-validation (~> 1.10)
i_do_not_need_interactor!
minitest (~> 5.16)
rake (~> 13.0)
Expand Down
36 changes: 36 additions & 0 deletions examples/activemodel_context.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# frozen_string_literal: true

require "bundler/inline"

gemfile true do
source "https://rubygems.org"

gem "activemodel"
gem "i_do_not_need_interactor", github: "alessandro-fazzi/i_do_not_need_interactor"
gem "amazing_print"
end

require "active_model"
require "amazing_print"

class DoSomething # rubocop:disable Style/Documentation
include Interactor
include Interactor::Contract::ActiveModel

def call(ctx)
ctx[:done] = true
end

contract do
attribute :done, :boolean
attribute :foo, :string
validates :foo, presence: true
end
end

result = DoSomething.call
if result.success?
ap result
else
ap result.errors
end
37 changes: 37 additions & 0 deletions examples/dry_validation_context.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# frozen_string_literal: true

require "bundler/inline"

gemfile true do
source "https://rubygems.org"

gem "dry-validation"
gem "i_do_not_need_interactor", github: "alessandro-fazzi/i_do_not_need_interactor"
gem "amazing_print"
end

require "dry-validation"
require "amazing_print"

class DoSomething # rubocop:disable Style/Documentation
include Interactor
include Interactor::Contract::DryValidation

def call(ctx)
ctx[:done] = true
end

contract do
params do
optional(:done).value(:bool)
required(:foo).value(:string)
end
end
end

result = DoSomething.call(done: false)
if result.success?
ap result
else
ap result.errors
end
49 changes: 49 additions & 0 deletions lib/i_do_not_need_interactor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ def pipe
end

def call(ctx = Context.new)
ctx = Context.new(**ctx) unless ctx.is_a?(Context)
new.maybe_call(ctx)
ctx
end
Expand All @@ -21,6 +22,11 @@ def call(ctx = Context.new)
def maybe_call(ctx = Context.new)
return ctx if ctx.failure?

if respond_to?(:validate)
validate(ctx)
return ctx if ctx.failure?
end

call(ctx)
ctx.register(self)

Expand Down Expand Up @@ -49,6 +55,49 @@ def success? = errors.empty?

def failure? = !success?
end

module Contract
module ActiveModel # rubocop:disable Style/Documentation
def self.included(descendant) # rubocop:disable Metrics/MethodLength
class << descendant
def contract(&block)
@contract = Class.new do
include ::ActiveModel::API
include ::ActiveModel::Attributes
include ::ActiveModel::Validations

def self.model_name
::ActiveModel::Name.new(self, nil, "temp")
end
end
@contract.class_exec(&block)
end
end
end

def validate(ctx)
self.class.instance_variable_get(:@contract).new(**ctx).validate!
rescue ::ActiveModel::ValidationError => e
ctx.errors << e.message
end
end

module DryValidation # rubocop:disable Style/Documentation
def self.included(descendant)
class << descendant
def contract(&block)
@contract = Class.new(::Dry::Validation::Contract)
@contract.class_exec(&block)
end
end
end

def validate(ctx)
validation_result = self.class.instance_variable_get(:@contract).new.call(ctx)
ctx.errors << validation_result.errors.to_h if validation_result.errors.any?
end
end
end
end

Interactor = IDoNotNeedInteractor unless Module.const_defined?("Interactor")
4 changes: 4 additions & 0 deletions test/test_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,7 @@
require "i_do_not_need_interactor"

require "minitest/autorun"

require "active_model"

require "dry-validation"
37 changes: 37 additions & 0 deletions test/test_i_do_not_need_interactor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,31 @@ def call(ctx)
end
end

class InteractorWithActiveModelContract
include Interactor
include IDoNotNeedInteractor::Contract::ActiveModel

def call(ctx); end

contract do
attribute :test
validates :test, presence: true
end
end

class InteractorWithDryValidationContract
include Interactor
include IDoNotNeedInteractor::Contract::DryValidation

def call(ctx); end

contract do
params do
required(:test)
end
end
end

def test_that_it_has_a_version_number
refute_nil ::IDoNotNeedInteractor::VERSION
end
Expand Down Expand Up @@ -166,4 +191,16 @@ def test_with_proc_is_possible_to_simulate_an_around_hook_also_in_composition #
assert_equal [7, 2, nil], outcome[:before_around]
assert_equal [7, 2, 9], outcome[:after_around]
end

def test_contract_active_model_integration
outcome = InteractorWithActiveModelContract.call

assert_equal ["Validation failed: Test can't be blank"], outcome.errors
end

def test_contract_dry_validation_integration
outcome = InteractorWithDryValidationContract.call

assert_equal [{ test: ["is missing"] }], outcome.errors
end
end

0 comments on commit 9c69e75

Please sign in to comment.